Aufgaben ohne Kunde/Vertrag anlegbar
ContractTask.contractId nullable (Migration). Neuer staff-only Endpoint POST /tasks fuer allgemeine Aufgaben ohne Vertrag/Kunde. Ohne Vertrag gibt es keinen Kunden -> visibleInPortal serverseitig immer false, Portal-Reply 403 bei contractloser Aufgabe, getAllTasks-Portal-Filter schliesst sie automatisch aus (kein contract-Match). Task-Modal (Mitarbeiter): Checkbox "Ohne Kunde (allgemeine Aufgabe)" blendet Kunden-/Vertragsauswahl UND "Im Kundenportal sichtbar" aus. Task-Liste zeigt solche Aufgaben als "Allgemeine Aufgabe (ohne Vertrag)" ohne Vertrags-Link/ Zum-Vertrag-Button. Verifiziert: contractlose Aufgabe -> contractId null, visibleInPortal erzwungen false (auch wenn true geschickt); mit Vertrag weiterhin waehlbar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -196,7 +196,7 @@ export default function TaskList() {
|
||||
|
||||
const contractDisplay = task.contract
|
||||
? `${task.contract.contractNumber} - ${task.contract.provider?.name || task.contract.providerName || 'Kein Anbieter'}`
|
||||
: `Vertrag #${task.contractId}`;
|
||||
: (task.contractId != null ? `Vertrag #${task.contractId}` : 'Allgemeine Aufgabe (ohne Vertrag)');
|
||||
|
||||
const customerDisplay = task.contract?.customer
|
||||
? (task.contract.customer.companyName || `${task.contract.customer.firstName} ${task.contract.customer.lastName}`)
|
||||
@@ -242,13 +242,17 @@ export default function TaskList() {
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1 flex items-center gap-2">
|
||||
<FileText className="w-4 h-4" />
|
||||
<Link
|
||||
to={`/contracts/${task.contractId}`}
|
||||
className="text-blue-600 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{contractDisplay}
|
||||
</Link>
|
||||
{task.contractId != null ? (
|
||||
<Link
|
||||
to={`/contracts/${task.contractId}`}
|
||||
className="text-blue-600 hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{contractDisplay}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-gray-500 italic">{contractDisplay}</span>
|
||||
)}
|
||||
{showCustomer && customerDisplay && (
|
||||
<>
|
||||
<span className="text-gray-400">|</span>
|
||||
@@ -296,17 +300,19 @@ export default function TaskList() {
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/contracts/${task.contractId}`);
|
||||
}}
|
||||
title="Zum Vertrag"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{task.contractId != null && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/contracts/${task.contractId}`);
|
||||
}}
|
||||
title="Zum Vertrag"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -763,6 +769,7 @@ function CreateTaskModal({
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedCustomerId, setSelectedCustomerId] = useState<number | null>(null);
|
||||
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
|
||||
const [noCustomer, setNoCustomer] = useState(false);
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [visibleInPortal, setVisibleInPortal] = useState(false);
|
||||
@@ -810,15 +817,25 @@ function CreateTaskModal({
|
||||
}, [contractsData?.data, contractSearch]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedContractId || !title.trim()) return;
|
||||
if (!title.trim()) return;
|
||||
if (!noCustomer && !selectedContractId) return;
|
||||
|
||||
const contractId = selectedContractId;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await contractTaskApi.create(selectedContractId, {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
visibleInPortal,
|
||||
});
|
||||
if (noCustomer) {
|
||||
// Allgemeine Aufgabe ohne Vertrag/Kunde – nie im Portal sichtbar.
|
||||
await contractTaskApi.createGeneral({
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
});
|
||||
} else {
|
||||
await contractTaskApi.create(contractId!, {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
visibleInPortal,
|
||||
});
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
|
||||
onClose();
|
||||
@@ -828,8 +845,9 @@ function CreateTaskModal({
|
||||
setVisibleInPortal(false);
|
||||
setSelectedContractId(null);
|
||||
setSelectedCustomerId(null);
|
||||
// Navigate to the contract
|
||||
navigate(`/contracts/${selectedContractId}`);
|
||||
setNoCustomer(false);
|
||||
// Bei vertragsgebundener Aufgabe zum Vertrag springen.
|
||||
if (!noCustomer && contractId) navigate(`/contracts/${contractId}`);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen der Aufgabe:', error);
|
||||
alert('Fehler beim Erstellen der Aufgabe. Bitte versuchen Sie es erneut.');
|
||||
@@ -844,6 +862,7 @@ function CreateTaskModal({
|
||||
setVisibleInPortal(false);
|
||||
setSelectedContractId(null);
|
||||
setSelectedCustomerId(null);
|
||||
setNoCustomer(false);
|
||||
setCustomerSearch('');
|
||||
setContractSearch('');
|
||||
onClose();
|
||||
@@ -861,7 +880,27 @@ function CreateTaskModal({
|
||||
title="Neue Aufgabe"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Ohne Kunde (allgemeine Aufgabe) */}
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={noCustomer}
|
||||
onChange={(e) => {
|
||||
setNoCustomer(e.target.checked);
|
||||
if (e.target.checked) {
|
||||
// Ohne Kunde → Kunden-/Vertragsauswahl + Portal-Sichtbarkeit weg.
|
||||
setSelectedCustomerId(null);
|
||||
setSelectedContractId(null);
|
||||
setVisibleInPortal(false);
|
||||
}
|
||||
}}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Ohne Kunde (allgemeine Aufgabe)</span>
|
||||
</label>
|
||||
|
||||
{/* Kundenauswahl */}
|
||||
{!noCustomer && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Kunde *
|
||||
@@ -896,6 +935,7 @@ function CreateTaskModal({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vertragsauswahl (nur wenn Kunde ausgewählt) */}
|
||||
{selectedCustomerId && (
|
||||
@@ -969,7 +1009,8 @@ function CreateTaskModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Im Kundenportal sichtbar */}
|
||||
{/* Im Kundenportal sichtbar – nur wenn ein Kunde/Vertrag zugeordnet ist */}
|
||||
{!noCustomer && (
|
||||
<div>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@@ -981,6 +1022,7 @@ function CreateTaskModal({
|
||||
<span className="text-sm text-gray-700">Im Kundenportal sichtbar</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
@@ -989,7 +1031,7 @@ function CreateTaskModal({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={!selectedContractId || !title.trim() || isSubmitting}
|
||||
disabled={(!noCustomer && !selectedContractId) || !title.trim() || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Wird erstellt...' : 'Aufgabe erstellen'}
|
||||
</Button>
|
||||
|
||||
@@ -1072,6 +1072,11 @@ export const contractTaskApi = {
|
||||
const res = await api.post<ApiResponse<ContractTask>>(`/contracts/${contractId}/tasks`, data);
|
||||
return res.data;
|
||||
},
|
||||
// Allgemeine Aufgabe ohne Vertrag/Kunde (nur Mitarbeiter).
|
||||
createGeneral: async (data: { title: string; description?: string }) => {
|
||||
const res = await api.post<ApiResponse<ContractTask>>(`/tasks`, data);
|
||||
return res.data;
|
||||
},
|
||||
update: async (taskId: number, data: { title?: string; description?: string; visibleInPortal?: boolean }) => {
|
||||
const res = await api.put<ApiResponse<ContractTask>>(`/tasks/${taskId}`, data);
|
||||
return res.data;
|
||||
|
||||
@@ -471,7 +471,7 @@ export interface ContractTaskContract {
|
||||
|
||||
export interface ContractTask {
|
||||
id: number;
|
||||
contractId: number;
|
||||
contractId: number | null;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: ContractTaskStatus;
|
||||
|
||||
Reference in New Issue
Block a user