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:
parent
b554c8e436
commit
c593700943
|
|
@ -2002,8 +2002,9 @@ export async function saveAttachmentAsContractDocument(req: Request, res: Respon
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Falls Lieferbestätigung + Vertrag DRAFT → automatisch auf ACTIVE
|
// Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer
|
||||||
await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req);
|
const deliveryDate = typeof req.body?.deliveryDate === 'string' ? req.body.deliveryDate : null;
|
||||||
|
await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req, deliveryDate);
|
||||||
|
|
||||||
res.json({ success: true, data: doc } as ApiResponse);
|
res.json({ success: true, data: doc } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,7 @@ export async function getContractDocuments(req: AuthRequest, res: Response): Pro
|
||||||
export async function uploadContractDocument(req: AuthRequest, res: Response): Promise<void> {
|
export async function uploadContractDocument(req: AuthRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const contractId = parseInt(req.params.id);
|
const contractId = parseInt(req.params.id);
|
||||||
const { documentType, notes } = req.body;
|
const { documentType, notes, deliveryDate } = req.body;
|
||||||
|
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' } as ApiResponse);
|
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' } as ApiResponse);
|
||||||
|
|
@ -495,8 +495,8 @@ export async function uploadContractDocument(req: AuthRequest, res: Response): P
|
||||||
customerId: contract?.customerId,
|
customerId: contract?.customerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Falls Lieferbestätigung + Vertrag DRAFT → automatisch auf ACTIVE
|
// Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer
|
||||||
await maybeActivateOnDeliveryConfirmation(contractId, documentType, req);
|
await maybeActivateOnDeliveryConfirmation(contractId, documentType, req, deliveryDate);
|
||||||
|
|
||||||
res.status(201).json({ success: true, data: doc } as ApiResponse);
|
res.status(201).json({ success: true, data: doc } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -563,10 +563,30 @@ async function handleContractDocumentUpload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bei Kündigungsbestätigung(s-Optionen): optionales Datum aus multipart
|
||||||
|
// übernehmen. Ohne Angabe: falls Feld noch leer → heute, sonst nicht anfassen.
|
||||||
|
const updateData: Record<string, unknown> = { [fieldName]: relativePath };
|
||||||
|
if (fieldName === 'cancellationConfirmationPath' || fieldName === 'cancellationConfirmationOptionsPath') {
|
||||||
|
const dateField = fieldName === 'cancellationConfirmationPath'
|
||||||
|
? 'cancellationConfirmationDate'
|
||||||
|
: 'cancellationConfirmationOptionsDate';
|
||||||
|
const provided = typeof req.body?.confirmationDate === 'string' ? req.body.confirmationDate : null;
|
||||||
|
let target: Date | null = null;
|
||||||
|
if (provided) {
|
||||||
|
const parsed = new Date(provided);
|
||||||
|
if (!isNaN(parsed.getTime())) target = parsed;
|
||||||
|
}
|
||||||
|
if (target) {
|
||||||
|
updateData[dateField] = target;
|
||||||
|
} else if (!contract[dateField]) {
|
||||||
|
updateData[dateField] = new Date();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Vertrag in der DB aktualisieren
|
// Vertrag in der DB aktualisieren
|
||||||
await prisma.contract.update({
|
await prisma.contract.update({
|
||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
data: { [fieldName]: relativePath },
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde und
|
// Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde und
|
||||||
|
|
|
||||||
|
|
@ -86,8 +86,9 @@ export { runExpireCheck };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine
|
* Wird nach einem ContractDocument-Upload aufgerufen. Wenn der Typ eine
|
||||||
* Lieferbestätigung ist UND der Vertrag aktuell DRAFT ist, wird er auf
|
* Lieferbestätigung ist:
|
||||||
* ACTIVE gesetzt (+ Audit-Log). Andere Typen/Status bleiben unangetastet.
|
* - Contract.status von DRAFT auf ACTIVE setzen (falls DRAFT)
|
||||||
|
* - Contract.startDate auf deliveryDate (oder heute) setzen, falls noch leer
|
||||||
*
|
*
|
||||||
* Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown
|
* Schreibweise "Lieferbestätigung" stammt aus dem Frontend-Dropdown
|
||||||
* (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive +
|
* (SaveAttachmentModal / ContractDetail). Vergleich case-insensitive +
|
||||||
|
|
@ -97,19 +98,43 @@ export async function maybeActivateOnDeliveryConfirmation(
|
||||||
contractId: number,
|
contractId: number,
|
||||||
documentType: string,
|
documentType: string,
|
||||||
req: unknown,
|
req: unknown,
|
||||||
|
deliveryDate?: Date | string | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!documentType || typeof documentType !== 'string') return;
|
if (!documentType || typeof documentType !== 'string') return;
|
||||||
if (documentType.trim().toLowerCase() !== 'lieferbestätigung') return;
|
if (documentType.trim().toLowerCase() !== 'lieferbestätigung') return;
|
||||||
|
|
||||||
const contract = await prisma.contract.findUnique({
|
const contract = await prisma.contract.findUnique({
|
||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
select: { status: true, contractNumber: true, customerId: true },
|
select: { status: true, contractNumber: true, customerId: true, startDate: true },
|
||||||
});
|
});
|
||||||
if (!contract || contract.status !== 'DRAFT') return;
|
if (!contract) return;
|
||||||
|
|
||||||
|
// deliveryDate parsen, Fallback auf heute
|
||||||
|
let parsedDate: Date | null = null;
|
||||||
|
if (deliveryDate) {
|
||||||
|
const parsed = new Date(deliveryDate);
|
||||||
|
if (!isNaN(parsed.getTime())) parsedDate = parsed;
|
||||||
|
}
|
||||||
|
const effectiveDate = parsedDate || new Date();
|
||||||
|
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};
|
||||||
|
|
||||||
|
if (contract.status === 'DRAFT') {
|
||||||
|
updateData.status = 'ACTIVE';
|
||||||
|
changes.status = { vorher: 'DRAFT', nachher: 'ACTIVE' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contract.startDate) {
|
||||||
|
updateData.startDate = effectiveDate;
|
||||||
|
changes.startDate = { vorher: null, nachher: effectiveDate.toISOString().split('T')[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length === 0) return;
|
||||||
|
|
||||||
await prisma.contract.update({
|
await prisma.contract.update({
|
||||||
where: { id: contractId },
|
where: { id: contractId },
|
||||||
data: { status: 'ACTIVE' },
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
await logChange({
|
await logChange({
|
||||||
|
|
@ -117,8 +142,8 @@ export async function maybeActivateOnDeliveryConfirmation(
|
||||||
action: 'UPDATE',
|
action: 'UPDATE',
|
||||||
resourceType: 'Contract',
|
resourceType: 'Contract',
|
||||||
resourceId: contractId.toString(),
|
resourceId: contractId.toString(),
|
||||||
label: `Vertrag ${contract.contractNumber} automatisch auf ACTIVE gesetzt (Lieferbestätigung hochgeladen)`,
|
label: `Vertrag ${contract.contractNumber} automatisch aktualisiert (Lieferbestätigung hochgeladen)`,
|
||||||
details: { vorher: 'DRAFT', nachher: 'ACTIVE', trigger: 'Lieferbestätigung-Upload' },
|
details: { ...changes, trigger: 'Lieferbestätigung-Upload' },
|
||||||
customerId: contract.customerId,
|
customerId: contract.customerId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,16 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||||
`status=ACTIVE` und `endDate < heute` → `EXPIRED` (mit Audit-Log).
|
`status=ACTIVE` und `endDate < heute` → `EXPIRED` (mit Audit-Log).
|
||||||
- Beim Upload der Kündigungsbestätigung (`cancellationConfirmationPath`):
|
- Beim Upload der Kündigungsbestätigung (`cancellationConfirmationPath`):
|
||||||
wenn Vertrag aktuell `ACTIVE` → auf `CANCELLED` setzen (Audit-Log).
|
wenn Vertrag aktuell `ACTIVE` → auf `CANCELLED` setzen (Audit-Log).
|
||||||
Der "Optionen"-Upload löst den Wechsel bewusst NICHT aus, da er für
|
Frontend fragt per Modal das Bestätigungs-Datum ab (Default: heute),
|
||||||
Vertragsänderungen (nicht echte Kündigungen) gedacht ist.
|
wird direkt als `cancellationConfirmationDate` gespeichert.
|
||||||
|
Der "Optionen"-Upload löst den Status-Wechsel bewusst NICHT aus, da er
|
||||||
|
für Vertragsänderungen (nicht echte Kündigungen) gedacht ist, setzt
|
||||||
|
aber `cancellationConfirmationOptionsDate` analog.
|
||||||
- Beim Upload einer `Lieferbestätigung` (ContractDocument via direkt-Upload
|
- Beim Upload einer `Lieferbestätigung` (ContractDocument via direkt-Upload
|
||||||
oder Email-Anhang-Import): wenn Vertrag aktuell `DRAFT` → auf `ACTIVE`
|
oder Email-Anhang-Import): wenn Vertrag aktuell `DRAFT` → auf `ACTIVE`
|
||||||
setzen (Audit-Log). Schreibweise stammt aus dem Frontend-Dropdown,
|
setzen + `startDate` auf das erfasste Lieferdatum (falls leer).
|
||||||
Vergleich case-insensitive + getrimmt.
|
Frontend zeigt Datums-Input conditional, wenn Typ "Lieferbestätigung"
|
||||||
|
ausgewählt ist.
|
||||||
- Keine neuen Status eingeführt: `cancellationSentDate` vs.
|
- Keine neuen Status eingeführt: `cancellationSentDate` vs.
|
||||||
`cancellationConfirmationDate` genügen, um "gesendet vs. bestätigt"
|
`cancellationConfirmationDate` genügen, um "gesendet vs. bestätigt"
|
||||||
abzubilden. `ACTIVE` bleibt bis zur Bestätigung.
|
abzubilden. `ACTIVE` bleibt bis zur Bestätigung.
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ export default function SaveAttachmentModal({
|
||||||
const [contractDocumentData, setContractDocumentData] = useState({
|
const [contractDocumentData, setContractDocumentData] = useState({
|
||||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||||
notes: '',
|
notes: '',
|
||||||
|
deliveryDate: new Date().toISOString().split('T')[0],
|
||||||
});
|
});
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
@ -130,9 +131,11 @@ export default function SaveAttachmentModal({
|
||||||
|
|
||||||
const saveContractDocumentMutation = useMutation({
|
const saveContractDocumentMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
|
const isDelivery = contractDocumentData.documentType.trim().toLowerCase() === 'lieferbestätigung';
|
||||||
return cachedEmailApi.saveAttachmentAsContractDocument(emailId, attachmentFilename, {
|
return cachedEmailApi.saveAttachmentAsContractDocument(emailId, attachmentFilename, {
|
||||||
documentType: contractDocumentData.documentType,
|
documentType: contractDocumentData.documentType,
|
||||||
notes: contractDocumentData.notes || undefined,
|
notes: contractDocumentData.notes || undefined,
|
||||||
|
deliveryDate: isDelivery ? contractDocumentData.deliveryDate : undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -164,6 +167,7 @@ export default function SaveAttachmentModal({
|
||||||
setContractDocumentData({
|
setContractDocumentData({
|
||||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||||
notes: '',
|
notes: '',
|
||||||
|
deliveryDate: new Date().toISOString().split('T')[0],
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
@ -459,6 +463,23 @@ export default function SaveAttachmentModal({
|
||||||
}
|
}
|
||||||
placeholder="Optionale Anmerkungen..."
|
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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1471,6 +1471,12 @@ export default function ContractDetail() {
|
||||||
// Un-Snooze Bestätigungsmodal
|
// Un-Snooze Bestätigungsmodal
|
||||||
const [showUnsnoozeConfirm, setShowUnsnoozeConfirm] = useState(false);
|
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({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['contract', id],
|
queryKey: ['contract', id],
|
||||||
queryFn: () => contractApi.getById(contractId),
|
queryFn: () => contractApi.getById(contractId),
|
||||||
|
|
@ -2103,8 +2109,13 @@ export default function ContractDetail() {
|
||||||
</a>
|
</a>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
onUpload={async (file) => {
|
onUpload={async (file) => {
|
||||||
await uploadApi.uploadCancellationConfirmation(contractId, file);
|
// Datei puffern, Datums-Modal öffnen
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract', id] });
|
setCancelConfirmDate(
|
||||||
|
c.cancellationConfirmationDate
|
||||||
|
? c.cancellationConfirmationDate.split('T')[0]
|
||||||
|
: new Date().toISOString().split('T')[0]
|
||||||
|
);
|
||||||
|
setPendingCancelFile(file);
|
||||||
}}
|
}}
|
||||||
existingFile={c.cancellationConfirmationPath}
|
existingFile={c.cancellationConfirmationPath}
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
|
|
@ -2151,8 +2162,8 @@ export default function ContractDetail() {
|
||||||
) : (
|
) : (
|
||||||
<FileUpload
|
<FileUpload
|
||||||
onUpload={async (file) => {
|
onUpload={async (file) => {
|
||||||
await uploadApi.uploadCancellationConfirmation(contractId, file);
|
setCancelConfirmDate(new Date().toISOString().split('T')[0]);
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract', id] });
|
setPendingCancelFile(file);
|
||||||
}}
|
}}
|
||||||
accept=".pdf"
|
accept=".pdf"
|
||||||
label="PDF hochladen"
|
label="PDF hochladen"
|
||||||
|
|
@ -3068,6 +3079,52 @@ export default function ContractDetail() {
|
||||||
{/* Status-Info Modal */}
|
{/* Status-Info Modal */}
|
||||||
<StatusInfoModal isOpen={showStatusInfo} onClose={() => setShowStatusInfo(false)} />
|
<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 */}
|
{/* Un-Snooze Bestätigungsmodal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showUnsnoozeConfirm}
|
isOpen={showUnsnoozeConfirm}
|
||||||
|
|
@ -3123,6 +3180,9 @@ function ContractDocumentsSection({
|
||||||
const [showUpload, setShowUpload] = useState(false);
|
const [showUpload, setShowUpload] = useState(false);
|
||||||
const [uploadType, setUploadType] = useState(DOCUMENT_TYPES[0]);
|
const [uploadType, setUploadType] = useState(DOCUMENT_TYPES[0]);
|
||||||
const [uploadNotes, setUploadNotes] = useState('');
|
const [uploadNotes, setUploadNotes] = useState('');
|
||||||
|
const [uploadDeliveryDate, setUploadDeliveryDate] = useState<string>(
|
||||||
|
() => new Date().toISOString().split('T')[0],
|
||||||
|
);
|
||||||
|
|
||||||
const { data: docsData } = useQuery({
|
const { data: docsData } = useQuery({
|
||||||
queryKey: ['contract-documents', contractId],
|
queryKey: ['contract-documents', contractId],
|
||||||
|
|
@ -3130,10 +3190,12 @@ function ContractDocumentsSection({
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadMutation = useMutation({
|
const uploadMutation = useMutation({
|
||||||
mutationFn: ({ file, documentType, notes }: { file: File; documentType: string; notes?: string }) =>
|
mutationFn: ({ file, documentType, notes, deliveryDate }: { file: File; documentType: string; notes?: string; deliveryDate?: string }) =>
|
||||||
contractApi.uploadDocument(contractId, file, documentType, notes),
|
contractApi.uploadDocument(contractId, file, documentType, notes, deliveryDate),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
|
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);
|
setShowUpload(false);
|
||||||
setUploadNotes('');
|
setUploadNotes('');
|
||||||
},
|
},
|
||||||
|
|
@ -3148,10 +3210,17 @@ function ContractDocumentsSection({
|
||||||
|
|
||||||
const documents: ContractDocument[] = docsData?.data || [];
|
const documents: ContractDocument[] = docsData?.data || [];
|
||||||
|
|
||||||
|
const isDelivery = uploadType.trim().toLowerCase() === 'lieferbestätigung';
|
||||||
|
|
||||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
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>
|
||||||
</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">
|
<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">
|
<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" />
|
<Plus className="w-4 h-4" />
|
||||||
|
|
|
||||||
|
|
@ -588,7 +588,7 @@ export const cachedEmailApi = {
|
||||||
saveAttachmentAsContractDocument: async (
|
saveAttachmentAsContractDocument: async (
|
||||||
emailId: number,
|
emailId: number,
|
||||||
filename: string,
|
filename: string,
|
||||||
params: { documentType: string; notes?: string },
|
params: { documentType: string; notes?: string; deliveryDate?: string },
|
||||||
) => {
|
) => {
|
||||||
const encodedFilename = encodeURIComponent(filename);
|
const encodedFilename = encodeURIComponent(filename);
|
||||||
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
|
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`);
|
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
|
||||||
return res.data;
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('documentType', documentType);
|
formData.append('documentType', documentType);
|
||||||
if (notes) formData.append('notes', notes);
|
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, {
|
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
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`);
|
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter`);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
uploadCancellationConfirmation: async (contractId: number, file: File) => {
|
uploadCancellationConfirmation: async (contractId: number, file: File, confirmationDate?: string) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('document', file);
|
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, {
|
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
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`);
|
const res = await api.delete<ApiResponse<void>>(`/upload/contracts/${contractId}/cancellation-letter-options`);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
uploadCancellationConfirmationOptions: async (contractId: number, file: File) => {
|
uploadCancellationConfirmationOptions: async (contractId: number, file: File, confirmationDate?: string) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('document', file);
|
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, {
|
const res = await api.post<ApiResponse<{ path: string; filename: string }>>(`/upload/contracts/${contractId}/cancellation-confirmation-options`, formData, {
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue