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:
@@ -56,6 +56,7 @@ export default function SaveAttachmentModal({
|
||||
const [contractDocumentData, setContractDocumentData] = useState({
|
||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||
notes: '',
|
||||
deliveryDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -130,9 +131,11 @@ export default function SaveAttachmentModal({
|
||||
|
||||
const saveContractDocumentMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const isDelivery = contractDocumentData.documentType.trim().toLowerCase() === 'lieferbestätigung';
|
||||
return cachedEmailApi.saveAttachmentAsContractDocument(emailId, attachmentFilename, {
|
||||
documentType: contractDocumentData.documentType,
|
||||
notes: contractDocumentData.notes || undefined,
|
||||
deliveryDate: isDelivery ? contractDocumentData.deliveryDate : undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -164,6 +167,7 @@ export default function SaveAttachmentModal({
|
||||
setContractDocumentData({
|
||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||
notes: '',
|
||||
deliveryDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
@@ -459,6 +463,23 @@ export default function SaveAttachmentModal({
|
||||
}
|
||||
placeholder="Optionale Anmerkungen..."
|
||||
/>
|
||||
|
||||
{contractDocumentData.documentType.trim().toLowerCase() === 'lieferbestätigung' && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Lieferdatum</label>
|
||||
<input
|
||||
type="date"
|
||||
value={contractDocumentData.deliveryDate}
|
||||
onChange={(e) =>
|
||||
setContractDocumentData({ ...contractDocumentData, deliveryDate: 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-600 mt-1">
|
||||
Falls der Vertrag noch auf Entwurf steht, wird er auf Aktiv gesetzt und dieses Datum als Vertragsbeginn übernommen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -588,7 +588,7 @@ export const cachedEmailApi = {
|
||||
saveAttachmentAsContractDocument: async (
|
||||
emailId: number,
|
||||
filename: string,
|
||||
params: { documentType: string; notes?: string },
|
||||
params: { documentType: string; notes?: string; deliveryDate?: string },
|
||||
) => {
|
||||
const encodedFilename = encodeURIComponent(filename);
|
||||
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
|
||||
@@ -683,11 +683,12 @@ export const contractApi = {
|
||||
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
|
||||
return res.data;
|
||||
},
|
||||
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string) => {
|
||||
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string, deliveryDate?: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('documentType', documentType);
|
||||
if (notes) formData.append('notes', notes);
|
||||
if (deliveryDate) formData.append('deliveryDate', deliveryDate);
|
||||
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
@@ -1087,9 +1088,10 @@ export const uploadApi = {
|
||||
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter`);
|
||||
return res.data;
|
||||
},
|
||||
uploadCancellationConfirmation: async (contractId: number, file: File) => {
|
||||
uploadCancellationConfirmation: async (contractId: number, file: File, confirmationDate?: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('document', file);
|
||||
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
|
||||
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
@@ -1111,9 +1113,10 @@ export const uploadApi = {
|
||||
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter-options`);
|
||||
return res.data;
|
||||
},
|
||||
uploadCancellationConfirmationOptions: async (contractId: number, file: File) => {
|
||||
uploadCancellationConfirmationOptions: async (contractId: number, file: File, confirmationDate?: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('document', file);
|
||||
if (confirmationDate) formData.append('confirmationDate', confirmationDate);
|
||||
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user