added date at support ticket, new order support tickets, delete edit support ticktes only from enploye and admins

This commit is contained in:
duffyduck 2026-02-08 18:26:34 +01:00
parent 06489299d5
commit ee4f1aacdd
8 changed files with 888 additions and 752 deletions

View File

@ -1 +1 @@
{"version":3,"file":"contractTask.service.d.ts","sourceRoot":"","sources":["../../src/services/contractTask.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAIlE,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;;;;;;;;;;;;;;;;;;;;;;MAsCpE;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;UAI3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;;;;;;;;;GAUA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;;;;;;;;;;;GAMF;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAQ5C;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAQ1C;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAI1C;AAID,wBAAsB,aAAa,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;GAQ9F;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;GAKvE;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GA8B/C;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GA0B7C;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GAI7C;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;WAK9C;AAID,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;IACrC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA0EzD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe;;GA6B1D"}
{"version":3,"file":"contractTask.service.d.ts","sourceRoot":"","sources":["../../src/services/contractTask.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAIlE,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;;;;;;;;;;;;;;;;;;;;;;MAgCpE;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;UAI3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;;;;;;;;;;;GAUA;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;;;;;;;;;;;GAMF;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAQ5C;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAQ1C;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;GAI1C;AAID,wBAAsB,aAAa,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;GAQ9F;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;GAKvE;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GA8B/C;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GA0B7C;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,MAAM;;;;;;;;;GAI7C;AAED,wBAAsB,cAAc,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;WAK9C;AAID,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;IACrC,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAoEzD;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,eAAe;;GA6B1D"}

View File

@ -42,16 +42,10 @@ async function getTasksByContract(filters) {
where,
include: {
subtasks: {
orderBy: [
{ status: 'asc' },
{ createdAt: 'asc' },
],
orderBy: { createdAt: 'asc' },
},
},
orderBy: [
{ status: 'asc' }, // OPEN first, then COMPLETED
{ createdAt: 'desc' },
],
orderBy: { createdAt: 'desc' },
});
}
async function getTaskById(id) {
@ -210,10 +204,7 @@ async function getAllTasks(filters) {
where,
include: {
subtasks: {
orderBy: [
{ status: 'asc' },
{ createdAt: 'asc' },
],
orderBy: { createdAt: 'asc' },
},
contract: {
select: {
@ -246,10 +237,7 @@ async function getAllTasks(filters) {
},
},
},
orderBy: [
{ status: 'asc' }, // OPEN first, then COMPLETED
{ createdAt: 'desc' },
],
orderBy: { createdAt: 'desc' },
});
}
async function getTaskStats(filters) {

File diff suppressed because one or more lines are too long

View File

@ -37,16 +37,10 @@ export async function getTasksByContract(filters: ContractTaskFilters) {
where,
include: {
subtasks: {
orderBy: [
{ status: 'asc' },
{ createdAt: 'asc' },
],
orderBy: { createdAt: 'asc' },
},
},
orderBy: [
{ status: 'asc' }, // OPEN first, then COMPLETED
{ createdAt: 'desc' },
],
orderBy: { createdAt: 'desc' },
});
}
@ -249,10 +243,7 @@ export async function getAllTasks(filters: AllTasksFilters) {
where,
include: {
subtasks: {
orderBy: [
{ status: 'asc' },
{ createdAt: 'asc' },
],
orderBy: { createdAt: 'asc' },
},
contract: {
select: {
@ -285,10 +276,7 @@ export async function getAllTasks(filters: AllTasksFilters) {
},
},
},
orderBy: [
{ status: 'asc' }, // OPEN first, then COMPLETED
{ createdAt: 'desc' },
],
orderBy: { createdAt: 'desc' },
});
}

File diff suppressed because one or more lines are too long

715
frontend/dist/assets/index-XQdvYOWp.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenCRM</title>
<script type="module" crossorigin src="/assets/index-OTeAOPxR.js"></script>
<script type="module" crossorigin src="/assets/index-XQdvYOWp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B06MVODt.css">
</head>
<body>

View File

