Vertragsstatus-Trigger: Datum beim Upload miterfassen

Beim automatischen Status-Wechsel wird jetzt auch das passende Datum gesetzt,
damit Status und Datumsfeld konsistent sind (Cockpit-Warnung "Datum fehlt"
verschwindet sofort nach Upload).

Backend:
- Upload-Handler für Kündigungsbestätigung(s-Optionen) nimmt optional
  `confirmationDate` aus multipart an, speichert als
  cancellationConfirmationDate / cancellationConfirmationOptionsDate.
  Fallback: heute (nur falls Feld noch leer war).
- maybeActivateOnDeliveryConfirmation nimmt optional deliveryDate, setzt
  Contract.startDate falls leer. Fallback: heute.

Frontend:
- ContractDetail: neues kleines Modal beim Kündigungsbestätigungs-Upload
  fragt das Bestätigungs-Datum ab (Default: heute oder bereits gesetzter
  Wert). Der bestehende inline-Datums-Editor bleibt für spätere Korrekturen.
- ContractDocumentsSection: Datums-Input erscheint conditional im
  Upload-Bereich, sobald Typ "Lieferbestätigung" gewählt ist.
- SaveAttachmentModal (E-Mail-Anhang → Vertragsdokument): gleicher
  Datums-Input conditional für "Lieferbestätigung".
- API-Methoden uploadCancellationConfirmation / uploadDocument /
  saveAttachmentAsContractDocument nehmen optional Datum entgegen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 13:40:04 +02:00
parent b554c8e436
commit c593700943
8 changed files with 185 additions and 28 deletions
@@ -1471,6 +1471,12 @@ export default function ContractDetail() {
// Un-Snooze Bestätigungsmodal
const [showUnsnoozeConfirm, setShowUnsnoozeConfirm] = useState(false);
// Kündigungsbestätigung-Upload: File gepuffert, Datum-Modal offen
const [pendingCancelFile, setPendingCancelFile] = useState<File | null>(null);
const [cancelConfirmDate, setCancelConfirmDate] = useState<string>(
() => new Date().toISOString().split('T')[0]
);
const { data, isLoading } = useQuery({
queryKey: ['contract', id],
queryFn: () => contractApi.getById(contractId),
@@ -2103,8 +2109,13 @@ export default function ContractDetail() {
</a>
<FileUpload
onUpload={async (file) => {
await uploadApi.uploadCancellationConfirmation(contractId, file);
queryClient.invalidateQueries({ queryKey: ['contract', id] });
// Datei puffern, Datums-Modal öffnen
setCancelConfirmDate(
c.cancellationConfirmationDate
? c.cancellationConfirmationDate.split('T')[0]
: new Date().toISOString().split('T')[0]
);
setPendingCancelFile(file);
}}
existingFile={c.cancellationConfirmationPath}
accept=".pdf"
@@ -2151,8 +2162,8 @@ export default function ContractDetail() {
) : (
<FileUpload
onUpload={async (file) => {
await uploadApi.uploadCancellationConfirmation(contractId, file);
queryClient.invalidateQueries({ queryKey: ['contract', id] });
setCancelConfirmDate(new Date().toISOString().split('T')[0]);
setPendingCancelFile(file);
}}
accept=".pdf"
label="PDF hochladen"
@@ -3068,6 +3079,52 @@ export default function ContractDetail() {
{/* Status-Info Modal */}
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
{/* Kündigungsbestätigung: Datum erfassen und dann Upload */}
<Modal
isOpen={pendingCancelFile !== null}
onClose={() => setPendingCancelFile(null)}
title="Kündigungsbestätigung Datum angeben"
size="sm"
>
<div className="space-y-4">
<p className="text-sm text-gray-700">
Wann wurde die Kündigung vom Anbieter bestätigt? Du kannst das Datum auch später noch anpassen.
</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Bestätigung erhalten am</label>
<input
type="date"
value={cancelConfirmDate}
onChange={(e) => setCancelConfirmDate(e.target.value)}
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex justify-end gap-3 pt-2">
<Button variant="secondary" onClick={() => setPendingCancelFile(null)}>
Abbrechen
</Button>
<Button
onClick={async () => {
if (!pendingCancelFile) return;
try {
await uploadApi.uploadCancellationConfirmation(
contractId,
pendingCancelFile,
cancelConfirmDate || undefined,
);
queryClient.invalidateQueries({ queryKey: ['contract', id] });
setPendingCancelFile(null);
} catch (err) {
alert('Fehler beim Hochladen: ' + (err instanceof Error ? err.message : 'Unbekannt'));
}
}}
>
Hochladen
</Button>
</div>
</div>
</Modal>
{/* Un-Snooze Bestätigungsmodal */}
<Modal
isOpen={showUnsnoozeConfirm}
@@ -3123,6 +3180,9 @@ function ContractDocumentsSection({
const [showUpload, setShowUpload] = useState(false);
const [uploadType, setUploadType] = useState(DOCUMENT_TYPES[0]);
const [uploadNotes, setUploadNotes] = useState('');
const [uploadDeliveryDate, setUploadDeliveryDate] = useState<string>(
() => new Date().toISOString().split('T')[0],
);
const { data: docsData } = useQuery({
queryKey: ['contract-documents', contractId],
@@ -3130,10 +3190,12 @@ function ContractDocumentsSection({
});
const uploadMutation = useMutation({
mutationFn: ({ file, documentType, notes }: { file: File; documentType: string; notes?: string }) =>
contractApi.uploadDocument(contractId, file, documentType, notes),
mutationFn: ({ file, documentType, notes, deliveryDate }: { file: File; documentType: string; notes?: string; deliveryDate?: string }) =>
contractApi.uploadDocument(contractId, file, documentType, notes, deliveryDate),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
// Contract selbst neu laden Status kann sich durch Lieferbestätigung geändert haben
queryClient.invalidateQueries({ queryKey: ['contract'] });
setShowUpload(false);
setUploadNotes('');
},
@@ -3148,10 +3210,17 @@ function ContractDocumentsSection({
const documents: ContractDocument[] = docsData?.data || [];
const isDelivery = uploadType.trim().toLowerCase() === 'lieferbestätigung';
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadMutation.mutate({ file, documentType: uploadType, notes: uploadNotes || undefined });
uploadMutation.mutate({
file,
documentType: uploadType,
notes: uploadNotes || undefined,
deliveryDate: isDelivery ? uploadDeliveryDate : undefined,
});
}
};
@@ -3197,6 +3266,20 @@ function ContractDocumentsSection({
/>
</div>
</div>
{isDelivery && (
<div className="mb-3 p-3 bg-white border border-blue-300 rounded">
<label className="block text-sm font-medium text-gray-700 mb-1">Lieferdatum</label>
<input
type="date"
value={uploadDeliveryDate}
onChange={(e) => setUploadDeliveryDate(e.target.value)}
className="block w-full max-w-[220px] px-3 py-2 border border-gray-300 rounded-lg text-sm"
/>
<p className="text-xs text-gray-500 mt-1">
Falls der Vertrag noch auf Entwurf steht, wird er auf Aktiv gesetzt und dieses Datum als Vertragsbeginn übernommen.
</p>
</div>
)}
<div className="flex items-center gap-3">
<label className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer text-sm">
<Plus className="w-4 h-4" />