@hed-hog/operations 0.0.325 → 0.0.326
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controllers/operations-collaborators.controller.d.ts +5 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/operations.service.d.ts +9 -1
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +140 -26
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/integration_event_catalog.yaml +313 -0
- package/hedhog/data/setting_group.yaml +21 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +410 -23
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +504 -375
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +258 -230
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +225 -162
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +484 -230
- package/hedhog/frontend/app/_lib/api.ts.ejs +13 -4
- package/hedhog/frontend/app/_lib/hooks/use-mention-items.ts.ejs +28 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +30 -29
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +347 -236
- package/hedhog/frontend/app/reports/projects/page.tsx.ejs +31 -7
- package/hedhog/frontend/messages/en.json +38 -55
- package/hedhog/frontend/messages/en.json.ejs +21 -4
- package/hedhog/frontend/messages/pt.json +36 -55
- package/hedhog/frontend/messages/pt.json.ejs +14 -3
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -1
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +1 -0
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -1
- package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +1 -0
- package/hedhog/table/operations_collaborator.yaml +5 -0
- package/hedhog/table/operations_collaborator_compensation_history.yaml +4 -0
- package/package.json +5 -5
- package/src/operations.service.ts +202 -26
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page } from '@/components/entity-list';
|
|
4
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
5
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
7
|
import {
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from '@/components/ui/dialog';
|
|
13
14
|
import { Input } from '@/components/ui/input';
|
|
14
15
|
import { Label } from '@/components/ui/label';
|
|
16
|
+
import { Progress } from '@/components/ui/progress';
|
|
15
17
|
import {
|
|
16
18
|
Select,
|
|
17
19
|
SelectContent,
|
|
@@ -19,6 +21,12 @@ import {
|
|
|
19
21
|
SelectTrigger,
|
|
20
22
|
SelectValue,
|
|
21
23
|
} from '@/components/ui/select';
|
|
24
|
+
import {
|
|
25
|
+
Sheet,
|
|
26
|
+
SheetContent,
|
|
27
|
+
SheetHeader,
|
|
28
|
+
SheetTitle,
|
|
29
|
+
} from '@/components/ui/sheet';
|
|
22
30
|
import {
|
|
23
31
|
Table,
|
|
24
32
|
TableBody,
|
|
@@ -27,8 +35,7 @@ import {
|
|
|
27
35
|
TableHeader,
|
|
28
36
|
TableRow,
|
|
29
37
|
} from '@/components/ui/table';
|
|
30
|
-
import {
|
|
31
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
38
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
32
39
|
import {
|
|
33
40
|
closestCenter,
|
|
34
41
|
DndContext,
|
|
@@ -48,17 +55,18 @@ import {
|
|
|
48
55
|
Archive,
|
|
49
56
|
ArchiveRestore,
|
|
50
57
|
FolderKanban,
|
|
58
|
+
Loader2,
|
|
51
59
|
MessageSquare,
|
|
52
60
|
Paperclip,
|
|
53
61
|
Pencil,
|
|
54
62
|
Plus,
|
|
55
|
-
Rows3,
|
|
56
63
|
Timer,
|
|
57
64
|
Trash2,
|
|
58
65
|
} from 'lucide-react';
|
|
59
66
|
import { useTranslations } from 'next-intl';
|
|
60
67
|
import { useCallback, useMemo, useState } from 'react';
|
|
61
68
|
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
69
|
+
import { useMentionItems } from '../_lib/hooks/use-mention-items';
|
|
62
70
|
import type {
|
|
63
71
|
OperationsMyProjectSummary,
|
|
64
72
|
OperationsTaskOption,
|
|
@@ -72,7 +80,12 @@ import {
|
|
|
72
80
|
import { OperationsHeader } from './operations-header';
|
|
73
81
|
import { SectionCard } from './section-card';
|
|
74
82
|
import { StatusBadge } from './status-badge';
|
|
75
|
-
import {
|
|
83
|
+
import {
|
|
84
|
+
TaskCommentsSection,
|
|
85
|
+
TaskDetailSheet,
|
|
86
|
+
type TaskDetailSheetData,
|
|
87
|
+
} from './task-detail-sheet';
|
|
88
|
+
import { TaskFileAttachments } from './task-file-attachments';
|
|
76
89
|
|
|
77
90
|
type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
|
|
78
91
|
|
|
@@ -150,20 +163,20 @@ function parseColumnId(
|
|
|
150
163
|
return KANBAN_COLUMNS.some((c) => c.id === id) ? (id as BoardColumnId) : null;
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
function apiTaskToBoardTask(
|
|
154
|
-
row: OperationsMyProjectSummary['tasks'][number]
|
|
155
|
-
): BoardTask {
|
|
156
|
-
const status = KANBAN_COLUMNS.some((c) => c.id === row.status)
|
|
157
|
-
? (row.status as BoardColumnId)
|
|
158
|
-
: 'todo';
|
|
159
|
-
const taskCounts = row as typeof row & {
|
|
160
|
-
commentCount?: number | null;
|
|
161
|
-
fileCount?: number | null;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
id: row.id,
|
|
166
|
-
name: row.name,
|
|
166
|
+
function apiTaskToBoardTask(
|
|
167
|
+
row: OperationsMyProjectSummary['tasks'][number]
|
|
168
|
+
): BoardTask {
|
|
169
|
+
const status = KANBAN_COLUMNS.some((c) => c.id === row.status)
|
|
170
|
+
? (row.status as BoardColumnId)
|
|
171
|
+
: 'todo';
|
|
172
|
+
const taskCounts = row as typeof row & {
|
|
173
|
+
commentCount?: number | null;
|
|
174
|
+
fileCount?: number | null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
id: row.id,
|
|
179
|
+
name: row.name,
|
|
167
180
|
description: row.description ?? null,
|
|
168
181
|
status,
|
|
169
182
|
priority: row.priority ?? 'medium',
|
|
@@ -171,13 +184,15 @@ function apiTaskToBoardTask(
|
|
|
171
184
|
estimateHours: row.estimateHours ?? null,
|
|
172
185
|
tags: row.tags ?? null,
|
|
173
186
|
assigneeCollaboratorId: row.assigneeCollaboratorId ?? null,
|
|
174
|
-
assigneeName: row.assigneeName ?? null,
|
|
175
|
-
assigneeUserPhotoId: row.assigneeUserPhotoId ?? null,
|
|
176
|
-
assigneePersonAvatarId: row.assigneePersonAvatarId ?? null,
|
|
177
|
-
commentCount:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
assigneeName: row.assigneeName ?? null,
|
|
188
|
+
assigneeUserPhotoId: row.assigneeUserPhotoId ?? null,
|
|
189
|
+
assigneePersonAvatarId: row.assigneePersonAvatarId ?? null,
|
|
190
|
+
commentCount:
|
|
191
|
+
((row as unknown as Record<string, unknown>).commentCount as number) ?? 0,
|
|
192
|
+
fileCount:
|
|
193
|
+
((row as unknown as Record<string, unknown>).fileCount as number) ?? 0,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
181
196
|
|
|
182
197
|
function splitTasksByColumn(tasks: BoardTask[]): BoardColumns {
|
|
183
198
|
return {
|
|
@@ -241,7 +256,8 @@ function getTaskPriorityLabel(value?: string | null) {
|
|
|
241
256
|
|
|
242
257
|
function getPriorityClassName(priority?: string | null) {
|
|
243
258
|
if (priority === 'high') return 'border-rose-300 bg-rose-100 text-rose-700';
|
|
244
|
-
if (priority === 'medium')
|
|
259
|
+
if (priority === 'medium')
|
|
260
|
+
return 'border-amber-300 bg-amber-100 text-amber-700';
|
|
245
261
|
return 'border-emerald-300 bg-emerald-100 text-emerald-700';
|
|
246
262
|
}
|
|
247
263
|
|
|
@@ -262,7 +278,10 @@ function isPastDue(dueDate?: string | null) {
|
|
|
262
278
|
|
|
263
279
|
function getTaskTags(task: BoardTask) {
|
|
264
280
|
if (!task.tags) return [];
|
|
265
|
-
return task.tags
|
|
281
|
+
return task.tags
|
|
282
|
+
.split(',')
|
|
283
|
+
.map((tag) => tag.trim())
|
|
284
|
+
.filter(Boolean);
|
|
266
285
|
}
|
|
267
286
|
|
|
268
287
|
function getTaskCommentCount(task: BoardTask) {
|
|
@@ -273,6 +292,26 @@ function getTaskAttachmentCount(task: BoardTask) {
|
|
|
273
292
|
return task.fileCount ?? 0;
|
|
274
293
|
}
|
|
275
294
|
|
|
295
|
+
function getColumnClassName(columnId: BoardColumnId) {
|
|
296
|
+
const styles: Record<BoardColumnId, string> = {
|
|
297
|
+
todo: 'from-slate-500/20 via-slate-500/5 to-transparent',
|
|
298
|
+
doing: 'from-sky-500/20 via-cyan-500/5 to-transparent',
|
|
299
|
+
review: 'from-amber-500/20 via-yellow-500/5 to-transparent',
|
|
300
|
+
done: 'from-emerald-500/20 via-green-500/5 to-transparent',
|
|
301
|
+
};
|
|
302
|
+
return styles[columnId];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function getColumnDotClassName(columnId: BoardColumnId) {
|
|
306
|
+
const styles: Record<BoardColumnId, string> = {
|
|
307
|
+
todo: 'bg-slate-500',
|
|
308
|
+
doing: 'bg-sky-500',
|
|
309
|
+
review: 'bg-amber-500',
|
|
310
|
+
done: 'bg-emerald-500',
|
|
311
|
+
};
|
|
312
|
+
return styles[columnId];
|
|
313
|
+
}
|
|
314
|
+
|
|
276
315
|
function DraggableTaskCard({
|
|
277
316
|
task,
|
|
278
317
|
children,
|
|
@@ -314,6 +353,8 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
314
353
|
const formT = useTranslations('operations.ProjectFormPage');
|
|
315
354
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
316
355
|
|
|
356
|
+
const mentionItems = useMentionItems(request);
|
|
357
|
+
|
|
317
358
|
const getProjectStatusLabel = (value?: string | null) => {
|
|
318
359
|
if (!value) return commonT('labels.notAvailable');
|
|
319
360
|
try {
|
|
@@ -519,14 +560,26 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
519
560
|
taskFormData,
|
|
520
561
|
]);
|
|
521
562
|
|
|
563
|
+
const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
|
|
564
|
+
|
|
522
565
|
const handleArchiveTask = useCallback(
|
|
523
566
|
async (taskId: number) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
567
|
+
setArchivingTaskId(taskId);
|
|
568
|
+
try {
|
|
569
|
+
await mutateOperations(
|
|
570
|
+
request,
|
|
571
|
+
`/operations/tasks/${taskId}`,
|
|
572
|
+
'PATCH',
|
|
573
|
+
{ archived: true }
|
|
574
|
+
);
|
|
575
|
+
setBoardState(null);
|
|
576
|
+
setSelectedTask(null);
|
|
577
|
+
await refetchAll();
|
|
578
|
+
} catch {
|
|
579
|
+
// ignore
|
|
580
|
+
} finally {
|
|
581
|
+
setArchivingTaskId(null);
|
|
582
|
+
}
|
|
530
583
|
},
|
|
531
584
|
[request, refetchAll]
|
|
532
585
|
);
|
|
@@ -663,193 +716,207 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
663
716
|
{(isOver) => (
|
|
664
717
|
<div
|
|
665
718
|
className={[
|
|
666
|
-
'rounded-
|
|
667
|
-
|
|
719
|
+
'flex min-h-128 flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
|
|
720
|
+
getColumnClassName(column.id),
|
|
721
|
+
isOver
|
|
722
|
+
? 'border-primary shadow-lg ring-2 ring-primary/15'
|
|
723
|
+
: 'border-border',
|
|
668
724
|
].join(' ')}
|
|
669
725
|
>
|
|
670
|
-
<div className="mb-3 flex items-center justify-between">
|
|
671
|
-
<div className="
|
|
672
|
-
<
|
|
673
|
-
|
|
726
|
+
<div className="mb-3 flex items-center justify-between gap-3 rounded-2xl border bg-background/85 p-3 shadow-xs">
|
|
727
|
+
<div className="min-w-0">
|
|
728
|
+
<div className="flex items-center gap-2 text-sm font-semibold">
|
|
729
|
+
<span
|
|
730
|
+
className={[
|
|
731
|
+
'size-2.5 rounded-full',
|
|
732
|
+
getColumnDotClassName(column.id),
|
|
733
|
+
].join(' ')}
|
|
734
|
+
/>
|
|
735
|
+
{column.label}
|
|
736
|
+
</div>
|
|
737
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
738
|
+
{taskColumns[column.id].length} {t('kanban.items')}
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div className="flex items-center gap-1">
|
|
742
|
+
<span className="rounded-full border bg-background px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
|
743
|
+
{taskColumns[column.id].length}
|
|
744
|
+
</span>
|
|
745
|
+
<button
|
|
746
|
+
type="button"
|
|
747
|
+
className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
748
|
+
onClick={() => openCreateTaskForm(column.id)}
|
|
749
|
+
>
|
|
750
|
+
<Plus className="size-3.5" />
|
|
751
|
+
</button>
|
|
674
752
|
</div>
|
|
675
|
-
<span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">
|
|
676
|
-
{taskColumns[column.id].length}
|
|
677
|
-
</span>
|
|
678
753
|
</div>
|
|
679
754
|
|
|
680
755
|
<div className="flex flex-1 flex-col gap-2">
|
|
681
756
|
<AnimatePresence initial={false}>
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
className={[
|
|
706
|
-
'group w-full cursor-pointer rounded-2xl border bg-card p-3 text-left shadow-xs transition',
|
|
707
|
-
isDragging
|
|
708
|
-
? 'opacity-0'
|
|
709
|
-
: 'hover:border-primary/40 hover:shadow-lg',
|
|
710
|
-
].join(' ')}
|
|
711
|
-
>
|
|
712
|
-
<div className="mb-3 flex items-start justify-between gap-2">
|
|
713
|
-
<div className="min-w-0 space-y-1">
|
|
714
|
-
<p className="line-clamp-2 text-sm font-semibold leading-snug">
|
|
715
|
-
{task.name}
|
|
716
|
-
</p>
|
|
717
|
-
{task.description ? (
|
|
718
|
-
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
719
|
-
{task.description.replace(/<[^>]*>/g, '')}
|
|
720
|
-
</p>
|
|
721
|
-
) : null}
|
|
722
|
-
</div>
|
|
723
|
-
<div className="flex items-start gap-2">
|
|
724
|
-
<span
|
|
725
|
-
className={[
|
|
726
|
-
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
727
|
-
getPriorityClassName(task.priority),
|
|
728
|
-
].join(' ')}
|
|
729
|
-
>
|
|
730
|
-
{getTaskPriorityLabel(task.priority)}
|
|
731
|
-
</span>
|
|
732
|
-
<Button
|
|
733
|
-
type="button"
|
|
734
|
-
variant="ghost"
|
|
735
|
-
size="icon"
|
|
736
|
-
className="size-7 shrink-0 rounded-full opacity-0 transition group-hover:opacity-100"
|
|
737
|
-
onPointerDown={(event) =>
|
|
738
|
-
event.stopPropagation()
|
|
739
|
-
}
|
|
740
|
-
onClick={(event) => {
|
|
741
|
-
event.stopPropagation();
|
|
757
|
+
{taskColumns[column.id].map((task) => {
|
|
758
|
+
const tags = getTaskTags(task);
|
|
759
|
+
const comments = getTaskCommentCount(task);
|
|
760
|
+
const attachments = getTaskAttachmentCount(task);
|
|
761
|
+
return (
|
|
762
|
+
<DraggableTaskCard key={task.id} task={task}>
|
|
763
|
+
{(isDragging) => (
|
|
764
|
+
<motion.div
|
|
765
|
+
initial={{ opacity: 0, scale: 0.96 }}
|
|
766
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
767
|
+
exit={{ opacity: 0, scale: 0.95, y: -4 }}
|
|
768
|
+
transition={{ duration: 0.18 }}
|
|
769
|
+
role="button"
|
|
770
|
+
tabIndex={0}
|
|
771
|
+
onClick={() =>
|
|
772
|
+
!isDragging && openEditTaskForm(task)
|
|
773
|
+
}
|
|
774
|
+
onKeyDown={(event) => {
|
|
775
|
+
if (
|
|
776
|
+
event.key === 'Enter' ||
|
|
777
|
+
event.key === ' '
|
|
778
|
+
) {
|
|
779
|
+
event.preventDefault();
|
|
742
780
|
openEditTaskForm(task);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
781
|
+
}
|
|
782
|
+
}}
|
|
783
|
+
className={[
|
|
784
|
+
'group w-full cursor-pointer rounded-2xl border bg-card p-3 text-left shadow-xs transition',
|
|
785
|
+
isDragging
|
|
786
|
+
? 'opacity-0'
|
|
787
|
+
: 'hover:border-primary/40 hover:shadow-lg',
|
|
788
|
+
].join(' ')}
|
|
789
|
+
>
|
|
790
|
+
<div className="mb-3 flex items-start justify-between gap-2">
|
|
791
|
+
<div className="min-w-0 space-y-1">
|
|
792
|
+
<p className="line-clamp-2 text-sm font-semibold leading-snug">
|
|
793
|
+
{task.name}
|
|
794
|
+
</p>
|
|
795
|
+
{task.description ? (
|
|
796
|
+
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
|
797
|
+
{task.description.replace(
|
|
798
|
+
/<[^>]*>/g,
|
|
799
|
+
''
|
|
800
|
+
)}
|
|
801
|
+
</p>
|
|
802
|
+
) : null}
|
|
803
|
+
</div>
|
|
753
804
|
<span
|
|
754
|
-
|
|
755
|
-
|
|
805
|
+
className={[
|
|
806
|
+
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
|
807
|
+
getPriorityClassName(task.priority),
|
|
808
|
+
].join(' ')}
|
|
756
809
|
>
|
|
757
|
-
{
|
|
758
|
-
</span>
|
|
759
|
-
))}
|
|
760
|
-
{tags.length > 4 ? (
|
|
761
|
-
<span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
762
|
-
+{tags.length - 4}
|
|
810
|
+
{getTaskPriorityLabel(task.priority)}
|
|
763
811
|
</span>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
{tags.length > 0 ? (
|
|
815
|
+
<div className="mb-3 flex flex-wrap gap-1">
|
|
816
|
+
{tags.slice(0, 4).map((tag) => (
|
|
817
|
+
<span
|
|
818
|
+
key={`${task.id}-${tag}`}
|
|
819
|
+
className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground"
|
|
820
|
+
>
|
|
821
|
+
{tag}
|
|
822
|
+
</span>
|
|
823
|
+
))}
|
|
824
|
+
{tags.length > 4 ? (
|
|
825
|
+
<span className="rounded-full border bg-muted/60 px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
|
|
826
|
+
+{tags.length - 4}
|
|
827
|
+
</span>
|
|
828
|
+
) : null}
|
|
829
|
+
</div>
|
|
764
830
|
) : null}
|
|
765
|
-
</div>
|
|
766
|
-
) : null}
|
|
767
831
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
809
|
-
<div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
|
|
810
|
-
{(() => {
|
|
811
|
-
const photoUrl = getUserPhotoUrl(
|
|
812
|
-
task.assigneeUserPhotoId
|
|
813
|
-
);
|
|
814
|
-
const avatarUrl =
|
|
815
|
-
task.assigneePersonAvatarId
|
|
816
|
-
? getPersonAvatarUrl(
|
|
817
|
-
task.assigneePersonAvatarId
|
|
818
|
-
)
|
|
819
|
-
: null;
|
|
820
|
-
const imgSrc = photoUrl ?? avatarUrl;
|
|
821
|
-
return imgSrc ? (
|
|
822
|
-
// eslint-disable-next-line @next/next/no-img-element
|
|
823
|
-
<img
|
|
824
|
-
src={imgSrc}
|
|
825
|
-
alt={task.assigneeName ?? ''}
|
|
826
|
-
className="size-full object-cover"
|
|
827
|
-
/>
|
|
828
|
-
) : (
|
|
829
|
-
getInitials(task.assigneeName)
|
|
830
|
-
);
|
|
831
|
-
})()}
|
|
832
|
+
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
833
|
+
<div
|
|
834
|
+
className={[
|
|
835
|
+
'rounded-xl border bg-muted/20 px-2 py-1.5',
|
|
836
|
+
isPastDue(task.dueDate) &&
|
|
837
|
+
task.status !== 'done'
|
|
838
|
+
? 'border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300'
|
|
839
|
+
: 'text-muted-foreground',
|
|
840
|
+
].join(' ')}
|
|
841
|
+
>
|
|
842
|
+
<span className="flex items-center gap-1">
|
|
843
|
+
<AlarmClock className="size-3.5" />
|
|
844
|
+
{formatDate(
|
|
845
|
+
task.dueDate,
|
|
846
|
+
getSettingValue,
|
|
847
|
+
currentLocaleCode
|
|
848
|
+
)}
|
|
849
|
+
</span>
|
|
850
|
+
</div>
|
|
851
|
+
<div className="rounded-xl border bg-muted/20 px-2 py-1.5 text-muted-foreground">
|
|
852
|
+
<span className="flex items-center gap-1">
|
|
853
|
+
<Timer className="size-3.5" />
|
|
854
|
+
{task.estimateHours != null
|
|
855
|
+
? `${task.estimateHours}h`
|
|
856
|
+
: '—'}
|
|
857
|
+
</span>
|
|
858
|
+
</div>
|
|
859
|
+
</div>
|
|
860
|
+
|
|
861
|
+
<div className="mt-3 space-y-1.5">
|
|
862
|
+
<div className="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
863
|
+
<span>Progresso</span>
|
|
864
|
+
<span>
|
|
865
|
+
{getTaskProgress(task.status)}%
|
|
866
|
+
</span>
|
|
867
|
+
</div>
|
|
868
|
+
<Progress
|
|
869
|
+
value={getTaskProgress(task.status)}
|
|
870
|
+
className="h-1.5"
|
|
871
|
+
/>
|
|
832
872
|
</div>
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
873
|
+
|
|
874
|
+
<div className="mt-3 flex items-center justify-between gap-3 border-t pt-3">
|
|
875
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
876
|
+
<div className="flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-[10px] font-semibold uppercase text-muted-foreground ring-1 ring-border">
|
|
877
|
+
{(() => {
|
|
878
|
+
const photoUrl = getUserPhotoUrl(
|
|
879
|
+
task.assigneeUserPhotoId
|
|
880
|
+
);
|
|
881
|
+
const avatarUrl =
|
|
882
|
+
task.assigneePersonAvatarId
|
|
883
|
+
? getPersonAvatarUrl(
|
|
884
|
+
task.assigneePersonAvatarId
|
|
885
|
+
)
|
|
886
|
+
: null;
|
|
887
|
+
const imgSrc = photoUrl ?? avatarUrl;
|
|
888
|
+
return imgSrc ? (
|
|
889
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
890
|
+
<img
|
|
891
|
+
src={imgSrc}
|
|
892
|
+
alt={task.assigneeName ?? ''}
|
|
893
|
+
className="size-full object-cover"
|
|
894
|
+
/>
|
|
895
|
+
) : (
|
|
896
|
+
getInitials(task.assigneeName)
|
|
897
|
+
);
|
|
898
|
+
})()}
|
|
899
|
+
</div>
|
|
900
|
+
<span className="truncate text-[11px] text-muted-foreground">
|
|
901
|
+
{task.assigneeName ?? '—'}
|
|
902
|
+
</span>
|
|
903
|
+
</div>
|
|
904
|
+
<div className="flex shrink-0 items-center gap-2 text-[11px] text-muted-foreground">
|
|
905
|
+
<span className="inline-flex items-center gap-1">
|
|
906
|
+
<MessageSquare className="size-3.5" />
|
|
907
|
+
{comments}
|
|
908
|
+
</span>
|
|
909
|
+
<span className="inline-flex items-center gap-1">
|
|
910
|
+
<Paperclip className="size-3.5" />
|
|
911
|
+
{attachments}
|
|
912
|
+
</span>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
</motion.div>
|
|
916
|
+
)}
|
|
917
|
+
</DraggableTaskCard>
|
|
918
|
+
);
|
|
919
|
+
})}
|
|
853
920
|
</AnimatePresence>
|
|
854
921
|
</div>
|
|
855
922
|
</div>
|
|
@@ -1009,6 +1076,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
1009
1076
|
<TaskDetailSheet
|
|
1010
1077
|
task={selectedTask}
|
|
1011
1078
|
open={selectedTask !== null}
|
|
1079
|
+
defaultTab="comments"
|
|
1012
1080
|
onOpenChange={(open) => {
|
|
1013
1081
|
if (!open) setSelectedTask(null);
|
|
1014
1082
|
}}
|
|
@@ -1072,7 +1140,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
1072
1140
|
}
|
|
1073
1141
|
/>
|
|
1074
1142
|
|
|
1075
|
-
<
|
|
1143
|
+
<Sheet
|
|
1076
1144
|
open={taskFormOpen}
|
|
1077
1145
|
onOpenChange={(open) => {
|
|
1078
1146
|
if (!open) {
|
|
@@ -1082,181 +1150,242 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
1082
1150
|
}
|
|
1083
1151
|
}}
|
|
1084
1152
|
>
|
|
1085
|
-
<
|
|
1086
|
-
<
|
|
1087
|
-
<
|
|
1153
|
+
<SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
|
|
1154
|
+
<SheetHeader className="shrink-0">
|
|
1155
|
+
<SheetTitle>
|
|
1088
1156
|
{editingTaskId ? t('taskForm.titleEdit') : t('taskForm.titleNew')}
|
|
1089
|
-
</
|
|
1090
|
-
</
|
|
1091
|
-
|
|
1092
|
-
<
|
|
1093
|
-
<
|
|
1094
|
-
<
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
{
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1157
|
+
</SheetTitle>
|
|
1158
|
+
</SheetHeader>
|
|
1159
|
+
|
|
1160
|
+
<Tabs defaultValue="info" className="flex min-h-0 flex-1 flex-col">
|
|
1161
|
+
<TabsList className="mx-4 grid w-[calc(100%-2rem)] shrink-0 grid-cols-2">
|
|
1162
|
+
<TabsTrigger value="info">{t('taskForm.tabInfo')}</TabsTrigger>
|
|
1163
|
+
{editingTaskId ? (
|
|
1164
|
+
<TabsTrigger value="comments">
|
|
1165
|
+
{t('taskForm.tabComments')}
|
|
1166
|
+
</TabsTrigger>
|
|
1167
|
+
) : null}
|
|
1168
|
+
</TabsList>
|
|
1169
|
+
|
|
1170
|
+
<TabsContent
|
|
1171
|
+
value="info"
|
|
1172
|
+
className="flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
|
|
1173
|
+
>
|
|
1174
|
+
<div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
|
|
1175
|
+
<div className="space-y-1.5">
|
|
1176
|
+
<Label htmlFor="task-name">{t('taskForm.nameLabel')} *</Label>
|
|
1177
|
+
<Input
|
|
1178
|
+
id="task-name"
|
|
1179
|
+
placeholder={t('taskForm.namePlaceholder')}
|
|
1180
|
+
value={taskFormData.name}
|
|
1181
|
+
onChange={(e) =>
|
|
1182
|
+
setTaskFormData((prev) => ({
|
|
1183
|
+
...prev,
|
|
1184
|
+
name: e.target.value,
|
|
1185
|
+
}))
|
|
1186
|
+
}
|
|
1187
|
+
/>
|
|
1188
|
+
</div>
|
|
1189
|
+
|
|
1190
|
+
<div className="space-y-1.5">
|
|
1191
|
+
<Label htmlFor="task-description">
|
|
1192
|
+
{t('taskForm.descriptionLabel')}
|
|
1193
|
+
</Label>
|
|
1194
|
+
<RichTextEditor
|
|
1195
|
+
value={taskFormData.description}
|
|
1196
|
+
onChange={(val) =>
|
|
1197
|
+
setTaskFormData((prev) => ({
|
|
1198
|
+
...prev,
|
|
1199
|
+
description: val,
|
|
1200
|
+
}))
|
|
1201
|
+
}
|
|
1202
|
+
mentions={mentionItems}
|
|
1203
|
+
/>
|
|
1204
|
+
</div>
|
|
1205
|
+
|
|
1206
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1207
|
+
<div className="space-y-1.5">
|
|
1208
|
+
<Label>{t('taskForm.priorityLabel')}</Label>
|
|
1209
|
+
<Select
|
|
1210
|
+
value={taskFormData.priority}
|
|
1211
|
+
onValueChange={(v) =>
|
|
1212
|
+
setTaskFormData((prev) => ({
|
|
1213
|
+
...prev,
|
|
1214
|
+
priority: v as TaskFormState['priority'],
|
|
1215
|
+
}))
|
|
1216
|
+
}
|
|
1217
|
+
>
|
|
1218
|
+
<SelectTrigger className="w-full">
|
|
1219
|
+
<SelectValue />
|
|
1220
|
+
</SelectTrigger>
|
|
1221
|
+
<SelectContent>
|
|
1222
|
+
<SelectItem value="low">
|
|
1223
|
+
{getTaskPriorityLabel('low')}
|
|
1224
|
+
</SelectItem>
|
|
1225
|
+
<SelectItem value="medium">
|
|
1226
|
+
{getTaskPriorityLabel('medium')}
|
|
1227
|
+
</SelectItem>
|
|
1228
|
+
<SelectItem value="high">
|
|
1229
|
+
{getTaskPriorityLabel('high')}
|
|
1230
|
+
</SelectItem>
|
|
1231
|
+
</SelectContent>
|
|
1232
|
+
</Select>
|
|
1233
|
+
</div>
|
|
1154
1234
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1235
|
+
<div className="space-y-1.5">
|
|
1236
|
+
<Label>{t('taskForm.columnLabel')}</Label>
|
|
1237
|
+
<Select
|
|
1238
|
+
value={taskFormData.status}
|
|
1239
|
+
onValueChange={(v) =>
|
|
1240
|
+
setTaskFormData((prev) => ({
|
|
1241
|
+
...prev,
|
|
1242
|
+
status: v as BoardColumnId,
|
|
1243
|
+
}))
|
|
1244
|
+
}
|
|
1245
|
+
>
|
|
1246
|
+
<SelectTrigger className="w-full">
|
|
1247
|
+
<SelectValue />
|
|
1248
|
+
</SelectTrigger>
|
|
1249
|
+
<SelectContent>
|
|
1250
|
+
{KANBAN_COLUMNS.map((col) => (
|
|
1251
|
+
<SelectItem key={col.id} value={col.id}>
|
|
1252
|
+
{col.label}
|
|
1253
|
+
</SelectItem>
|
|
1254
|
+
))}
|
|
1255
|
+
</SelectContent>
|
|
1256
|
+
</Select>
|
|
1257
|
+
</div>
|
|
1258
|
+
</div>
|
|
1259
|
+
|
|
1260
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1261
|
+
<div className="space-y-1.5">
|
|
1262
|
+
<Label htmlFor="task-due-date">
|
|
1263
|
+
{t('taskForm.deadlineLabel')}
|
|
1264
|
+
</Label>
|
|
1265
|
+
<Input
|
|
1266
|
+
id="task-due-date"
|
|
1267
|
+
type="date"
|
|
1268
|
+
value={taskFormData.dueDate}
|
|
1269
|
+
onChange={(e) =>
|
|
1270
|
+
setTaskFormData((prev) => ({
|
|
1271
|
+
...prev,
|
|
1272
|
+
dueDate: e.target.value,
|
|
1273
|
+
}))
|
|
1274
|
+
}
|
|
1275
|
+
/>
|
|
1276
|
+
</div>
|
|
1179
1277
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1278
|
+
<div className="space-y-1.5">
|
|
1279
|
+
<Label htmlFor="task-estimate">
|
|
1280
|
+
{t('taskForm.estimateLabel')}
|
|
1281
|
+
</Label>
|
|
1282
|
+
<Input
|
|
1283
|
+
id="task-estimate"
|
|
1284
|
+
type="number"
|
|
1285
|
+
min="0"
|
|
1286
|
+
step="0.5"
|
|
1287
|
+
placeholder="0"
|
|
1288
|
+
value={taskFormData.estimateHours}
|
|
1289
|
+
onChange={(e) =>
|
|
1290
|
+
setTaskFormData((prev) => ({
|
|
1291
|
+
...prev,
|
|
1292
|
+
estimateHours: e.target.value,
|
|
1293
|
+
}))
|
|
1294
|
+
}
|
|
1295
|
+
/>
|
|
1296
|
+
</div>
|
|
1297
|
+
</div>
|
|
1298
|
+
|
|
1299
|
+
<div className="space-y-1.5">
|
|
1300
|
+
<Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
|
|
1301
|
+
<Input
|
|
1302
|
+
id="task-tags"
|
|
1303
|
+
placeholder={t('taskForm.tagsPlaceholder')}
|
|
1304
|
+
value={taskFormData.tags}
|
|
1305
|
+
onChange={(e) =>
|
|
1306
|
+
setTaskFormData((prev) => ({
|
|
1307
|
+
...prev,
|
|
1308
|
+
tags: e.target.value,
|
|
1309
|
+
}))
|
|
1310
|
+
}
|
|
1311
|
+
/>
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1314
|
+
{editingTaskId ? (
|
|
1315
|
+
<div className="space-y-1.5">
|
|
1316
|
+
<Label className="flex items-center gap-1.5">
|
|
1317
|
+
<Paperclip className="size-3.5" />
|
|
1318
|
+
{t('taskForm.attachmentsLabel')}
|
|
1319
|
+
</Label>
|
|
1320
|
+
<TaskFileAttachments taskId={editingTaskId} />
|
|
1321
|
+
</div>
|
|
1322
|
+
) : null}
|
|
1196
1323
|
</div>
|
|
1197
1324
|
|
|
1198
|
-
<div className="
|
|
1199
|
-
<
|
|
1200
|
-
{
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1325
|
+
<div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
|
|
1326
|
+
<div className="flex gap-2">
|
|
1327
|
+
{editingTaskId ? (
|
|
1328
|
+
<Button
|
|
1329
|
+
type="button"
|
|
1330
|
+
variant="outline"
|
|
1331
|
+
disabled={
|
|
1332
|
+
taskFormLoading || archivingTaskId === editingTaskId
|
|
1333
|
+
}
|
|
1334
|
+
onClick={() => {
|
|
1335
|
+
if (!editingTaskId) return;
|
|
1336
|
+
const id = editingTaskId;
|
|
1337
|
+
setTaskFormOpen(false);
|
|
1338
|
+
setEditingTaskId(null);
|
|
1339
|
+
setTaskFormData(EMPTY_TASK_FORM);
|
|
1340
|
+
void handleArchiveTask(id);
|
|
1341
|
+
}}
|
|
1342
|
+
>
|
|
1343
|
+
{archivingTaskId === editingTaskId ? (
|
|
1344
|
+
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
1345
|
+
) : (
|
|
1346
|
+
<Archive className="mr-2 size-4" />
|
|
1347
|
+
)}
|
|
1348
|
+
{commonT('actions.archive')}
|
|
1349
|
+
</Button>
|
|
1350
|
+
) : null}
|
|
1351
|
+
</div>
|
|
1352
|
+
<div className="flex gap-2">
|
|
1353
|
+
<Button
|
|
1354
|
+
variant="outline"
|
|
1355
|
+
onClick={() => {
|
|
1356
|
+
setTaskFormOpen(false);
|
|
1357
|
+
setEditingTaskId(null);
|
|
1358
|
+
setTaskFormData(EMPTY_TASK_FORM);
|
|
1359
|
+
}}
|
|
1360
|
+
disabled={taskFormLoading}
|
|
1361
|
+
>
|
|
1362
|
+
{commonT('actions.cancel')}
|
|
1363
|
+
</Button>
|
|
1364
|
+
<Button
|
|
1365
|
+
onClick={() => void handleTaskFormSubmit()}
|
|
1366
|
+
disabled={taskFormLoading || !taskFormData.name.trim()}
|
|
1367
|
+
>
|
|
1368
|
+
{taskFormLoading
|
|
1369
|
+
? t('taskForm.saving')
|
|
1370
|
+
: editingTaskId
|
|
1371
|
+
? commonT('actions.save')
|
|
1372
|
+
: commonT('actions.create')}
|
|
1373
|
+
</Button>
|
|
1374
|
+
</div>
|
|
1216
1375
|
</div>
|
|
1217
|
-
</
|
|
1376
|
+
</TabsContent>
|
|
1218
1377
|
|
|
1219
|
-
|
|
1220
|
-
<
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
}
|
|
1231
|
-
/>
|
|
1232
|
-
</div>
|
|
1233
|
-
</div>
|
|
1234
|
-
|
|
1235
|
-
<DialogFooter className="mt-4">
|
|
1236
|
-
<Button
|
|
1237
|
-
variant="outline"
|
|
1238
|
-
onClick={() => {
|
|
1239
|
-
setTaskFormOpen(false);
|
|
1240
|
-
setEditingTaskId(null);
|
|
1241
|
-
setTaskFormData(EMPTY_TASK_FORM);
|
|
1242
|
-
}}
|
|
1243
|
-
disabled={taskFormLoading}
|
|
1244
|
-
>
|
|
1245
|
-
{commonT('actions.cancel')}
|
|
1246
|
-
</Button>
|
|
1247
|
-
<Button
|
|
1248
|
-
onClick={() => void handleTaskFormSubmit()}
|
|
1249
|
-
disabled={taskFormLoading || !taskFormData.name.trim()}
|
|
1250
|
-
>
|
|
1251
|
-
{taskFormLoading
|
|
1252
|
-
? t('taskForm.saving')
|
|
1253
|
-
: editingTaskId
|
|
1254
|
-
? commonT('actions.save')
|
|
1255
|
-
: commonT('actions.create')}
|
|
1256
|
-
</Button>
|
|
1257
|
-
</DialogFooter>
|
|
1258
|
-
</DialogContent>
|
|
1259
|
-
</Dialog>
|
|
1378
|
+
{editingTaskId ? (
|
|
1379
|
+
<TabsContent
|
|
1380
|
+
value="comments"
|
|
1381
|
+
className="min-h-0 flex-1 overflow-y-auto px-4 py-2 data-[state=inactive]:hidden"
|
|
1382
|
+
>
|
|
1383
|
+
<TaskCommentsSection taskId={editingTaskId} />
|
|
1384
|
+
</TabsContent>
|
|
1385
|
+
) : null}
|
|
1386
|
+
</Tabs>
|
|
1387
|
+
</SheetContent>
|
|
1388
|
+
</Sheet>
|
|
1260
1389
|
|
|
1261
1390
|
<Dialog
|
|
1262
1391
|
open={deletePromptTask !== null}
|