added date at support ticket, new order support tickets, delete edit support ticktes only from enploye and admins
This commit is contained in:
parent
06489299d5
commit
ee4f1aacdd
|
|
@ -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"}
|
||||
|
|
@ -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
|
|
@ -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
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue