diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..f6f7040 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,74 @@ +name: Pull Request +on: + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: 20 + - name: Install web dependencies + run: | + cd web + npm ci + - name: Lint + run: | + cd web + npm run lint + build: + runs-on: ubuntu-latest + services: + postgres: + image: postgres + # Provide the password for postgres + env: + POSTGRES_PASSWORD: mysecretpassword + POSTGRES_DB: immowealth + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps tcp port 5432 on service container to the host + - 5432:5432 + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: 'gradle' + - name: Build with Gradle + run: ./gradlew build -Dquarkus.package.type=uber-jar + - name: Start application + run: | + nohup ./gradlew --console=plain quarkusDev & + for attempt in {1..20}; do sleep 1; if curl http://localhost:8080/; then echo ready; break; fi; echo waiting...; done + - name: Generate schema + run: curl http://localhost:8080/graphql/schema.graphql >> web/schema.graphql + - name: Update schema + run: | + echo "scalar Date" >> web/schema.graphql + echo "scalar BigInteger" >> web/schema.graphql + echo "scalar DateTime" >> web/schema.graphql + - name: Setup node + uses: actions/setup-node@v1 + with: + node-version: 20 + - name: Build web + run: | + cd web + npm ci + npm run compile + npm run build diff --git a/README.md b/README.md index ae39b24..1143654 100644 --- a/README.md +++ b/README.md @@ -36,14 +36,6 @@ docker-compose up -d There are many new features planned. You can find a little roadmap of planned future releases here: -### v1.1.0 -- [ ] Add extra information to objects -- [ ] More entities should be editable -- [ ] Update geo location by hand -- [ ] Split large numbers with dots -- [ ] Add credit rate note -- [ ] Migrate all tables to MUI datagrid - ### v1.2.0 - [ ] Add translations - [ ] Add different currencies diff --git a/src/main/kotlin/de/mathisburger/data/input/CreditRateInput.kt b/src/main/kotlin/de/mathisburger/data/input/CreditRateInput.kt index 48a19ac..9b481ca 100644 --- a/src/main/kotlin/de/mathisburger/data/input/CreditRateInput.kt +++ b/src/main/kotlin/de/mathisburger/data/input/CreditRateInput.kt @@ -21,5 +21,9 @@ data class CreditRateInput @JsonbCreator constructor( /** * The date of the booking */ - val date: Date + val date: Date, + /** + * The note of the booking + */ + val note: String? ) diff --git a/src/main/kotlin/de/mathisburger/data/input/RealEstateInput.kt b/src/main/kotlin/de/mathisburger/data/input/RealEstateInput.kt index 86cc475..a3cc7de 100644 --- a/src/main/kotlin/de/mathisburger/data/input/RealEstateInput.kt +++ b/src/main/kotlin/de/mathisburger/data/input/RealEstateInput.kt @@ -34,5 +34,50 @@ data class RealEstateInput @JsonbCreator constructor( * Date the real estate was bought */ @AdaptToScalar(Scalar.Date::class) - val dateBought: Date + val dateBought: Date, + /** + * Amount of rooms + */ + val rooms: Double, + /** + * Space for living + */ + val space: Double, + /** + * Type of the object + * e.g. house or flat (1. OG, EG) + */ + val objectType: String, + /** + * Construction year + */ + val constructionYear: Int, + /** + * Renovation year + */ + val renovationYear: Int?, + /** + * Energy efficiency class + */ + val energyEfficiency: String?, + /** + * Gross return of the object investment + */ + val grossReturn: Double?, + /** + * Garden exists + */ + val garden: Boolean, + /** + * Kitchen exists + */ + val kitchen: Boolean, + /** + * Heating type (wood, oil, gas) + */ + val heatingType: String, + /** + * Notes + */ + val notes: String? ) diff --git a/src/main/kotlin/de/mathisburger/data/input/UpdateCreditInput.kt b/src/main/kotlin/de/mathisburger/data/input/UpdateCreditInput.kt new file mode 100644 index 0000000..1968205 --- /dev/null +++ b/src/main/kotlin/de/mathisburger/data/input/UpdateCreditInput.kt @@ -0,0 +1,29 @@ +package de.mathisburger.data.input + +import jakarta.json.bind.annotation.JsonbCreator + +/** + * The update credit input for creating credits + */ +data class UpdateCreditInput @JsonbCreator constructor( + /** + * The ID of the credit that should be updated + */ + var id: Long, + /** + * The credit amount + */ + val amount: Long?, + /** + * The interest rate + */ + val interestRate: Double?, + /** + * The redemption rate + */ + val redemptionRate: Double?, + /** + * The credit bank + */ + val bank: String? +) \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/data/input/UpdateHousePriceChangeInput.kt b/src/main/kotlin/de/mathisburger/data/input/UpdateHousePriceChangeInput.kt new file mode 100644 index 0000000..5cf1303 --- /dev/null +++ b/src/main/kotlin/de/mathisburger/data/input/UpdateHousePriceChangeInput.kt @@ -0,0 +1,25 @@ +package de.mathisburger.data.input + +import jakarta.json.bind.annotation.JsonbCreator + +/** + * Update house prices input + */ +data class UpdateHousePriceChangeInput @JsonbCreator constructor( + /** + * The ID of the entity + */ + val id: Long, + /** + * The zip + */ + val zip: String?, + /** + * The year + */ + val year: Int?, + /** + * The change + */ + val change: Double?, +) diff --git a/src/main/kotlin/de/mathisburger/data/input/UpdateRealEstateInput.kt b/src/main/kotlin/de/mathisburger/data/input/UpdateRealEstateInput.kt index 80512ab..4ab149e 100644 --- a/src/main/kotlin/de/mathisburger/data/input/UpdateRealEstateInput.kt +++ b/src/main/kotlin/de/mathisburger/data/input/UpdateRealEstateInput.kt @@ -33,5 +33,58 @@ data class UpdateRealEstateInput @JsonbCreator constructor( * Date the real estate was bought */ @AdaptToScalar(Scalar.Date::class) - val dateBought: Date? + val dateBought: Date?, + /** + * Position lat + */ + val positionLat: Double?, + /** + * Position lon + */ + val positionLon: Double?, + /** + * Amount of rooms + */ + val rooms: Double?, + /** + * Space for living + */ + val space: Double?, + /** + * Type of the object + * e.g. house or flat (1. OG, EG) + */ + val objectType: String?, + /** + * Construction year + */ + val constructionYear: Int?, + /** + * Renovation year + */ + val renovationYear: Int?, + /** + * Energy efficiency class + */ + val energyEfficiency: String?, + /** + * Gross return of the object investment + */ + val grossReturn: Double?, + /** + * Garden exists + */ + val garden: Boolean?, + /** + * Kitchen exists + */ + val kitchen: Boolean?, + /** + * Heating type (wood, oil, gas) + */ + val heatingType: String?, + /** + * General notes + */ + val notes: String? ) \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/data/response/CreditResponse.kt b/src/main/kotlin/de/mathisburger/data/response/CreditResponse.kt index 67d12f8..9b62c48 100644 --- a/src/main/kotlin/de/mathisburger/data/response/CreditResponse.kt +++ b/src/main/kotlin/de/mathisburger/data/response/CreditResponse.kt @@ -18,5 +18,9 @@ data class CreditResponse @JsonbCreator constructor( /** * The credit rate accumulation steps */ - val creditRateCummulationSteps: List + val creditRateCummulationSteps: List, + /** + * Linked real estate object id + */ + val realEstateObjectId: Long ); diff --git a/src/main/kotlin/de/mathisburger/data/response/MapResponse.kt b/src/main/kotlin/de/mathisburger/data/response/MapResponse.kt new file mode 100644 index 0000000..37f678a --- /dev/null +++ b/src/main/kotlin/de/mathisburger/data/response/MapResponse.kt @@ -0,0 +1,22 @@ +package de.mathisburger.data.response + +import de.mathisburger.entity.RealEstateObject +import jakarta.json.bind.annotation.JsonbCreator + +/** + * Map response + */ +data class MapResponse @JsonbCreator constructor( + /** + * All objects that should be shown on map + */ + val objects: List, + /** + * Middle lat of the map + */ + val lat: Double, + /** + * Middle lon of the map + */ + val lon: Double +) diff --git a/src/main/kotlin/de/mathisburger/entity/Credit.kt b/src/main/kotlin/de/mathisburger/entity/Credit.kt index 5686983..7c47e4d 100644 --- a/src/main/kotlin/de/mathisburger/entity/Credit.kt +++ b/src/main/kotlin/de/mathisburger/entity/Credit.kt @@ -5,6 +5,7 @@ import jakarta.persistence.Entity import jakarta.persistence.GeneratedValue import jakarta.persistence.Id import jakarta.persistence.OneToMany +import jakarta.persistence.OneToOne import java.util.Date /** diff --git a/src/main/kotlin/de/mathisburger/entity/CreditRate.kt b/src/main/kotlin/de/mathisburger/entity/CreditRate.kt index 7b0dfcd..acad71a 100644 --- a/src/main/kotlin/de/mathisburger/entity/CreditRate.kt +++ b/src/main/kotlin/de/mathisburger/entity/CreditRate.kt @@ -27,4 +27,9 @@ class CreditRate { * The amount of money */ var amount: Double? = null; + + /** + * The note of the credit rate + */ + var note: String? = null; } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/entity/RealEstateObject.kt b/src/main/kotlin/de/mathisburger/entity/RealEstateObject.kt index 9d130f3..f033eaa 100644 --- a/src/main/kotlin/de/mathisburger/entity/RealEstateObject.kt +++ b/src/main/kotlin/de/mathisburger/entity/RealEstateObject.kt @@ -51,9 +51,65 @@ class RealEstateObject { */ var positionLon: Double? = null + /** + * Amount of rooms + */ + var rooms: Double? = null + + /** + * Space for living + */ + var space: Double? = null + + /** + * Type of the object + * e.g. house or flat (1. OG, EG) + */ + var objectType: String? = null + + /** + * Construction year + */ + var constructionYear: Int? = null + + /** + * Renovation year + */ + var renovationYear: Int? = null + + /** + * Energy efficiency class + */ + var energyEfficiency: String? = null + + /** + * Gross return of the object investment + */ + var grossReturn: Double? = null + + /** + * Garden exists + */ + var garden: Boolean? = null + + /** + * Kitchen exists + */ + var kitchen: Boolean? = null + + /** + * Heating type (wood, oil, gas) + */ + var heatingType: String? = null + + /** + * All multiline notes of the + */ + var notes: String? = null; + /** * The linked credit */ - @OneToOne + @OneToOne(cascade = [CascadeType.ALL]) var credit: Credit? = null } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/repository/RealEstateRepository.kt b/src/main/kotlin/de/mathisburger/repository/RealEstateRepository.kt index 24285fe..9b40de6 100644 --- a/src/main/kotlin/de/mathisburger/repository/RealEstateRepository.kt +++ b/src/main/kotlin/de/mathisburger/repository/RealEstateRepository.kt @@ -1,12 +1,22 @@ package de.mathisburger.repository +import de.mathisburger.entity.Credit import de.mathisburger.entity.RealEstateObject import io.quarkus.hibernate.orm.panache.PanacheRepository import jakarta.enterprise.context.ApplicationScoped +import java.util.Optional /** * Real estate repository */ @ApplicationScoped class RealEstateRepository : PanacheRepository { + + /** + * Gets a real estate object by credit + */ + fun getByCredit(credit: Credit): RealEstateObject { + println(credit.id); + return find("from RealEstateObject o where o.credit.id = ?1", credit.id).firstResult() + } } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/resources/CreditResource.kt b/src/main/kotlin/de/mathisburger/resources/CreditResource.kt index f3646cc..f43945e 100644 --- a/src/main/kotlin/de/mathisburger/resources/CreditResource.kt +++ b/src/main/kotlin/de/mathisburger/resources/CreditResource.kt @@ -1,6 +1,7 @@ package de.mathisburger.resources import de.mathisburger.data.input.CreditRateInput +import de.mathisburger.data.input.UpdateCreditInput import de.mathisburger.data.response.CreditResponse import de.mathisburger.entity.enum.AutoPayInterval import de.mathisburger.service.CreditService @@ -25,7 +26,7 @@ class CreditResource { */ @Mutation fun addCreditRate(input: CreditRateInput): Boolean { - this.creditService.addCreditRate(input.id, input.rate, input.date); + this.creditService.addCreditRate(input.id, input.rate, input.date, input.note); return true; } @@ -70,4 +71,12 @@ class CreditResource { fun getCredit(id: Long): CreditResponse { return this.creditService.getCredit(id); } + + /** + * Updates a credit + */ + @Mutation + fun updateCredit(input: UpdateCreditInput): CreditResponse { + return this.creditService.updateCredit(input); + } } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/resources/HousePriceChangeResource.kt b/src/main/kotlin/de/mathisburger/resources/HousePriceChangeResource.kt index 8334f1c..8db444a 100644 --- a/src/main/kotlin/de/mathisburger/resources/HousePriceChangeResource.kt +++ b/src/main/kotlin/de/mathisburger/resources/HousePriceChangeResource.kt @@ -1,5 +1,6 @@ package de.mathisburger.resources +import de.mathisburger.data.input.UpdateHousePriceChangeInput import de.mathisburger.entity.HousePriceChange import de.mathisburger.service.HousePriceChangeService import jakarta.inject.Inject @@ -56,4 +57,12 @@ class HousePriceChangeResource { fun getAllHousePricesChangeByZip(zip: String): List { return this.housePriceChangeService.getAllChangesWithZip(zip); } + + /** + * Updates the house price change + */ + @Mutation + fun updateHousePriceChange(input: UpdateHousePriceChangeInput): HousePriceChange { + return this.housePriceChangeService.updateHousePrices(input); + } } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/resources/MapResource.kt b/src/main/kotlin/de/mathisburger/resources/MapResource.kt new file mode 100644 index 0000000..4795dfa --- /dev/null +++ b/src/main/kotlin/de/mathisburger/resources/MapResource.kt @@ -0,0 +1,31 @@ +package de.mathisburger.resources + +import de.mathisburger.data.response.MapResponse +import de.mathisburger.repository.RealEstateRepository +import jakarta.inject.Inject +import org.eclipse.microprofile.graphql.GraphQLApi +import org.eclipse.microprofile.graphql.Query + +@GraphQLApi +class MapResource { + + @Inject + lateinit var realEstateRepository: RealEstateRepository; + + + @Query + fun getMapObjects(): MapResponse { + val objs = this.realEstateRepository.listAll(); + var lonSum = 0.0; + var latSum = 0.0; + for (obj in objs) { + lonSum += obj.positionLon!!; + latSum += obj.positionLat!!; + } + return MapResponse( + objs, + latSum / objs.size, + lonSum / objs.size + ); + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/scheduler/CreditAutoBookingScheduler.kt b/src/main/kotlin/de/mathisburger/scheduler/CreditAutoBookingScheduler.kt index e09cb7e..37eaaff 100644 --- a/src/main/kotlin/de/mathisburger/scheduler/CreditAutoBookingScheduler.kt +++ b/src/main/kotlin/de/mathisburger/scheduler/CreditAutoBookingScheduler.kt @@ -3,6 +3,7 @@ package de.mathisburger.scheduler import de.mathisburger.entity.CreditRate import de.mathisburger.repository.CreditRepository import de.mathisburger.util.AutoBookingUtils +import de.mathisburger.util.DateUtils import jakarta.enterprise.context.ApplicationScoped import io.quarkus.scheduler.Scheduled; import jakarta.inject.Inject @@ -33,6 +34,7 @@ class CreditAutoBookingScheduler { val rate = CreditRate(); rate.date = Calendar.getInstance().time; rate.amount = credit.autoPayAmount; + rate.note = "[AUTOBOOKING] Buchung vom " + DateUtils.getTodayFormat(); this.entityManager.persist(rate); credit.nextCreditRate = AutoBookingUtils.getNextAutoPayIntervalDate(credit.autoPayInterval!!) credit.rates.add(rate); diff --git a/src/main/kotlin/de/mathisburger/service/CreditService.kt b/src/main/kotlin/de/mathisburger/service/CreditService.kt index 4cf2c37..5b9f076 100644 --- a/src/main/kotlin/de/mathisburger/service/CreditService.kt +++ b/src/main/kotlin/de/mathisburger/service/CreditService.kt @@ -1,12 +1,15 @@ package de.mathisburger.service +import de.mathisburger.data.input.UpdateCreditInput import de.mathisburger.data.response.CreditResponse import de.mathisburger.entity.Credit import de.mathisburger.entity.CreditRate +import de.mathisburger.entity.RealEstateObject import de.mathisburger.entity.enum.AutoPayInterval import de.mathisburger.exception.DateNotAllowedException import de.mathisburger.repository.CreditRateRepository import de.mathisburger.repository.CreditRepository +import de.mathisburger.repository.RealEstateRepository import de.mathisburger.util.AutoBookingUtils import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject @@ -30,15 +33,19 @@ class CreditService { @Inject lateinit var creditRateRepository: CreditRateRepository; + @Inject + lateinit var realEstateRepository: RealEstateRepository; + /** * Adds a new credit rate * * @param id The ID of the credit * @param rate The credit rate amount * @param date The date of booking + * @param note The note of the booking */ @Transactional - fun addCreditRate(id: Long, rate: Double, date: Date) { + fun addCreditRate(id: Long, rate: Double, date: Date, note: String?) { if (date.after(Date())) { throw DateNotAllowedException("Date in future is not allowed"); } @@ -46,6 +53,7 @@ class CreditService { val creditRate = CreditRate() creditRate.date = date; creditRate.amount = rate; + creditRate.note = note; this.entityManager.persist(creditRate); credit.rates.add(creditRate); this.entityManager.persist(credit); @@ -71,6 +79,25 @@ class CreditService { return this.getResponseObject(credit); } + /** + * Updates a credit. + * + * @param input The update input + * @return The updated credit + */ + @Transactional + fun updateCredit(input: UpdateCreditInput): CreditResponse { + val credit = this.creditRepository.findById(input.id); + credit.amount = input.amount ?: credit.amount; + credit.interestRate = input.interestRate ?: credit.interestRate; + credit.redemptionRate = input.redemptionRate ?: credit.redemptionRate; + credit.bank = input.bank ?: credit.bank; + this.entityManager.persist(credit); + this.entityManager.flush(); + return this.getCredit(credit.id!!); + + } + /** * Configures auto booking * @@ -128,6 +155,7 @@ class CreditService { sum += rate.amount!!; list.add(sum); } - return CreditResponse(credit, sum, list); + val res = + return CreditResponse(credit, sum, list, this.realEstateRepository.getByCredit(credit).id!!); } } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/service/HousePriceChangeService.kt b/src/main/kotlin/de/mathisburger/service/HousePriceChangeService.kt index 98da9d0..cd9c917 100644 --- a/src/main/kotlin/de/mathisburger/service/HousePriceChangeService.kt +++ b/src/main/kotlin/de/mathisburger/service/HousePriceChangeService.kt @@ -1,5 +1,6 @@ package de.mathisburger.service +import de.mathisburger.data.input.UpdateHousePriceChangeInput import de.mathisburger.entity.HousePriceChange import de.mathisburger.repository.HousePriceChangeRepository import jakarta.enterprise.context.ApplicationScoped @@ -65,4 +66,20 @@ class HousePriceChangeService { fun getAllChangesWithZip(zip: String): List { return this.housePriceChangeRepository.findByZip(zip); } + + /** + * Updates a house price + * + * @param input The update input + */ + @Transactional + fun updateHousePrices(input: UpdateHousePriceChangeInput): HousePriceChange { + val obj = this.housePriceChangeRepository.findById(input.id); + obj.change = input.change ?: obj.change; + obj.zip = input.zip ?: obj.zip; + obj.year = input.year ?: obj.year; + this.entityManager.persist(obj); + this.entityManager.flush(); + return obj; + } } \ No newline at end of file diff --git a/src/main/kotlin/de/mathisburger/service/RealEstateService.kt b/src/main/kotlin/de/mathisburger/service/RealEstateService.kt index 58d3c90..3129617 100644 --- a/src/main/kotlin/de/mathisburger/service/RealEstateService.kt +++ b/src/main/kotlin/de/mathisburger/service/RealEstateService.kt @@ -57,6 +57,17 @@ class RealEstateService { obj.initialValue = input.initialValue; obj.credit = credit; obj.dateBought = input.dateBought; + obj.rooms = input.rooms; + obj.space = input.space; + obj.objectType = input.objectType; + obj.constructionYear = input.constructionYear; + obj.renovationYear = input.renovationYear; + obj.energyEfficiency = input.energyEfficiency; + obj.grossReturn = input.grossReturn; + obj.garden = input.garden; + obj.kitchen = input.kitchen; + obj.heatingType = input.heatingType; + obj.notes = input.notes; val results = this.geocodingApi.getLocations(input.streetAndHouseNr + " " + input.zip + " " + input.city); if (results.isNotEmpty()) { @@ -115,6 +126,20 @@ class RealEstateService { obj.initialValue = input.initialValue ?: obj.initialValue; obj.city = input.city ?: obj.city; obj.streetAndHouseNr = input.streetAndHouseNr ?: obj.streetAndHouseNr; + obj.positionLat = input.positionLat ?: obj.positionLat; + obj.positionLon = input.positionLon ?: obj.positionLon; + obj.rooms = input.rooms ?: obj.rooms; + obj.space = input.space ?: obj.space; + obj.objectType = input.objectType ?: obj.objectType; + obj.constructionYear = input.constructionYear ?: obj.constructionYear; + obj.renovationYear = input.renovationYear ?: obj.renovationYear; + obj.energyEfficiency = input.energyEfficiency ?: obj.energyEfficiency; + obj.grossReturn = input.grossReturn ?: obj.grossReturn; + obj.garden = input.garden ?: obj.garden; + obj.kitchen = input.kitchen ?: obj.kitchen; + obj.heatingType = input.heatingType ?: obj.heatingType; + obj.notes = input.notes ?: obj.notes; + this.entityManager.persist(obj); this.entityManager.flush(); return this.getObject(obj.id!!, 10); diff --git a/src/main/kotlin/de/mathisburger/util/DateUtils.kt b/src/main/kotlin/de/mathisburger/util/DateUtils.kt index 0da7232..52162275 100644 --- a/src/main/kotlin/de/mathisburger/util/DateUtils.kt +++ b/src/main/kotlin/de/mathisburger/util/DateUtils.kt @@ -20,5 +20,15 @@ class DateUtils { cal.time = date; return cal.get(Calendar.YEAR); } + + /** + * Gets today formatted as date string + * + * @return Formatted date string + */ + fun getTodayFormat(): String { + var cal = Calendar.getInstance(); + return "" + cal.get(Calendar.DAY_OF_MONTH) + "/" + cal.get(Calendar.MONTH) + "/" + cal.get(Calendar.YEAR); + } } } \ No newline at end of file diff --git a/web/app/credits/details/pageComponent.tsx b/web/app/credits/details/pageComponent.tsx index 9656102..882e8f0 100644 --- a/web/app/credits/details/pageComponent.tsx +++ b/web/app/credits/details/pageComponent.tsx @@ -7,7 +7,8 @@ import AddCreditRateModal from "@/components/object/modal/AddCreditRateModal"; import CreditRateList from "@/components/credit/CreditRateList"; import CreditDataTab from "@/components/credit/CreditDataTab"; import ConfigureCreditAutoPayModal from "@/components/credit/ConfigureCreditAutoPayModal"; -import {useParams, useSearchParams} from "next/navigation"; +import {useParams, useRouter, useSearchParams} from "next/navigation"; +import MapsHomeWorkIcon from '@mui/icons-material/MapsHomeWork'; const CreditDetailsPage = () => { @@ -16,6 +17,7 @@ const CreditDetailsPage = () => { const {data, loading} = useGetCreditQuery({ variables: {id: parseInt(id, 10)} }); + const router = useRouter(); const [creditRateModalOpen, setCreditRateModalOpen] = useState(false); const [configureAutoBookingModal, setConfigureAutoBookingModal] = useState(false); @@ -59,6 +61,11 @@ const CreditDetailsPage = () => { Automatische Kreditbuchung + + + diff --git a/web/app/credits/page.tsx b/web/app/credits/page.tsx index 95cc7ec..f0a2d15 100644 --- a/web/app/credits/page.tsx +++ b/web/app/credits/page.tsx @@ -3,56 +3,85 @@ import {useGetCreditsQuery} from "@/generated/graphql"; import {Button, Divider, Grid, Table, Typography} from "@mui/joy"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import {useRouter} from "next/navigation"; +import MapsHomeWorkIcon from '@mui/icons-material/MapsHomeWork'; +import {formatNumber} from "@/utilts/formatter"; +import {useMemo} from "react"; +import {GridColDef, GridRenderCellParams, GridValueFormatterParams, GridValueGetterParams} from "@mui/x-data-grid"; +import EntityList from "@/components/EntityList"; const CreditsPage = () => { const {data} = useGetCreditsQuery(); const router = useRouter(); + const cols = useMemo(() => [ + { + field: 'id', + headerName: 'ID', + valueGetter: ({row}: GridValueGetterParams) => row.credit.id, + }, + { + field: 'amount', + headerName: 'Summe', + width: 200, + valueGetter: ({row}: GridValueGetterParams) => row.credit.amount, + valueFormatter: ({value}: GridValueFormatterParams) => `${formatNumber(value)}€` + }, + { + field: 'creditRateSum', + headerName: 'Getilgt', + width: 200, + valueFormatter: ({value}: GridValueFormatterParams) => `${formatNumber(value)}€` + }, + { + field: 'interestRate', + headerName: 'Zins', + width: 100, + valueGetter: ({row}: GridValueGetterParams) => row.credit.interestRate, + valueFormatter: ({value}: GridValueFormatterParams) => `${formatNumber(value)}%` + }, + { + field: 'redemptionRate', + headerName: 'Tilgung', + width: 100, + valueGetter: ({row}: GridValueGetterParams) => row.credit.redemptionRate, + valueFormatter: ({value}: GridValueFormatterParams) => `${formatNumber(value)}%` + }, + { + field: 'bank', + headerName: 'Bank', + width: 200, + valueGetter: ({row}: GridValueGetterParams) => row.credit.bank, + }, + { + field: 'actions', + headerName: 'Aktionen', + width: 400, + renderCell: ({row}: GridRenderCellParams) => ( + + + + + + + + + ) + } + ], [router]); + return ( <> Kredite - - - - - - - - - - - - - - {data?.allCredits.map((credit: any) => ( - - - - - - - - - - ))} - -
IDSummeGetilgtZinsTilgungBankAktionen
{credit?.credit.id}{credit?.credit.amount}€{credit?.creditRateSum}€{credit?.credit.interestRate}%{credit?.credit.redemptionRate}%{credit?.credit.bank} - - - - - -
+ ); } diff --git a/web/app/housingPrices/page.tsx b/web/app/housingPrices/page.tsx index e015fc2..1d9a405 100644 --- a/web/app/housingPrices/page.tsx +++ b/web/app/housingPrices/page.tsx @@ -6,16 +6,22 @@ import { useAllHousePriceChangesQuery, useDeleteHousePriceChangeMutation, useDeleteRealEstateMutation } from "@/generated/graphql"; -import {useMemo, useState} from "react"; -import {Divider, Select, Typography, Option, Table, Button, Autocomplete} from "@mui/joy"; +import {useCallback, useMemo, useState} from "react"; +import {Divider, Select, Typography, Option, Table, Button, Autocomplete, Grid} from "@mui/joy"; import {useRouter} from "next/navigation"; import DeleteIcon from "@mui/icons-material/Delete"; +import UpdateHousePriceModal from "@/components/housePriceChanges/UpdateHousePriceModal"; +import EditIcon from "@mui/icons-material/Edit"; +import {formatNumber} from "@/utilts/formatter"; +import {GridColDef, GridValueFormatterParams} from "@mui/x-data-grid"; +import EntityList from "@/components/EntityList"; const HousingPrices = () => { const {data} = useAllHousePriceChangesQuery(); const router = useRouter(); + const [updateObject, setUpdateObject] = useState(null); const [deleteMutation, {loading: deleteLoading}] = useDeleteHousePriceChangeMutation({ refetchQueries: [ @@ -25,14 +31,54 @@ const HousingPrices = () => { ] }); - const deleteObject = async (id: string) => { + const deleteObject = useCallback(async (id: string) => { const result = await deleteMutation({ variables: {id: parseInt(`${id}`)} }); if (result.errors === undefined) { router.push('/objects'); } - } + }, [router, deleteMutation]); + + const cols = useMemo(() => [ + { + field: 'id', + headerName: 'ID' + }, + { + field: 'change', + headerName: 'Änderung', + width: 200, + valueFormatter: ({value}: GridValueFormatterParams) => `${formatNumber(value)}€` + }, + { + field: 'zip', + headerName: 'Postleitzahl' + }, + { + field: 'year', + headerName: 'Jahr' + }, + { + field: 'actions', + width: 400, + headerName: 'Aktionen', + renderCell: ({row}) => ( + + + + + + + + + ) + } + ], [deleteLoading, deleteObject]); const [filter, setFilter] = useState(''); @@ -71,38 +117,13 @@ const HousingPrices = () => { inputValue={filter} onInputChange={(_, f) => setFilter(f ?? '')} /> - - - - - - - - - - - - {filtered.map((change) => ( - - - - - - - - ))} - -
IDÄnderungZipYearActions
{change.id}{change.change}%{change.zip}{change.year} - -
+ + {updateObject && ( + setUpdateObject(null)} + housePrice={updateObject} + /> + )} ); } diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 404bd2b..88068bb 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -1,6 +1,5 @@ 'use client'; import { Inter } from "next/font/google"; -import {ApolloClient, ApolloLink, ApolloProvider, HttpLink, InMemoryCache} from "@apollo/client"; import BaseLayout from "@/components/BaseLayout"; import {ReactNode} from "react"; import { LocalizationProvider } from '@mui/x-date-pickers'; diff --git a/web/app/map/mapPageComponent.tsx b/web/app/map/mapPageComponent.tsx index 7d3a72d..9ce0893 100644 --- a/web/app/map/mapPageComponent.tsx +++ b/web/app/map/mapPageComponent.tsx @@ -1,11 +1,17 @@ 'use client'; import React, {useEffect, useRef, useState} from "react"; -import {MapContainer, Marker, Popup, TileLayer} from "react-leaflet"; +import {MapContainer, Marker, Popup, TileLayer, useMapEvents} from "react-leaflet"; import "leaflet/dist/leaflet.css"; -import {useGetMapObjectsQuery} from "@/generated/graphql"; -import {Button, CircularProgress, Typography} from "@mui/joy"; +import {MapObjectFragment, useGetMapObjectsQuery} from "@/generated/graphql"; +import {Alert, Button, CircularProgress, Grid, Typography} from "@mui/joy"; import {useRouter} from "next/navigation"; -import LoadingSpinner from "@/components/LoadingSpinner"; +import MapClickListener from "@/components/map/MapClickListener"; + +export interface ChangeElement { + element: MapObjectFragment; + clientX: number; + clientY: number; +} const MapPage = () => { @@ -15,34 +21,68 @@ const MapPage = () => { const latitude = 48.7760256; const longitude = 11.4456758; const [loaded, setLoaded] = useState(false); + const [changeElement, setChangeElement] = useState(null); + + useEffect(() => { setLoaded(true); }, []); if (loaded) { return ( - - - {data?.allObjects.map((obj) => ( - - - - Objekt {obj?.id} - - - {obj?.streetAndHouseNr}, {obj?.zip} {obj?.city} - - - - - ))} - {/* Additional map layers or components can be added here */} - + <> + {changeElement && ( + + Du befindest dich im Korrektur-Modus. + + + )} + + + {changeElement && ( + setChangeElement(null)} /> + )} + {changeElement === null && ( + data?.mapObjects.objects.map((obj) => ( + + + + Objekt {obj?.id} + + + {obj?.streetAndHouseNr}, {obj?.zip} {obj?.city} + + + + + + + + + + + + )) + )} + {/* Additional map layers or components can be added here */} + + ); } return ; diff --git a/web/app/objects/new/page.tsx b/web/app/objects/new/page.tsx index 1e3bd55..e5256ce 100644 --- a/web/app/objects/new/page.tsx +++ b/web/app/objects/new/page.tsx @@ -1,5 +1,17 @@ 'use client'; -import {Button, Card, CardContent, Divider, FormControl, FormLabel, Grid, Input, Stack, Typography} from "@mui/joy"; +import { + Button, + Card, + CardContent, Checkbox, + Divider, + FormControl, + FormLabel, + Grid, + Input, + Stack, + Textarea, + Typography +} from "@mui/joy"; import {DatePicker} from "@mui/x-date-pickers"; import {useRouter} from "next/navigation"; import {FormEvent} from "react"; @@ -28,13 +40,22 @@ const NewObject = () => { dateBought: new Date(formData.get("dateBought") as string), initialValue: parseInt(`${formData.get("initialPrice")}`, 10), streetAndHouseNr: formData.get("streetAndHouseNr") as string, - zip: formData.get("zip") as string + zip: formData.get("zip") as string, + rooms: parseFloat(`${formData.get('rooms')}`), + space: parseFloat(`${formData.get('space')}`), + objectType: `${formData.get('objectType')}`, + constructionYear: parseInt(`${formData.get('space')}`, 10), + renovationYear: formData.get('renovationYear') ? parseInt(`${formData.get('space')}`, 10) : null, + energyEfficiency: formData.get('energyEfficiency') ? `${formData.get('energyEfficiency')}` : null, + grossReturn: parseFloat(`${formData.get('grossReturn')}`), + garden: formData.get('garden') === 'true', + kitchen: formData.get('kitchen') === 'true', + heatingType: `${formData.get('heatingType')}`, + notes: formData.get('notes') ? `${formData.get('objectType')}` : null, }} }); if (result.errors === undefined) { - router.push(`/objects/${result.data?.createRealEstate.id}`); - } else { - alert(result.errors[0].message); + router.push(`/objects/details?id=${result.data?.createRealEstate.id}`); } } @@ -50,7 +71,7 @@ const NewObject = () => { Grunddaten - Straße und Hausnummer + Straße und Hausnummer * { - Postleitzahl + Postleitzahl * { - Ort + Ort * { - Kaufpreis + Kaufpreis * { /> - Kaufdatum + Kaufdatum * + + Räume * + + + + Nutzfläche * + + + + Wohnungstyp * + + + + Baujahr * + + + + Renoviert (Jahr) + + + + Energie Effizienz + + + + Rendite * + + + + Heiztyp * + + + + + + Notizen +