Damit ist Etappe 1 abgeschlossen und betriebsfertig dokumentiert. README mit beiden Betriebswegen. Docker Compose auf eigenem Server ist der Hauptweg; Vercel und Neon sind vollstaendig beschrieben, aber mit einer klaren Begruendung, warum ich davon abrate: beide Anbieter sind US-Unternehmen und unterliegen auch bei EU-Region potenziell dem CLOUD-Act. Bei Gesundheitsdaten einer nicht entscheidungsfaehigen Person ist das eine Abwaegung, die man bewusst treffen muss. Reverse-Proxy-Beispiele fuer Caddy, nginx und Traefik - der Proxy selbst ist nicht Teil des Compose-Setups, weil er auf dem Zielserver schon steht. Die Datenbank hat keine Portfreigabe. Sicherung: pg_dump schreibt direkt in die Verschluesselung, der Klartext landet nie auf der Platte. Danach eine Selbstpruefung - laesst sich die Datei nicht wieder entschluesseln, endet der Lauf mit Fehler statt mit einem stillen Scheinerfolg. Der jeweils erste Stand eines Monats bleibt dauerhaft stehen. Die README sagt ausdruecklich, was das Backup NICHT enthaelt: den Schluessel ENC_KEY_V1 und die Dateien im Objektspeicher. Dazu eine Anleitung, die Wiederherstellung einmal im Jahr zu ueben - mit dem letzten Schritt, in der Anwendung nachzusehen, ob die Diagnosen im Klartext erscheinen. Erst dann stimmt nicht nur die Datenbank, sondern auch der Schluessel. Verarbeitungsverzeichnis und Datenschutzhinweise als ausfuellbare Textbausteine. Die Hinweise sind in einfacher Sprache geschrieben - sie sollen der betreuten Person vorgelesen werden koennen. Die Seite /datenschutz liest denselben Text aus docs/, damit es nicht zwei Fassungen gibt, die auseinanderlaufen. Dockerfile in drei Stufen mit Tesseract, deutschen Sprachdaten und poppler-utils. Zeitzone Europe/Berlin ist gesetzt - ohne sie rechnet der Server in UTC und verschiebt Fristen um einen Tag. Etappe: 1 Phase: Fundament
717 lines
23 KiB
TypeScript
717 lines
23 KiB
TypeScript
'use server'
|
|
|
|
import { revalidatePath } from 'next/cache'
|
|
import { redirect } from 'next/navigation'
|
|
import type { Prisma } from '@prisma/client'
|
|
import { db, nichtGeloescht } from '@/lib/db'
|
|
import { erfordereZugang, ZugriffVerweigert } from '@/lib/auth/sitzung'
|
|
import { aenderungErmitteln, protokollieren } from '@/lib/audit'
|
|
import { oeffnen, sealen } from '@/lib/crypto/feld'
|
|
import { heuteAlsTag, tagesdatum } from '@/lib/fristen/datum'
|
|
import { leerstandCheckliste, naechsteFaelligkeit } from '@/lib/immobilie/leerstand-checkliste'
|
|
import { INVENTAR_WERTQUELLE, inventarNotizLesen, inventarNotizSchreiben } from '@/lib/immobilie/inventar'
|
|
import {
|
|
BelastungSchema,
|
|
CheckpunktSchema,
|
|
ImmobilieSchema,
|
|
InventarSchema,
|
|
StatusSozialhilfeSchema,
|
|
WertstandSchema,
|
|
} from '@/lib/validierung/immobilie'
|
|
import { formularLesen, haken, zodFehler, type Formularzustand } from '@/lib/validierung/substanz'
|
|
|
|
/**
|
|
* Server Actions des Immobilienmoduls.
|
|
*
|
|
* Jede Aktion prueft den Zugriff serverseitig gegen die betreuteId, jede
|
|
* validiert mit Zod, jede protokolliert, jede aktualisiert die betroffene
|
|
* Seite. Fehler werden als Zustand zurueckgegeben und nicht geworfen -
|
|
* ein geworfener Fehler zeigt eine Fehlerseite und verliert die Eingaben.
|
|
*/
|
|
|
|
const MODUL = 'IMMOBILIE' as const
|
|
|
|
async function zugangOderFehler(betreuteId: string) {
|
|
return erfordereZugang(betreuteId, MODUL, 'schreiben')
|
|
}
|
|
|
|
function alsFehler(fehler: unknown): Formularzustand {
|
|
if (fehler instanceof ZugriffVerweigert) return { fehler: fehler.message }
|
|
console.error('[immobilie]', fehler)
|
|
return {
|
|
fehler: 'Das Speichern hat nicht geklappt. Bitte noch einmal versuchen.',
|
|
}
|
|
}
|
|
|
|
/** Laedt eine Immobilie und stellt sicher, dass sie zur betreuten Person gehoert. */
|
|
async function immobilieLaden(betreuteId: string, immobilieId: string) {
|
|
const immobilie = await db.immobilie.findFirst({
|
|
where: { id: immobilieId, betreuteId, ...nichtGeloescht },
|
|
})
|
|
if (!immobilie) throw new ZugriffVerweigert('Dieses Objekt gibt es nicht.')
|
|
return immobilie
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Stammdaten
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export async function immobilieSpeichern(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
let ziel: string | null = null
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
|
|
const gelesen = ImmobilieSchema.safeParse({
|
|
...formularLesen(formular),
|
|
checklisteUebernehmen: haken(formular, 'checklisteUebernehmen'),
|
|
})
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const daten = gelesen.data
|
|
const gemeinsam = {
|
|
bezeichnung: daten.bezeichnung,
|
|
strasse: daten.strasse,
|
|
plz: daten.plz,
|
|
ort: daten.ort,
|
|
grundbuchamt: daten.grundbuchamt,
|
|
grundbuchBlatt: daten.grundbuchBlatt,
|
|
gemarkung: daten.gemarkung,
|
|
flurstueck: daten.flurstueck,
|
|
eigentumsanteil: daten.eigentumsanteil,
|
|
wohnflaecheQm: daten.wohnflaecheQm,
|
|
grundstueckQm: daten.grundstueckQm,
|
|
baujahr: daten.baujahr,
|
|
bewohntStatus: daten.bewohntStatus,
|
|
leerstandSeit: daten.leerstandSeit,
|
|
...sealen('Immobilie', { notizen: daten.notizen }),
|
|
} satisfies Prisma.ImmobilieUncheckedUpdateInput
|
|
|
|
let zielId = immobilieId
|
|
|
|
if (immobilieId) {
|
|
const vorher = await immobilieLaden(betreuteId, immobilieId)
|
|
const nachher = await db.immobilie.update({ where: { id: immobilieId }, data: gemeinsam })
|
|
|
|
await protokollieren({
|
|
aktion: 'GEAENDERT',
|
|
entitaet: 'Immobilie',
|
|
entitaetId: nachher.id,
|
|
bezeichnung: nachher.bezeichnung,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
aenderung: aenderungErmitteln(
|
|
oeffnen('Immobilie', vorher),
|
|
oeffnen('Immobilie', nachher),
|
|
),
|
|
})
|
|
} else {
|
|
const neu = await db.immobilie.create({ data: { betreuteId, ...gemeinsam } })
|
|
zielId = neu.id
|
|
|
|
await protokollieren({
|
|
aktion: 'ERSTELLT',
|
|
entitaet: 'Immobilie',
|
|
entitaetId: neu.id,
|
|
bezeichnung: neu.bezeichnung,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
}
|
|
|
|
if (daten.checklisteUebernehmen && daten.bewohntStatus === 'leerstehend') {
|
|
await checklisteAnlegen(betreuteId, zielId, daten.leerstandSeit)
|
|
}
|
|
|
|
revalidatePath('/immobilie')
|
|
revalidatePath(`/immobilie/${zielId}`)
|
|
ziel = `/immobilie/${zielId}`
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
|
|
// Der Umweg ueber die Variable ist Absicht: redirect() wirft, und in
|
|
// einem try-Block landete der Wurf im catch und wuerde als Fehler
|
|
// ausgegeben statt weiterzuleiten.
|
|
redirect(ziel)
|
|
}
|
|
|
|
export async function immobilieLoeschen(betreuteId: string, immobilieId: string): Promise<void> {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
const immobilie = await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
await db.immobilie.update({
|
|
where: { id: immobilieId },
|
|
data: { geloeschtAm: new Date() },
|
|
})
|
|
|
|
await protokollieren({
|
|
aktion: 'GELOESCHT',
|
|
entitaet: 'Immobilie',
|
|
entitaetId: immobilieId,
|
|
bezeichnung: immobilie.bezeichnung,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath('/immobilie')
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Status im Sozialhilfeverfahren
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export async function statusSozialhilfeSpeichern(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
const vorher = await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const gelesen = StatusSozialhilfeSchema.safeParse(formularLesen(formular))
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const nachher = await db.immobilie.update({
|
|
where: { id: immobilieId },
|
|
data: {
|
|
statusSozialhilfe: gelesen.data.statusSozialhilfe,
|
|
...sealen('Immobilie', { statusBegruendung: gelesen.data.begruendung }),
|
|
},
|
|
})
|
|
|
|
await protokollieren({
|
|
aktion: 'GEAENDERT',
|
|
entitaet: 'Immobilie',
|
|
entitaetId: immobilieId,
|
|
bezeichnung: `${nachher.bezeichnung} — Status im Sozialhilfeverfahren`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
aenderung: aenderungErmitteln(
|
|
{ statusSozialhilfe: vorher.statusSozialhilfe },
|
|
{ statusSozialhilfe: nachher.statusSozialhilfe },
|
|
),
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}`)
|
|
return { erfolg: 'Status gespeichert.' }
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Belastungen
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export async function belastungSpeichern(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
const belastungId = String(formular.get('belastungId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
const immobilie = await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const gelesen = BelastungSchema.safeParse(formularLesen(formular))
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const daten = {
|
|
art: gelesen.data.art,
|
|
abteilung: gelesen.data.abteilung,
|
|
laufendeNr: gelesen.data.laufendeNr,
|
|
glaeubiger: gelesen.data.glaeubiger,
|
|
betragCent: gelesen.data.betrag,
|
|
bemerkung: gelesen.data.bemerkung,
|
|
geloeschtImGrundbuchAm: gelesen.data.geloeschtImGrundbuchAm,
|
|
}
|
|
|
|
if (belastungId) {
|
|
const vorhanden = await db.belastung.findFirst({
|
|
where: { id: belastungId, immobilieId },
|
|
})
|
|
if (!vorhanden) return { fehler: 'Diese Belastung gibt es nicht.' }
|
|
|
|
await db.belastung.update({ where: { id: belastungId }, data: daten })
|
|
await protokollieren({
|
|
aktion: 'GEAENDERT',
|
|
entitaet: 'Belastung',
|
|
entitaetId: belastungId,
|
|
bezeichnung: `${daten.art} · ${immobilie.bezeichnung}`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
aenderung: aenderungErmitteln(vorhanden, { ...vorhanden, ...daten }),
|
|
})
|
|
} else {
|
|
const neu = await db.belastung.create({ data: { immobilieId, ...daten } })
|
|
await protokollieren({
|
|
aktion: 'ERSTELLT',
|
|
entitaet: 'Belastung',
|
|
entitaetId: neu.id,
|
|
bezeichnung: `${daten.art} · ${immobilie.bezeichnung}`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
}
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}`)
|
|
return { erfolg: 'Belastung gespeichert.' }
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
export async function belastungLoeschen(
|
|
betreuteId: string,
|
|
immobilieId: string,
|
|
belastungId: string,
|
|
): Promise<void> {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const belastung = await db.belastung.findFirst({ where: { id: belastungId, immobilieId } })
|
|
if (!belastung) throw new ZugriffVerweigert('Diese Belastung gibt es nicht.')
|
|
|
|
await db.belastung.delete({ where: { id: belastungId } })
|
|
await protokollieren({
|
|
aktion: 'GELOESCHT',
|
|
entitaet: 'Belastung',
|
|
entitaetId: belastungId,
|
|
bezeichnung: `${belastung.art} ${belastung.laufendeNr ?? ''}`.trim(),
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}`)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Wert-Historie
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Der Vermoegensposten zur Immobilie. Er wird bei Bedarf angelegt, damit
|
|
* die Wert-Historie und die Vermoegensuebersicht dieselbe Quelle benutzen.
|
|
*/
|
|
async function vermoegenspostenZurImmobilie(betreuteId: string, immobilieId: string, bezeichnung: string) {
|
|
const vorhanden = await db.vermoegensposten.findFirst({
|
|
where: { betreuteId, immobilieId, art: 'IMMOBILIE', ...nichtGeloescht },
|
|
})
|
|
if (vorhanden) return vorhanden
|
|
|
|
return db.vermoegensposten.create({
|
|
data: { betreuteId, immobilieId, art: 'IMMOBILIE', bezeichnung },
|
|
})
|
|
}
|
|
|
|
export async function wertstandErfassen(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
const immobilie = await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const gelesen = WertstandSchema.safeParse(formularLesen(formular))
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const posten = await vermoegenspostenZurImmobilie(betreuteId, immobilieId, immobilie.bezeichnung)
|
|
|
|
const vorhanden = await db.wertstand.findUnique({
|
|
where: {
|
|
vermoegenspostenId_stichtag: {
|
|
vermoegenspostenId: posten.id,
|
|
stichtag: gelesen.data.stichtag,
|
|
},
|
|
},
|
|
})
|
|
if (vorhanden) {
|
|
return {
|
|
fehler:
|
|
'Zu diesem Stichtag steht bereits ein Wert in der Historie. Wähle einen ' +
|
|
'anderen Stichtag — bestehende Werte werden nicht überschrieben, damit ' +
|
|
'jede Vermögensübersicht reproduzierbar bleibt.',
|
|
felder: { stichtag: 'Stichtag bereits belegt.' },
|
|
}
|
|
}
|
|
|
|
const wertstand = await db.wertstand.create({
|
|
data: {
|
|
vermoegenspostenId: posten.id,
|
|
stichtag: gelesen.data.stichtag,
|
|
wertCent: gelesen.data.wert,
|
|
wertquelle: gelesen.data.wertquelle,
|
|
bemerkung: gelesen.data.bemerkung,
|
|
},
|
|
})
|
|
|
|
await protokollieren({
|
|
aktion: 'ERSTELLT',
|
|
entitaet: 'Wertstand',
|
|
entitaetId: wertstand.id,
|
|
bezeichnung: `${immobilie.bezeichnung} — Wert zum Stichtag (${gelesen.data.wertquelle})`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}`)
|
|
return { erfolg: 'Wert in die Historie aufgenommen.' }
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Leerstands-Checkliste
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Uebernimmt die Pflichten-Checkliste. Abgeglichen wird ueber den Titel:
|
|
* Ein Punkt, den es schon gibt, wird nicht noch einmal angelegt.
|
|
*/
|
|
async function checklisteAnlegen(
|
|
betreuteId: string,
|
|
immobilieId: string,
|
|
leerstandSeit: Date | null,
|
|
): Promise<number> {
|
|
const punkte = leerstandCheckliste(leerstandSeit, heuteAlsTag())
|
|
const vorhandene = await db.immobilienCheckpunkt.findMany({
|
|
where: { immobilieId },
|
|
select: { titel: true },
|
|
})
|
|
const bekannt = new Set(vorhandene.map((p) => p.titel))
|
|
|
|
const neue = punkte.filter((p) => !bekannt.has(p.titel))
|
|
if (neue.length === 0) return 0
|
|
|
|
await db.immobilienCheckpunkt.createMany({
|
|
data: neue.map((punkt) => ({
|
|
immobilieId,
|
|
titel: punkt.titel,
|
|
beschreibung: punkt.beschreibung,
|
|
faelligAm: punkt.faelligAm,
|
|
wiederholungRrule: punkt.wiederholungRrule ?? null,
|
|
reihenfolge: punkt.reihenfolge,
|
|
})),
|
|
})
|
|
|
|
return neue.length
|
|
}
|
|
|
|
export async function checklisteUebernehmen(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
const immobilie = await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const anzahl = await checklisteAnlegen(betreuteId, immobilieId, immobilie.leerstandSeit)
|
|
|
|
await protokollieren({
|
|
aktion: 'ERSTELLT',
|
|
entitaet: 'ImmobilienCheckpunkt',
|
|
entitaetId: immobilieId,
|
|
bezeichnung: `Leerstands-Checkliste übernommen (${anzahl} Punkte) — ${immobilie.bezeichnung}`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/checkliste`)
|
|
return {
|
|
erfolg:
|
|
anzahl === 0
|
|
? 'Alle Punkte der Checkliste stehen bereits in der Liste.'
|
|
: `${anzahl} Punkte übernommen.`,
|
|
}
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
export async function checkpunktSpeichern(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
const checkpunktId = String(formular.get('checkpunktId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const gelesen = CheckpunktSchema.safeParse(formularLesen(formular))
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const daten = {
|
|
titel: gelesen.data.titel,
|
|
beschreibung: gelesen.data.beschreibung,
|
|
faelligAm: gelesen.data.faelligAm,
|
|
wiederholungRrule: gelesen.data.wiederholungRrule,
|
|
}
|
|
|
|
if (checkpunktId) {
|
|
const vorhanden = await db.immobilienCheckpunkt.findFirst({
|
|
where: { id: checkpunktId, immobilieId },
|
|
})
|
|
if (!vorhanden) return { fehler: 'Diesen Punkt gibt es nicht.' }
|
|
await db.immobilienCheckpunkt.update({ where: { id: checkpunktId }, data: daten })
|
|
} else {
|
|
await db.immobilienCheckpunkt.create({ data: { immobilieId, ...daten } })
|
|
}
|
|
|
|
await protokollieren({
|
|
aktion: checkpunktId ? 'GEAENDERT' : 'ERSTELLT',
|
|
entitaet: 'ImmobilienCheckpunkt',
|
|
entitaetId: checkpunktId || null,
|
|
bezeichnung: daten.titel,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/checkliste`)
|
|
return { erfolg: 'Punkt gespeichert.' }
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hakt einen Punkt ab. Bei einem wiederkehrenden Punkt wird er nicht
|
|
* geschlossen, sondern auf den naechsten Termin gesetzt - ein
|
|
* Kontrollgang ist nie "erledigt", er ist nur bis naechste Woche erledigt.
|
|
*/
|
|
export async function checkpunktErledigen(
|
|
betreuteId: string,
|
|
immobilieId: string,
|
|
checkpunktId: string,
|
|
erledigt: boolean,
|
|
): Promise<void> {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const punkt = await db.immobilienCheckpunkt.findFirst({
|
|
where: { id: checkpunktId, immobilieId },
|
|
})
|
|
if (!punkt) throw new ZugriffVerweigert('Diesen Punkt gibt es nicht.')
|
|
|
|
const heute = heuteAlsTag()
|
|
|
|
if (erledigt && punkt.wiederholungRrule) {
|
|
const naechste = naechsteFaelligkeit(punkt.wiederholungRrule, heute)
|
|
await db.immobilienCheckpunkt.update({
|
|
where: { id: checkpunktId },
|
|
data: { erledigt: false, erledigtAm: heute, faelligAm: naechste ?? punkt.faelligAm },
|
|
})
|
|
} else {
|
|
await db.immobilienCheckpunkt.update({
|
|
where: { id: checkpunktId },
|
|
data: { erledigt, erledigtAm: erledigt ? heute : null },
|
|
})
|
|
}
|
|
|
|
await protokollieren({
|
|
aktion: 'GEAENDERT',
|
|
entitaet: 'ImmobilienCheckpunkt',
|
|
entitaetId: checkpunktId,
|
|
bezeichnung: punkt.titel,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
aenderung: { erledigt: { alt: punkt.erledigt, neu: erledigt } },
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/checkliste`)
|
|
revalidatePath(`/immobilie/${immobilieId}`)
|
|
}
|
|
|
|
export async function checkpunktLoeschen(
|
|
betreuteId: string,
|
|
immobilieId: string,
|
|
checkpunktId: string,
|
|
): Promise<void> {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const punkt = await db.immobilienCheckpunkt.findFirst({
|
|
where: { id: checkpunktId, immobilieId },
|
|
})
|
|
if (!punkt) throw new ZugriffVerweigert('Diesen Punkt gibt es nicht.')
|
|
|
|
await db.immobilienCheckpunkt.delete({ where: { id: checkpunktId } })
|
|
await protokollieren({
|
|
aktion: 'GELOESCHT',
|
|
entitaet: 'ImmobilienCheckpunkt',
|
|
entitaetId: checkpunktId,
|
|
bezeichnung: punkt.titel,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/checkliste`)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Inventar
|
|
// -----------------------------------------------------------------------------
|
|
|
|
export async function inventarSpeichern(
|
|
_vorher: Formularzustand,
|
|
formular: FormData,
|
|
): Promise<Formularzustand> {
|
|
const betreuteId = String(formular.get('betreuteId') ?? '')
|
|
const immobilieId = String(formular.get('immobilieId') ?? '')
|
|
const postenId = String(formular.get('postenId') ?? '')
|
|
|
|
try {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const gelesen = InventarSchema.safeParse(formularLesen(formular))
|
|
if (!gelesen.success) return zodFehler(gelesen.error)
|
|
|
|
const notiz = inventarNotizSchreiben({
|
|
raum: gelesen.data.raum,
|
|
verbleib: gelesen.data.verbleib,
|
|
bemerkung: gelesen.data.bemerkung,
|
|
})
|
|
|
|
const daten = {
|
|
bezeichnung: gelesen.data.bezeichnung,
|
|
art: 'WERTGEGENSTAND' as const,
|
|
immobilieId,
|
|
...sealen('Vermoegensposten', { notizen: notiz }),
|
|
}
|
|
|
|
let zielId = postenId
|
|
|
|
if (postenId) {
|
|
const vorhanden = await db.vermoegensposten.findFirst({
|
|
where: { id: postenId, betreuteId, immobilieId, ...nichtGeloescht },
|
|
})
|
|
if (!vorhanden) return { fehler: 'Diesen Gegenstand gibt es nicht.' }
|
|
await db.vermoegensposten.update({ where: { id: postenId }, data: daten })
|
|
} else {
|
|
const neu = await db.vermoegensposten.create({ data: { betreuteId, ...daten } })
|
|
zielId = neu.id
|
|
}
|
|
|
|
// Geschaetzter Wert als Wertstand zum heutigen Tag - Historie, kein
|
|
// Ueberschreiben. Wird am selben Tag zweimal gespeichert, gewinnt der
|
|
// letzte Eintrag desselben Tages.
|
|
if (gelesen.data.geschaetzterWert !== null) {
|
|
const stichtag = tagesdatum(heuteAlsTag())
|
|
await db.wertstand.upsert({
|
|
where: { vermoegenspostenId_stichtag: { vermoegenspostenId: zielId, stichtag } },
|
|
create: {
|
|
vermoegenspostenId: zielId,
|
|
stichtag,
|
|
wertCent: gelesen.data.geschaetzterWert,
|
|
wertquelle: INVENTAR_WERTQUELLE,
|
|
},
|
|
update: { wertCent: gelesen.data.geschaetzterWert, wertquelle: INVENTAR_WERTQUELLE },
|
|
})
|
|
}
|
|
|
|
await protokollieren({
|
|
aktion: postenId ? 'GEAENDERT' : 'ERSTELLT',
|
|
entitaet: 'Vermoegensposten',
|
|
entitaetId: zielId,
|
|
bezeichnung: `Inventar: ${gelesen.data.bezeichnung}`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/inventar`)
|
|
return { erfolg: 'Gegenstand gespeichert.' }
|
|
} catch (fehler) {
|
|
return alsFehler(fehler)
|
|
}
|
|
}
|
|
|
|
export async function inventarLoeschen(
|
|
betreuteId: string,
|
|
immobilieId: string,
|
|
postenId: string,
|
|
): Promise<void> {
|
|
const { benutzer } = await zugangOderFehler(betreuteId)
|
|
await immobilieLaden(betreuteId, immobilieId)
|
|
|
|
const posten = await db.vermoegensposten.findFirst({
|
|
where: { id: postenId, betreuteId, immobilieId, ...nichtGeloescht },
|
|
})
|
|
if (!posten) throw new ZugriffVerweigert('Diesen Gegenstand gibt es nicht.')
|
|
|
|
await db.vermoegensposten.update({
|
|
where: { id: postenId },
|
|
data: { geloeschtAm: new Date() },
|
|
})
|
|
|
|
await protokollieren({
|
|
aktion: 'GELOESCHT',
|
|
entitaet: 'Vermoegensposten',
|
|
entitaetId: postenId,
|
|
bezeichnung: `Inventar: ${posten.bezeichnung}`,
|
|
betreuteId,
|
|
userId: benutzer.id,
|
|
userEmail: benutzer.email,
|
|
userName: benutzer.name,
|
|
})
|
|
|
|
revalidatePath(`/immobilie/${immobilieId}/inventar`)
|
|
}
|
|
|
|
/** Liest den Inventarposten fuer das Bearbeitungsformular auf. */
|
|
export async function inventarLesen(betreuteId: string, postenId: string) {
|
|
await erfordereZugang(betreuteId, MODUL, 'lesen')
|
|
const posten = await db.vermoegensposten.findFirst({
|
|
where: { id: postenId, betreuteId, ...nichtGeloescht },
|
|
})
|
|
if (!posten) return null
|
|
const offen = oeffnen('Vermoegensposten', posten)
|
|
return { bezeichnung: posten.bezeichnung, ...inventarNotizLesen(offen.notizen) }
|
|
}
|