@ -21,6 +21,8 @@ import {
ChevronRight,
FileText,
Send,
Edit,
Trash2,
} from 'lucide-react';
import type { ContractTask, ContractTaskStatus, Contract, Customer, ContractTaskSubtask } from '../../types';
@ -42,6 +44,7 @@ export default function TaskList() {
const [expandedTasks, setExpandedTasks] = useState<Set<number>>(new Set());
const [showCreateModal, setShowCreateModal] = useState(false);
const [replyInputs, setReplyInputs] = useState<Record<number, string>>({});
const [editingTask, setEditingTask] = useState<ContractTask | null>(null);
// Labels abhängig von Benutzertyp
const pageTitle = isCustomerPortal ? 'Support-Anfragen' : 'Aufgaben';
@ -94,6 +97,23 @@ export default function TaskList() {
},
});
const deleteTaskMutation = useMutation({
mutationFn: (taskId: number) => contractTaskApi.delete(taskId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
queryClient.invalidateQueries({ queryKey: ['task-stats'] });
},
});
const updateTaskMutation = useMutation({
mutationFn: ({ taskId, data }: { taskId: number; data: { title?: string; description?: string; visibleInPortal?: boolean } }) =>
contractTaskApi.update(taskId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['all-tasks'] });
setEditingTask(null);
},
});
// Gruppiere Tasks nach Vertrag für Kundenportal
const groupedTasks = useMemo(() => {
if (!tasksData?.data) return { ownTasks: [], representedTasks: [], allTasks: [] };
@ -159,6 +179,20 @@ export default function TaskList() {
const totalSubtasks = task.subtasks?.length || 0;
const isTaskCompleted = task.status === 'COMPLETED';
// Datum+Uhrzeit für Tasks
const taskCreatedDateTime = new Date(task.createdAt).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
// Mitarbeiter können eigene Aufgaben bearbeiten/löschen
// Kunden können Support-Tickets NICHT löschen/bearbeiten
const canEditTask = !isCustomerPortal && hasPermission('contracts:update');
const canDeleteTask = !isCustomerPortal && hasPermission('contracts:update');
const contractDisplay = task.contract
? `${task.contract.contractNumber} - ${task.contract.provider?.name || task.contract.providerName || 'Kein Anbieter'}`
: `Vertrag #${task.contractId}`;
@ -224,10 +258,43 @@ export default function TaskList() {
{task.description && (
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{task.description}</p>
)}
{/* Ersteller und Datum */}
<div className="text-xs text-gray-400 mt-1">
{task.createdBy} {taskCreatedDateTime}
</div>
</div>
{/* Actions */}
<div className="ml-4 flex gap-2">
{canEditTask && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
setEditingTask(task);
}}
title="Bearbeiten"
>
<Edit className="w-4 h-4" />
</Button>
)}
{canDeleteTask && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
if (confirm('Aufgabe wirklich löschen?')) {
deleteTaskMutation.mutate(task.id);
}
}}
title="Löschen"
className="text-red-500 hover:text-red-700"
>
<Trash2 className="w-4 h-4" />
</Button>
)}
<Button
variant="ghost"
size="sm"
@ -249,10 +316,12 @@ export default function TaskList() {
{hasSubtasks && (
<div className="space-y-2 mb-4">
{task.subtasks?.map((subtask) => {
const createdDate = new Date(subtask.createdAt).toLocaleDateString('de-DE', {
const createdDateTime = new Date(subtask.createdAt).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
return (
@ -272,7 +341,7 @@ export default function TaskList() {
<span className={subtask.status === 'COMPLETED' ? 'text-gray-500 line-through' : ''}>
{subtask.title}
<span className="text-xs text-gray-400 ml-2">
{subtask.createdBy} {createdDate}
{subtask.createdBy} {createdDateTime}
</span>
</span>
</div>
@ -426,6 +495,16 @@ export default function TaskList() {
onClose={() => setShowCreateModal(false)}
/>
)}
{/* Modal zum Bearbeiten einer Aufgabe */}
{editingTask && (
<EditTaskModal
task={editingTask}
onClose={() => setEditingTask(null)}
onSave={(data) => updateTaskMutation.mutate({ taskId: editingTask.id, data })}
isPending={updateTaskMutation.isPending}
/>
)}
</div>
);
}
@ -886,3 +965,84 @@ function CreateTaskModal({
</Modal>
);
}
// Modal zum Bearbeiten einer Aufgabe
function EditTaskModal({
task,
onClose,
onSave,
isPending,
}: {
task: ContractTask;
onClose: () => void;
onSave: (data: { title?: string; description?: string; visibleInPortal?: boolean }) => void;
isPending: boolean;
}) {
const [title, setTitle] = useState(task.title);
const [description, setDescription] = useState(task.description || '');
const [visibleInPortal, setVisibleInPortal] = useState(task.visibleInPortal || false);
const handleSubmit = () => {
if (!title.trim()) return;
onSave({
title: title.trim(),
description: description.trim() || undefined,
visibleInPortal,
});
};
return (
<Modal isOpen={true} onClose={onClose} title="Aufgabe bearbeiten">
<div className="space-y-4">
{/* Titel */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Titel *
</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Aufgabentitel"
/>
</div>
{/* Beschreibung */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Beschreibung
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Detaillierte Beschreibung (optional)"
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Im Kundenportal sichtbar */}
<div>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={visibleInPortal}
onChange={(e) => setVisibleInPortal(e.target.checked)}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm text-gray-700">Im Kundenportal sichtbar</span>
</label>
</div>
{/* Buttons */}
<div className="flex justify-end gap-2 pt-4">
<Button variant="secondary" onClick={onClose}>
Abbrechen
</Button>
<Button onClick={handleSubmit} disabled={!title.trim() || isPending}>
{isPending ? 'Wird gespeichert...' : 'Speichern'}
</Button>
</div>
</div>
</Modal>
);
}