Ansprechpartner am Vorgang hinterlegen
Ein namentlich adressiertes Schreiben landet bei der Person, die den Vorgang kennt, statt in der Poststelle. Am Vorgang laesst sich jetzt ein Kontakt aus dem Verzeichnis als Ansprechpartner benennen; die Sachstandsanfrage redet ihn an, und sein Name steht ueber der Behoerde. Die Sachstandsanfrage konnte das schon - sie bekam den Namen nur nie uebergeben. entwurf.ts lud einen der verknuepften Kontakte und verwarf ihn wieder. Wer angeredet wird, ist jetzt eine Entscheidung und kein Zufall. Aufbau: - Migration: ansprechpartnerId am Vorgang (SetNull - ein Vorgang darf nicht daran haengen, dass es den Kontakt noch gibt) und anrede am Kontakt. - Die Anrede als reine Funktion in kontakte/anrede.ts. Geraten wird nie: ohne hinterlegte Anrede wird mit vollem Namen angeredet. Ein leerer Name mit gesetzter Anrede faellt auf die allgemeine Anrede zurueck - "Sehr geehrte Frau ," darf nie herausgehen. - Der Wechsel des Ansprechpartners steht mit NAMEN im Protokoll. Zwei cuids nebeneinander waeren protokolliert, aber nicht nachvollziehbar, und nachvollziehbar war die Anforderung. - Der Ansprechpartner wird gegen die Akte geprueft, bevor er gespeichert wird - sein Name steht anschliessend im Briefkopf. Nebenbei behoben: Der Schreiben-Editor verwarf die empfaengerAnschrift aus dem Entwurf, sein State startete immer leer. Jetzt kommt die Anschrift des Ansprechpartners im Formular an. Geprueft: neun Einheitstests fuer die Anrede, fuenf End-to-End-Tests von der Auswahl bis zum Protokolleintrag. Refs #9
This commit is contained in:
@@ -47,6 +47,7 @@ export default async function KontaktBearbeitenSeite({
|
||||
name: kontakt.name,
|
||||
organisation: kontakt.organisation,
|
||||
rolle: kontakt.rolle,
|
||||
anrede: kontakt.anrede,
|
||||
telefon: kontakt.telefon,
|
||||
mobil: kontakt.mobil,
|
||||
fax: kontakt.fax,
|
||||
|
||||
@@ -126,6 +126,7 @@ export default async function SchreibenSeite({
|
||||
betreff: entwurf.betreff,
|
||||
text: entwurf.text,
|
||||
empfaengerName: entwurf.empfaengerName,
|
||||
empfaengerAnschrift: entwurf.empfaengerAnschrift,
|
||||
wiedervorlageAm: entwurf.wiedervorlageAm,
|
||||
}}
|
||||
kontakte={kontakte}
|
||||
|
||||
@@ -56,6 +56,7 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
buchungen: { where: nichtGeloescht, orderBy: { datum: 'desc' } },
|
||||
kontaktprotokolle: { where: nichtGeloescht, orderBy: { datum: 'desc' } },
|
||||
verknuepfungen: { include: { dokument: true } },
|
||||
ansprechpartner: true,
|
||||
},
|
||||
})
|
||||
if (!vorgang) notFound()
|
||||
@@ -65,6 +66,14 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
|
||||
const offen = oeffnen('Vorgang', vorgang)
|
||||
|
||||
// Kontakte dieser Akte fuer die Wahl des Ansprechpartners.
|
||||
const kontakte = await db.kontakt.findMany({
|
||||
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
||||
select: { id: true, name: true, organisation: true },
|
||||
orderBy: [{ organisation: 'asc' }, { name: 'asc' }],
|
||||
take: 300,
|
||||
})
|
||||
|
||||
// --- Statuswechsel aus dem Audit-Log --------------------------------------
|
||||
// Das Log ist ohnehin der Nachweis gegenueber dem Gericht; daraus die
|
||||
// Statuschronik zu lesen, spart eine zusaetzliche Tabelle.
|
||||
@@ -182,6 +191,12 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
aktenzeichenGericht: aktive.aktenzeichenGericht,
|
||||
betreuungsgericht: aktive.betreuungsgericht,
|
||||
},
|
||||
ansprechpartner: vorgang.ansprechpartner
|
||||
? {
|
||||
name: vorgang.ansprechpartner.name,
|
||||
anrede: vorgang.ansprechpartner.anrede,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
: null
|
||||
|
||||
@@ -285,6 +300,40 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
<p className="font-medium">{vorgang.aktenzeichen ?? '–'}</p>
|
||||
</div>
|
||||
|
||||
{/* Wen frage ich, bevor ich anrufe? Durchwahl und Adresse
|
||||
stehen dabei - sonst schlaegt man sie doch wieder nach. */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Ansprechpartner</p>
|
||||
{vorgang.ansprechpartner ? (
|
||||
<>
|
||||
<p className="font-medium">
|
||||
<Link
|
||||
href={`/kontakte/${vorgang.ansprechpartner.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{vorgang.ansprechpartner.name}
|
||||
</Link>
|
||||
</p>
|
||||
{vorgang.ansprechpartner.organisation && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{vorgang.ansprechpartner.organisation}
|
||||
</p>
|
||||
)}
|
||||
{(vorgang.ansprechpartner.telefon || vorgang.ansprechpartner.email) && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{[vorgang.ansprechpartner.telefon, vorgang.ansprechpartner.email]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Keiner benannt — Schreiben gehen an „Sehr geehrte Damen und Herren“.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 sm:col-span-2">
|
||||
<p className="text-sm text-muted-foreground">Nächster Schritt</p>
|
||||
<p className="font-medium">
|
||||
@@ -363,6 +412,7 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
betreuteId={aktive.id}
|
||||
vorgangId={vorgang.id}
|
||||
abbrechenHref={`/vorgaenge/${vorgang.id}`}
|
||||
kontakte={kontakte}
|
||||
werte={{
|
||||
titel: vorgang.titel,
|
||||
kategorie: vorgang.kategorie,
|
||||
@@ -371,6 +421,7 @@ export default async function VorgangSeite({ params }: { params: Promise<{ id: s
|
||||
beschreibung: offen.beschreibung ?? null,
|
||||
gegenstelle: vorgang.gegenstelle,
|
||||
aktenzeichen: vorgang.aktenzeichen,
|
||||
ansprechpartnerId: vorgang.ansprechpartnerId,
|
||||
verantwortlich: vorgang.verantwortlich,
|
||||
naechsterSchritt: vorgang.naechsterSchritt,
|
||||
faelligAm: vorgang.faelligAm,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { VorlagenKacheln } from '@/components/vorgaenge/vorlagen-kacheln'
|
||||
import { aktiveBetreute, erfordereBenutzer, erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { GENEHMIGUNG_WARNUNG } from '@/lib/vorgaenge/genehmigungskette'
|
||||
import { vorlageFinden } from '@/lib/vorgaenge/vorlagen'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
import { addDays } from 'date-fns'
|
||||
|
||||
@@ -31,6 +32,14 @@ export default async function NeuerVorgangSeite({
|
||||
|
||||
await erfordereZugang(aktive.id, 'VORGAENGE', 'schreiben')
|
||||
|
||||
// Kontakte dieser Akte fuer die Wahl des Ansprechpartners.
|
||||
const kontakte = await db.kontakt.findMany({
|
||||
where: { betreuteId: aktive.id, ...nichtGeloescht },
|
||||
select: { id: true, name: true, organisation: true },
|
||||
orderBy: [{ organisation: 'asc' }, { name: 'asc' }],
|
||||
take: 300,
|
||||
})
|
||||
|
||||
const auswahl = await searchParams
|
||||
const vorlage = auswahl.vorlage ? vorlageFinden(auswahl.vorlage) : undefined
|
||||
const leer = auswahl.leer === '1'
|
||||
@@ -155,6 +164,7 @@ export default async function NeuerVorgangSeite({
|
||||
betreuteId={aktive.id}
|
||||
vorlageSchluessel={vorlage?.schluessel ?? null}
|
||||
abbrechenHref="/vorgaenge"
|
||||
kontakte={kontakte}
|
||||
werte={{
|
||||
titel: vorlage?.titel ?? '',
|
||||
kategorie: vorlage?.kategorie ?? 'SONSTIGES',
|
||||
@@ -163,6 +173,7 @@ export default async function NeuerVorgangSeite({
|
||||
beschreibung: vorlage?.beschreibung ?? null,
|
||||
gegenstelle: null,
|
||||
aktenzeichen: null,
|
||||
ansprechpartnerId: null,
|
||||
verantwortlich: benutzer.name,
|
||||
naechsterSchritt: vorlage?.aufgaben[0]?.titel ?? null,
|
||||
faelligAm: null,
|
||||
|
||||
@@ -9,7 +9,12 @@ import { Input } from '@/components/ui/input'
|
||||
import { Select } from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Feld, Feldgruppe } from '@/components/feld'
|
||||
import { KONTAKT_ROLLEN, KONTAKT_ROLLE_BESCHRIFTUNG } from '@/lib/validierung/kontakte'
|
||||
import {
|
||||
KONTAKT_ANREDEN,
|
||||
KONTAKT_ROLLEN,
|
||||
KONTAKT_ROLLE_BESCHRIFTUNG,
|
||||
} from '@/lib/validierung/kontakte'
|
||||
import { ANREDE_BESCHRIFTUNG } from '@/lib/kontakte/anrede'
|
||||
import { kontaktSpeichern, type KontaktZustand } from '@/server/actions/kontakte'
|
||||
|
||||
/**
|
||||
@@ -24,6 +29,7 @@ export type KontaktWerte = {
|
||||
name: string
|
||||
organisation: string | null
|
||||
rolle: string
|
||||
anrede: string
|
||||
telefon: string | null
|
||||
mobil: string | null
|
||||
fax: string | null
|
||||
@@ -91,6 +97,21 @@ export function KontaktFormular({
|
||||
</Select>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="anrede"
|
||||
beschriftung="Anrede"
|
||||
erklaerung="Für die Briefanrede. Ohne Angabe wird mit vollem Namen angeredet — geraten wird nie."
|
||||
fehler={zustand.felder?.anrede}
|
||||
>
|
||||
<Select name="anrede" defaultValue={wert('anrede') || 'OHNE'}>
|
||||
{KONTAKT_ANREDEN.map((anrede) => (
|
||||
<option key={anrede} value={anrede}>
|
||||
{ANREDE_BESCHRIFTUNG[anrede]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="aktenzeichen"
|
||||
beschriftung="Aktenzeichen dort"
|
||||
|
||||
@@ -59,7 +59,13 @@ export function SchreibenEditor({
|
||||
}: {
|
||||
betreuteId: string
|
||||
art: SchreibenArt
|
||||
entwurf: { betreff: string; text: string; empfaengerName: string; wiedervorlageAm: string }
|
||||
entwurf: {
|
||||
betreff: string
|
||||
text: string
|
||||
empfaengerName: string
|
||||
empfaengerAnschrift: string
|
||||
wiedervorlageAm: string
|
||||
}
|
||||
kontakte: KontaktAuswahl[]
|
||||
vertragId: string | null
|
||||
vorgangId: string | null
|
||||
@@ -69,7 +75,9 @@ export function SchreibenEditor({
|
||||
const [text, setText] = React.useState(entwurf.text)
|
||||
const [betreff, setBetreff] = React.useState(entwurf.betreff)
|
||||
const [empfaengerName, setEmpfaengerName] = React.useState(entwurf.empfaengerName)
|
||||
const [empfaengerAnschrift, setEmpfaengerAnschrift] = React.useState('')
|
||||
const [empfaengerAnschrift, setEmpfaengerAnschrift] = React.useState(
|
||||
entwurf.empfaengerAnschrift,
|
||||
)
|
||||
const [empfaengerEmail, setEmpfaengerEmail] = React.useState('')
|
||||
const [versandweg, setVersandweg] = React.useState<'BRIEF' | 'EMAIL'>('BRIEF')
|
||||
const [betreffKopiert, setBetreffKopiert] = React.useState(false)
|
||||
|
||||
@@ -36,6 +36,7 @@ export type VorgangsFormularWerte = {
|
||||
beschreibung: string | null
|
||||
gegenstelle: string | null
|
||||
aktenzeichen: string | null
|
||||
ansprechpartnerId: string | null
|
||||
verantwortlich: string | null
|
||||
naechsterSchritt: string | null
|
||||
faelligAm: Date | null
|
||||
@@ -50,6 +51,7 @@ export function VorgangsFormular({
|
||||
vorlageSchluessel,
|
||||
werte,
|
||||
abbrechenHref,
|
||||
kontakte,
|
||||
}: {
|
||||
modus: 'anlegen' | 'aendern'
|
||||
betreuteId: string
|
||||
@@ -57,6 +59,8 @@ export function VorgangsFormular({
|
||||
vorlageSchluessel?: string | null
|
||||
werte: VorgangsFormularWerte
|
||||
abbrechenHref: string
|
||||
/** Kontakte dieser Akte, zur Wahl des Ansprechpartners. */
|
||||
kontakte: { id: string; name: string; organisation: string | null }[]
|
||||
}) {
|
||||
const [zustand, aktion, laeuft] = useActionState<Formularzustand, FormData>(
|
||||
modus === 'anlegen' ? vorgangAnlegen : vorgangAendern,
|
||||
@@ -146,6 +150,22 @@ export function VorgangsFormular({
|
||||
/>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="ansprechpartnerId"
|
||||
beschriftung="Ansprechpartner"
|
||||
erklaerung="Wer dort die Sache bearbeitet. Schreiben zu diesem Vorgang reden ihn namentlich an — das bringt sie an der Poststelle vorbei."
|
||||
fehler={zustand.felder?.ansprechpartnerId}
|
||||
>
|
||||
<Select name="ansprechpartnerId" defaultValue={werte.ansprechpartnerId ?? ''}>
|
||||
<option value="">– keiner benannt –</option>
|
||||
{kontakte.map((kontakt) => (
|
||||
<option key={kontakt.id} value={kontakt.id}>
|
||||
{[kontakt.name, kontakt.organisation].filter(Boolean).join(' · ')}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Feld>
|
||||
|
||||
<Feld
|
||||
id="aktenzeichen"
|
||||
beschriftung="Aktenzeichen"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { KontaktAnrede } from '@prisma/client'
|
||||
|
||||
/**
|
||||
* Die Briefanrede eines Ansprechpartners.
|
||||
*
|
||||
* Ein namentlich adressiertes Schreiben landet bei der Person, die den
|
||||
* Vorgang kennt, statt in der Poststelle. Beim Bezirk Oberfranken ist das
|
||||
* der Unterschied zwischen zwei Wochen und zwei Monaten.
|
||||
*
|
||||
* Zwei Regeln, die hier nicht verhandelbar sind:
|
||||
*
|
||||
* 1. Es wird nie geraten. Ohne hinterlegte Anrede wird mit dem vollen
|
||||
* Namen angeredet ("Sehr geehrte(r) Anna Schmitt,"). Eine falsch
|
||||
* geratene Anrede faellt dem Empfaenger sofort auf und kostet genau
|
||||
* die Aufmerksamkeit, die man gewinnen wollte.
|
||||
* 2. Ohne Ansprechpartner bleibt es bei "Sehr geehrte Damen und Herren,".
|
||||
* Ein halb gefuellter Name darf nie zu "Sehr geehrte Frau ," werden -
|
||||
* lieber unpersoenlich als offensichtlich automatisch.
|
||||
*/
|
||||
|
||||
export const ANREDE_BESCHRIFTUNG: Record<KontaktAnrede, string> = {
|
||||
OHNE: 'keine Angabe',
|
||||
FRAU: 'Frau',
|
||||
HERR: 'Herr',
|
||||
}
|
||||
|
||||
export const ALLGEMEINE_ANREDE = 'Sehr geehrte Damen und Herren,'
|
||||
|
||||
/**
|
||||
* Der Nachname aus einem vollen Namen.
|
||||
*
|
||||
* Titel und Vornamen fallen weg: "Dr. Anna Schmitt" wird zu "Schmitt".
|
||||
* Doppelnamen mit Bindestrich bleiben ganz, weil sie ein Wort sind.
|
||||
*/
|
||||
export function nachnameAus(name: string): string {
|
||||
const teile = name.trim().split(/\s+/).filter(Boolean)
|
||||
return teile.length > 0 ? teile[teile.length - 1]! : ''
|
||||
}
|
||||
|
||||
export type Ansprechpartner = {
|
||||
name: string
|
||||
anrede: KontaktAnrede
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Anredezeile fuer ein Schreiben, einschliesslich Komma.
|
||||
*
|
||||
* Ohne Ansprechpartner - oder mit einem, dessen Name leer ist - die
|
||||
* allgemeine Anrede.
|
||||
*/
|
||||
export function anredeZeile(ansprechpartner: Ansprechpartner | null): string {
|
||||
if (!ansprechpartner) return ALLGEMEINE_ANREDE
|
||||
|
||||
const name = ansprechpartner.name.trim()
|
||||
if (!name) return ALLGEMEINE_ANREDE
|
||||
|
||||
if (ansprechpartner.anrede === 'FRAU') return `Sehr geehrte Frau ${nachnameAus(name)},`
|
||||
if (ansprechpartner.anrede === 'HERR') return `Sehr geehrter Herr ${nachnameAus(name)},`
|
||||
|
||||
return `Sehr geehrte(r) ${name},`
|
||||
}
|
||||
@@ -201,8 +201,19 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
||||
|
||||
const vorgang = await db.vorgang.findFirst({
|
||||
where: { id: eingabe.vorgangId, betreuteId: eingabe.betreuteId, ...nichtGeloescht },
|
||||
// Der benannte Ansprechpartner, nicht irgendeiner der verknuepften
|
||||
// Kontakte: Wer angeredet wird, ist eine Entscheidung und kein Zufall.
|
||||
include: {
|
||||
kontakte: { select: { name: true, organisation: true }, take: 1 },
|
||||
ansprechpartner: {
|
||||
select: {
|
||||
name: true,
|
||||
anrede: true,
|
||||
organisation: true,
|
||||
strasse: true,
|
||||
plz: true,
|
||||
ort: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!vorgang) throw new Error('Diesen Vorgang gibt es nicht (mehr).')
|
||||
@@ -222,15 +233,31 @@ export async function entwurfBauen(eingabe: EntwurfEingabe): Promise<SchreibenEn
|
||||
aktenzeichenGericht: betreute.aktenzeichenGericht,
|
||||
betreuungsgericht: betreute.betreuungsgericht,
|
||||
},
|
||||
ansprechpartner: vorgang.ansprechpartner,
|
||||
heute,
|
||||
})
|
||||
|
||||
// Ist ein Ansprechpartner benannt, kommt seine Anschrift gleich mit -
|
||||
// sie steht im Kontaktverzeichnis und muesste sonst abgetippt werden.
|
||||
const anschrift = vorgang.ansprechpartner
|
||||
? [
|
||||
vorgang.ansprechpartner.strasse,
|
||||
[vorgang.ansprechpartner.plz, vorgang.ansprechpartner.ort]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.trim() || null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
: ''
|
||||
|
||||
return {
|
||||
...gemeinsam,
|
||||
text: anfrage.text,
|
||||
betreff: anfrage.betreff,
|
||||
empfaengerName: vorgang.gegenstelle ?? '',
|
||||
empfaengerAnschrift: '',
|
||||
empfaengerName:
|
||||
vorgang.ansprechpartner?.name ?? vorgang.gegenstelle ?? '',
|
||||
empfaengerAnschrift: anschrift,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import type { KontaktArt, KontaktRolle } from '@prisma/client'
|
||||
import type { KontaktAnrede, KontaktArt, KontaktRolle } from '@prisma/client'
|
||||
|
||||
/**
|
||||
* Eingabepruefung fuer Kontakte und Kontaktprotokolle.
|
||||
@@ -101,6 +101,13 @@ export const KONTAKT_ROLLEN = [
|
||||
'SONSTIGES',
|
||||
] as const satisfies readonly KontaktRolle[]
|
||||
|
||||
/** Anreden fuer die Briefanrede - Reihenfolge der Auswahlliste. */
|
||||
export const KONTAKT_ANREDEN = [
|
||||
'OHNE',
|
||||
'FRAU',
|
||||
'HERR',
|
||||
] as const satisfies readonly KontaktAnrede[]
|
||||
|
||||
export const KONTAKT_ROLLE_BESCHRIFTUNG: Record<KontaktRolle, string> = {
|
||||
RECHTSPFLEGER: 'Rechtspfleger',
|
||||
RICHTER: 'Richter',
|
||||
@@ -177,6 +184,7 @@ export const KontaktSchema = z.object({
|
||||
rolle: z.enum(KONTAKT_ROLLEN, {
|
||||
errorMap: () => ({ message: 'Bitte eine Rolle aus der Liste wählen.' }),
|
||||
}),
|
||||
anrede: z.enum(KONTAKT_ANREDEN).default('OHNE'),
|
||||
telefon: optionalerText(50),
|
||||
mobil: optionalerText(50),
|
||||
fax: optionalerText(50),
|
||||
@@ -198,6 +206,7 @@ export function kontaktFormularLesen(formular: FormData): Record<string, unknown
|
||||
for (const feld of [
|
||||
'name',
|
||||
'organisation',
|
||||
'anrede',
|
||||
'rolle',
|
||||
'telefon',
|
||||
'mobil',
|
||||
|
||||
@@ -19,6 +19,14 @@ const optionalerText = (max = 200) =>
|
||||
.transform((wert) => (wert === '' ? null : wert))
|
||||
.nullable()
|
||||
|
||||
/** Ein Verweis auf einen anderen Datensatz - leer heisst "keiner". */
|
||||
const optionaleId = z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((wert) => (wert === '' ? null : wert))
|
||||
.nullable()
|
||||
.default(null)
|
||||
|
||||
const optionalesDatum = z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -90,6 +98,7 @@ export const VorgangSchema = z.object({
|
||||
beschreibung: optionalerText(4000),
|
||||
gegenstelle: optionalerText(150),
|
||||
aktenzeichen: optionalerText(80),
|
||||
ansprechpartnerId: optionaleId,
|
||||
verantwortlich: optionalerText(120),
|
||||
naechsterSchritt: optionalerText(300),
|
||||
faelligAm: optionalesDatum,
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
/** Muss zur "version" in der package.json passen. */
|
||||
export const VERSION = '1.8.0'
|
||||
export const VERSION = '1.9.0'
|
||||
|
||||
/**
|
||||
* Kurzer Git-Stand, beim Bauen hineingereicht:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { differenceInCalendarDays } from 'date-fns'
|
||||
import { anredeZeile, type Ansprechpartner } from '@/lib/kontakte/anrede'
|
||||
import { formatDatum } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
@@ -36,8 +37,12 @@ export type SachstandsanfrageEingabe = {
|
||||
aktenzeichenGericht?: string | null
|
||||
betreuungsgericht?: string | null
|
||||
}
|
||||
/** Ansprechpartner bei der Gegenstelle, wenn bekannt. */
|
||||
ansprechpartner?: string | null
|
||||
/**
|
||||
* Ansprechpartner bei der Gegenstelle, wenn am Vorgang benannt.
|
||||
* Bestimmt die Anrede - ohne ihn bleibt es bei "Sehr geehrte Damen
|
||||
* und Herren".
|
||||
*/
|
||||
ansprechpartner?: Ansprechpartner | null
|
||||
/** Für den Test überschreibbar. */
|
||||
heute?: Date
|
||||
}
|
||||
@@ -69,13 +74,15 @@ export function sachstandsanfrageErzeugen(
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
|
||||
const anrede = eingabe.ansprechpartner
|
||||
? `Sehr geehrte(r) ${eingabe.ansprechpartner},`
|
||||
: 'Sehr geehrte Damen und Herren,'
|
||||
const anrede = anredeZeile(eingabe.ansprechpartner ?? null)
|
||||
|
||||
const zeilen: string[] = []
|
||||
|
||||
if (eingabe.gegenstelle) zeilen.push(eingabe.gegenstelle, '')
|
||||
// Der Name steht ueber der Stelle, nicht darunter: So sortiert die
|
||||
// Poststelle den Brief zur Person und nicht ins allgemeine Fach.
|
||||
if (eingabe.ansprechpartner?.name.trim()) zeilen.push(eingabe.ansprechpartner.name.trim())
|
||||
if (eingabe.gegenstelle) zeilen.push(eingabe.gegenstelle)
|
||||
if (eingabe.ansprechpartner?.name.trim() || eingabe.gegenstelle) zeilen.push('')
|
||||
zeilen.push(formatDatum(heute), '')
|
||||
zeilen.push(`Betreff: ${betreff}`, '')
|
||||
zeilen.push(anrede, '')
|
||||
|
||||
@@ -5,7 +5,7 @@ import { redirect } from 'next/navigation'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import { db, nichtGeloescht } from '@/lib/db'
|
||||
import { erfordereZugang } from '@/lib/auth/sitzung'
|
||||
import { aenderungErmitteln, protokollieren } from '@/lib/audit'
|
||||
import { aenderungErmitteln, protokollieren, type Aenderung } from '@/lib/audit'
|
||||
import { oeffnen, sealen } from '@/lib/crypto/feld'
|
||||
import {
|
||||
AufgabeAbhakenSchema,
|
||||
@@ -58,6 +58,45 @@ function pfadeAuffrischen(vorgangId?: string): void {
|
||||
// Anlegen
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/** Der Name eines Kontakts fuers Protokoll - null bleibt null. */
|
||||
async function kontaktnameZu(kontaktId: string | null): Promise<string | null> {
|
||||
if (!kontaktId) return null
|
||||
const kontakt = await db.kontakt.findUnique({
|
||||
where: { id: kontaktId },
|
||||
select: { name: true, organisation: true },
|
||||
})
|
||||
if (!kontakt) return null
|
||||
return [kontakt.name, kontakt.organisation].filter(Boolean).join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gehoert der Ansprechpartner zu dieser Akte?
|
||||
*
|
||||
* Ohne die Pruefung koennte ein veraendertes Formular einen Kontakt aus
|
||||
* einer fremden Akte an den Vorgang haengen - und sein Name stuende
|
||||
* anschliessend im Briefkopf.
|
||||
*/
|
||||
async function ansprechpartnerPruefen(
|
||||
betreuteId: string,
|
||||
ansprechpartnerId: string | null,
|
||||
): Promise<{ ok: true } | { ok: false; zustand: Formularzustand }> {
|
||||
if (!ansprechpartnerId) return { ok: true }
|
||||
|
||||
const kontakt = await db.kontakt.findFirst({
|
||||
where: { id: ansprechpartnerId, betreuteId, geloeschtAm: null },
|
||||
select: { id: true },
|
||||
})
|
||||
if (kontakt) return { ok: true }
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
zustand: {
|
||||
fehler: 'Diesen Kontakt gibt es nicht (mehr).',
|
||||
felder: { ansprechpartnerId: 'Bitte einen Kontakt aus dem Verzeichnis wählen.' },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function vorgangAnlegen(
|
||||
_vorher: Formularzustand,
|
||||
formular: FormData,
|
||||
@@ -79,6 +118,10 @@ export async function vorgangAnlegen(
|
||||
}
|
||||
|
||||
const eingabe = gelesen.data
|
||||
|
||||
const geprueft = await ansprechpartnerPruefen(betreuteId, eingabe.ansprechpartnerId)
|
||||
if (!geprueft.ok) return geprueft.zustand
|
||||
|
||||
const jetzt = new Date()
|
||||
|
||||
// Aus der Vorlage entstehen Aufgaben und Fristen in einem Rutsch. Alles
|
||||
@@ -101,6 +144,7 @@ export async function vorgangAnlegen(
|
||||
prioritaet: eingabe.prioritaet,
|
||||
gegenstelle: eingabe.gegenstelle,
|
||||
aktenzeichen: eingabe.aktenzeichen,
|
||||
ansprechpartnerId: eingabe.ansprechpartnerId,
|
||||
verantwortlich: eingabe.verantwortlich ?? benutzer.name,
|
||||
naechsterSchritt:
|
||||
eingabe.naechsterSchritt ?? ergebnis?.aufgaben[0]?.titel ?? null,
|
||||
@@ -205,6 +249,10 @@ export async function vorgangAendern(
|
||||
}
|
||||
|
||||
const eingabe = gelesen.data
|
||||
|
||||
const geprueft = await ansprechpartnerPruefen(vorgang.betreuteId, eingabe.ansprechpartnerId)
|
||||
if (!geprueft.ok) return geprueft.zustand
|
||||
|
||||
const vorherOffen = oeffnen('Vorgang', vorgang)
|
||||
|
||||
const daten: Prisma.VorgangUpdateInput = {
|
||||
@@ -214,6 +262,11 @@ export async function vorgangAendern(
|
||||
prioritaet: eingabe.prioritaet,
|
||||
gegenstelle: eingabe.gegenstelle,
|
||||
aktenzeichen: eingabe.aktenzeichen,
|
||||
// Ueber die Beziehung, nicht ueber das Feld: connect/disconnect ist
|
||||
// das, was Prisma bei einer Update-Eingabe erwartet.
|
||||
ansprechpartner: eingabe.ansprechpartnerId
|
||||
? { connect: { id: eingabe.ansprechpartnerId } }
|
||||
: { disconnect: true },
|
||||
verantwortlich: eingabe.verantwortlich,
|
||||
naechsterSchritt: eingabe.naechsterSchritt,
|
||||
faelligAm: eingabe.faelligAm,
|
||||
@@ -227,6 +280,19 @@ export async function vorgangAendern(
|
||||
|
||||
const geaendert = await db.vorgang.update({ where: { id: vorgang.id }, data: daten })
|
||||
|
||||
// Der Wechsel des Ansprechpartners gehoert mit NAMEN ins Protokoll.
|
||||
// Zwei cuids nebeneinander sind protokolliert, aber nicht
|
||||
// nachvollziehbar - und nachvollziehbar war die Anforderung.
|
||||
const ansprechpartnerWechsel: Aenderung =
|
||||
vorgang.ansprechpartnerId !== geaendert.ansprechpartnerId
|
||||
? {
|
||||
ansprechpartner: {
|
||||
alt: await kontaktnameZu(vorgang.ansprechpartnerId),
|
||||
neu: await kontaktnameZu(geaendert.ansprechpartnerId),
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
await protokollieren({
|
||||
aktion: 'GEAENDERT',
|
||||
entitaet: 'Vorgang',
|
||||
@@ -238,6 +304,7 @@ export async function vorgangAendern(
|
||||
userName: kontext.benutzer.name,
|
||||
aenderung: {
|
||||
...aenderungErmitteln(vorgang, geaendert),
|
||||
...ansprechpartnerWechsel,
|
||||
...(vorherOffen.beschreibung !== (eingabe.beschreibung ?? null)
|
||||
? { beschreibung: { alt: vorherOffen.beschreibung, neu: eingabe.beschreibung } }
|
||||
: {}),
|
||||
|
||||
Reference in New Issue
Block a user