snooze vor expired, contracts, display snoozed contracts if an item is missing, un snooze implemented, fixed invoice upload bug
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { contractApi } from '../../services/api';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Badge from '../../components/ui/Badge';
|
||||
import Select from '../../components/ui/Select';
|
||||
import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import {
|
||||
AlertCircle,
|
||||
AlertTriangle,
|
||||
@@ -23,6 +25,9 @@ import {
|
||||
Tv,
|
||||
Car,
|
||||
Flame,
|
||||
BellOff,
|
||||
RotateCcw,
|
||||
Receipt,
|
||||
} from 'lucide-react';
|
||||
import type { CockpitContract, CockpitUrgencyLevel, ContractType } from '../../types';
|
||||
|
||||
@@ -77,6 +82,8 @@ const issueTypeIcons: Record<string, typeof Calendar> = {
|
||||
open_tasks: ClipboardList,
|
||||
pending_status: Clock,
|
||||
draft_status: FileText,
|
||||
review_due: RotateCcw,
|
||||
missing_invoice: Receipt,
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
@@ -86,9 +93,11 @@ const categoryLabels: Record<string, string> = {
|
||||
missingData: 'Fehlende Daten',
|
||||
openTasks: 'Offene Aufgaben',
|
||||
pendingContracts: 'Wartende Verträge',
|
||||
missingInvoices: 'Fehlende Rechnungen',
|
||||
reviewDue: 'Erneute Prüfung fällig',
|
||||
};
|
||||
|
||||
type FilterType = 'all' | 'critical' | 'warning' | 'ok' | 'deadlines' | 'credentials' | 'data' | 'tasks';
|
||||
type FilterType = 'all' | 'critical' | 'warning' | 'ok' | 'deadlines' | 'credentials' | 'data' | 'tasks' | 'review' | 'invoices';
|
||||
|
||||
export default function ContractCockpit() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -114,6 +123,46 @@ export default function ContractCockpit() {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [snoozeContractId, setSnoozeContractId] = useState<number | null>(null);
|
||||
const [customDate, setCustomDate] = useState('');
|
||||
const snoozeDropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close snooze dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (snoozeDropdownRef.current && !snoozeDropdownRef.current.contains(event.target as Node)) {
|
||||
setSnoozeContractId(null);
|
||||
setCustomDate('');
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const snoozeMutation = useMutation({
|
||||
mutationFn: ({ contractId, data }: { contractId: number; data: { months?: number; nextReviewDate?: string } }) =>
|
||||
contractApi.snooze(contractId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-cockpit'] });
|
||||
setSnoozeContractId(null);
|
||||
setCustomDate('');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSnooze = (contractId: number, months?: number) => {
|
||||
if (months) {
|
||||
snoozeMutation.mutate({ contractId, data: { months } });
|
||||
} else if (customDate) {
|
||||
snoozeMutation.mutate({ contractId, data: { nextReviewDate: customDate } });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnsnooze = (contractId: number) => {
|
||||
// Snooze aufheben: Leeres Objekt senden → nextReviewDate wird auf null gesetzt
|
||||
snoozeMutation.mutate({ contractId, data: {} });
|
||||
};
|
||||
|
||||
const toggleExpanded = (contractId: number) => {
|
||||
setExpandedContracts(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -155,6 +204,14 @@ export default function ContractCockpit() {
|
||||
return contracts.filter(c =>
|
||||
c.issues.some(i => ['open_tasks', 'pending_status', 'draft_status'].includes(i.type))
|
||||
);
|
||||
case 'review':
|
||||
return contracts.filter(c =>
|
||||
c.issues.some(i => i.type === 'review_due')
|
||||
);
|
||||
case 'invoices':
|
||||
return contracts.filter(c =>
|
||||
c.issues.some(i => i.type.includes('invoice'))
|
||||
);
|
||||
default:
|
||||
return contracts;
|
||||
}
|
||||
@@ -243,15 +300,97 @@ export default function ContractCockpit() {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<Link
|
||||
to={`/contracts/${contract.id}`}
|
||||
state={{ from: 'cockpit', filter: filter !== 'all' ? filter : undefined }}
|
||||
className="ml-4 p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Zum Vertrag"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
{/* Snooze Button */}
|
||||
<div className="relative" ref={snoozeContractId === contract.id ? snoozeDropdownRef : undefined}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSnoozeContractId(snoozeContractId === contract.id ? null : contract.id);
|
||||
setCustomDate('');
|
||||
}}
|
||||
className="p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
||||
title="Zurückstellen"
|
||||
>
|
||||
<BellOff className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{/* Snooze Dropdown */}
|
||||
{snoozeContractId === contract.id && (
|
||||
<div
|
||||
className="absolute right-0 top-full mt-1 w-56 bg-white border rounded-lg shadow-lg z-50 p-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="text-sm font-medium mb-2">Zurückstellen</div>
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => handleSnooze(contract.id, 3)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 rounded"
|
||||
disabled={snoozeMutation.isPending}
|
||||
>
|
||||
+3 Monate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSnooze(contract.id, 6)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 rounded bg-blue-50 border-blue-200"
|
||||
disabled={snoozeMutation.isPending}
|
||||
>
|
||||
+6 Monate <span className="text-xs text-gray-500">(Empfohlen)</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSnooze(contract.id, 12)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-gray-100 rounded"
|
||||
disabled={snoozeMutation.isPending}
|
||||
>
|
||||
+12 Monate
|
||||
</button>
|
||||
</div>
|
||||
<div className="border-t mt-2 pt-2">
|
||||
<label className="text-xs text-gray-500 block mb-1">Eigenes Datum:</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="date"
|
||||
value={customDate}
|
||||
onChange={(e) => setCustomDate(e.target.value)}
|
||||
className="flex-1 text-sm"
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleSnooze(contract.id)}
|
||||
disabled={!customDate || snoozeMutation.isPending}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Snooze aufheben - zeige nur wenn review_due Issue existiert */}
|
||||
{contract.issues.some(i => i.type === 'review_due') && (
|
||||
<div className="border-t mt-2 pt-2">
|
||||
<button
|
||||
onClick={() => handleUnsnooze(contract.id)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-red-50 text-red-600 rounded flex items-center gap-2"
|
||||
disabled={snoozeMutation.isPending}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Snooze aufheben
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/contracts/${contract.id}`}
|
||||
state={{ from: 'cockpit', filter: filter !== 'all' ? filter : undefined }}
|
||||
className="p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Zum Vertrag"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded: Issues */}
|
||||
@@ -387,6 +526,8 @@ export default function ContractCockpit() {
|
||||
{ value: 'credentials', label: `Zugangsdaten (${summary.byCategory.missingCredentials})` },
|
||||
{ value: 'data', label: `Fehlende Daten (${summary.byCategory.missingData})` },
|
||||
{ value: 'tasks', label: `Aufgaben/Status (${summary.byCategory.openTasks + summary.byCategory.pendingContracts})` },
|
||||
{ value: 'review', label: `Erneute Prüfung (${summary.byCategory.reviewDue || 0})` },
|
||||
{ value: 'invoices', label: `Fehlende Rechnungen (${summary.byCategory.missingInvoices || 0})` },
|
||||
]}
|
||||
className="w-64"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user