@hed-hog/operations 0.0.325 → 0.0.327

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.
Files changed (32) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +5 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/operations.service.d.ts +9 -1
  4. package/dist/operations.service.d.ts.map +1 -1
  5. package/dist/operations.service.js +140 -26
  6. package/dist/operations.service.js.map +1 -1
  7. package/hedhog/data/integration_event_catalog.yaml +313 -0
  8. package/hedhog/data/setting_group.yaml +21 -0
  9. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +410 -23
  10. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +504 -375
  11. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +258 -230
  12. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +225 -162
  13. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +484 -230
  14. package/hedhog/frontend/app/_lib/api.ts.ejs +13 -4
  15. package/hedhog/frontend/app/_lib/hooks/use-mention-items.ts.ejs +28 -0
  16. package/hedhog/frontend/app/_lib/types.ts.ejs +30 -29
  17. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +347 -236
  18. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +31 -7
  19. package/hedhog/frontend/messages/en.json +38 -55
  20. package/hedhog/frontend/messages/en.json.ejs +21 -4
  21. package/hedhog/frontend/messages/pt.json +36 -55
  22. package/hedhog/frontend/messages/pt.json.ejs +14 -3
  23. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts +1 -0
  24. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.d.ts.map +1 -1
  25. package/hedhog/frontend/src/app/(app)/(libraries)/operations/_lib/types.ts +1 -0
  26. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts +1 -0
  27. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.d.ts.map +1 -1
  28. package/hedhog/frontend/src/app/(app)/(libraries)/operations/operations/_lib/types.ts +1 -0
  29. package/hedhog/table/operations_collaborator.yaml +5 -0
  30. package/hedhog/table/operations_collaborator_compensation_history.yaml +4 -0
  31. package/package.json +5 -5
  32. package/src/operations.service.ts +202 -26
@@ -19,6 +19,7 @@ import {
19
19
  import { Input } from '@/components/ui/input';
20
20
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
21
21
  import { Label } from '@/components/ui/label';
22
+ import { Progress } from '@/components/ui/progress';
22
23
  import {
23
24
  Select,
24
25
  SelectContent,
@@ -26,6 +27,12 @@ import {
26
27
  SelectTrigger,
27
28
  SelectValue,
28
29
  } from '@/components/ui/select';
30
+ import {
31
+ Sheet,
32
+ SheetContent,
33
+ SheetHeader,
34
+ SheetTitle,
35
+ } from '@/components/ui/sheet';
29
36
  import {
30
37
  Table,
31
38
  TableBody,
@@ -34,7 +41,7 @@ import {
34
41
  TableHeader,
35
42
  TableRow,
36
43
  } from '@/components/ui/table';
37
- import { Progress } from '@/components/ui/progress';
44
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
38
45
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
39
46
  import {
40
47
  closestCenter,
@@ -66,7 +73,6 @@ import {
66
73
  Pencil,
67
74
  PlayCircle,
68
75
  Plus,
69
- Rows3,
70
76
  Timer,
71
77
  Trash2,
72
78
  } from 'lucide-react';
@@ -76,10 +82,13 @@ import { useCallback, useMemo, useState } from 'react';
76
82
  import { OperationsHeader } from '../_components/operations-header';
77
83
  import { StatusBadge } from '../_components/status-badge';
78
84
  import {
85
+ TaskCommentsSection,
79
86
  TaskDetailSheet,
80
87
  type TaskDetailSheetData,
81
88
  } from '../_components/task-detail-sheet';
89
+ import { TaskFileAttachments } from '../_components/task-file-attachments';
82
90
  import { fetchOperations, mutateOperations } from '../_lib/api';
91
+ import { useMentionItems } from '../_lib/hooks/use-mention-items';
83
92
  import type {
84
93
  OperationsProjectOption,
85
94
  OperationsTaskOption,
@@ -191,6 +200,26 @@ function isPastDue(dueDate?: string | null) {
191
200
  return new Date(dueDate) < new Date();
192
201
  }
193
202
 
203
+ function getColumnClassName(columnId: BoardColumnId) {
204
+ const styles: Record<BoardColumnId, string> = {
205
+ todo: 'from-slate-500/20 via-slate-500/5 to-transparent',
206
+ doing: 'from-sky-500/20 via-cyan-500/5 to-transparent',
207
+ review: 'from-amber-500/20 via-yellow-500/5 to-transparent',
208
+ done: 'from-emerald-500/20 via-green-500/5 to-transparent',
209
+ };
210
+ return styles[columnId];
211
+ }
212
+
213
+ function getColumnDotClassName(columnId: BoardColumnId) {
214
+ const styles: Record<BoardColumnId, string> = {
215
+ todo: 'bg-slate-500',
216
+ doing: 'bg-sky-500',
217
+ review: 'bg-amber-500',
218
+ done: 'bg-emerald-500',
219
+ };
220
+ return styles[columnId];
221
+ }
222
+
194
223
  function getInitials(value?: string | null) {
195
224
  const parts = String(value ?? '')
196
225
  .trim()
@@ -276,6 +305,7 @@ export default function OperationsMyTasksPage() {
276
305
  const detailT = useTranslations('operations.ProjectDetailsPage');
277
306
 
278
307
  const { request, currentLocaleCode, getSettingValue } = useApp();
308
+ const mentionItems = useMentionItems(request);
279
309
  const [search, setSearch] = useState('');
280
310
  const [statusFilter, setStatusFilter] = useState('all');
281
311
  const [page, setPage] = useState(1);
@@ -303,6 +333,7 @@ export default function OperationsMyTasksPage() {
303
333
  const [taskFormOpen, setTaskFormOpen] = useState(false);
304
334
  const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
305
335
  const [taskFormLoading, setTaskFormLoading] = useState(false);
336
+ const [archivingTaskId, setArchivingTaskId] = useState<number | null>(null);
306
337
  const [taskFormData, setTaskFormData] =
307
338
  useState<TaskFormState>(EMPTY_TASK_FORM);
308
339
  const [inlineCreateColumn, setInlineCreateColumn] =
@@ -486,12 +517,22 @@ export default function OperationsMyTasksPage() {
486
517
 
487
518
  const handleArchiveTask = useCallback(
488
519
  async (taskId: number) => {
489
- await mutateOperations(request, `/operations/tasks/${taskId}`, 'PATCH', {
490
- archived: true,
491
- });
492
- setSelectedTask(null);
493
- setBoardOverride(null);
494
- await refetchAll();
520
+ setArchivingTaskId(taskId);
521
+ try {
522
+ await mutateOperations(
523
+ request,
524
+ `/operations/tasks/${taskId}`,
525
+ 'PATCH',
526
+ { archived: true }
527
+ );
528
+ setSelectedTask(null);
529
+ setBoardOverride(null);
530
+ await refetchAll();
531
+ } catch {
532
+ // ignore
533
+ } finally {
534
+ setArchivingTaskId(null);
535
+ }
495
536
  },
496
537
  [request, refetchAll]
497
538
  );
@@ -746,19 +787,31 @@ export default function OperationsMyTasksPage() {
746
787
  {(isOver) => (
747
788
  <div
748
789
  className={[
749
- 'rounded-xl border bg-muted/20 p-3 transition-colors',
790
+ 'flex min-h-[32rem] flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
791
+ getColumnClassName(column.id),
750
792
  isOver
751
- ? 'border-primary bg-primary/5'
793
+ ? 'border-primary shadow-lg ring-2 ring-primary/15'
752
794
  : 'border-border',
753
795
  ].join(' ')}
754
796
  >
755
- <div className="mb-3 flex items-center justify-between">
756
- <div className="flex items-center gap-2 text-sm font-semibold">
757
- <Rows3 className="size-4 text-muted-foreground" />
758
- {column.label}
797
+ <div className="mb-3 flex items-center justify-between gap-3 rounded-2xl border bg-background/85 p-3 shadow-xs">
798
+ <div className="min-w-0">
799
+ <div className="flex items-center gap-2 text-sm font-semibold">
800
+ <span
801
+ className={[
802
+ 'size-2.5 rounded-full',
803
+ getColumnDotClassName(column.id),
804
+ ].join(' ')}
805
+ />
806
+ {column.label}
807
+ </div>
808
+ <div className="mt-1 text-xs text-muted-foreground">
809
+ {boardColumns[column.id].length}{' '}
810
+ {detailT('kanban.items')}
811
+ </div>
759
812
  </div>
760
813
  <div className="flex items-center gap-1">
761
- <span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">
814
+ <span className="rounded-full border bg-background px-2 py-0.5 text-xs font-medium text-muted-foreground">
762
815
  {boardColumns[column.id].length}
763
816
  </span>
764
817
  <button
@@ -795,7 +848,7 @@ export default function OperationsMyTasksPage() {
795
848
  role="button"
796
849
  tabIndex={0}
797
850
  onClick={() =>
798
- !isDragging && setSelectedTask(task)
851
+ !isDragging && openEditTaskForm(task)
799
852
  }
800
853
  onKeyDown={(event) => {
801
854
  if (
@@ -803,7 +856,7 @@ export default function OperationsMyTasksPage() {
803
856
  event.key === ' '
804
857
  ) {
805
858
  event.preventDefault();
806
- setSelectedTask(task);
859
+ openEditTaskForm(task);
807
860
  }
808
861
  }}
809
862
  className={[
@@ -833,31 +886,14 @@ export default function OperationsMyTasksPage() {
833
886
  </p>
834
887
  ) : null}
835
888
  </div>
836
- <div className="flex items-start gap-2">
837
- <span
838
- className={[
839
- 'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
840
- getPriorityClassName(task.priority),
841
- ].join(' ')}
842
- >
843
- {getTaskPriorityLabel(task.priority)}
844
- </span>
845
- <Button
846
- type="button"
847
- variant="ghost"
848
- size="icon"
849
- className="size-7 shrink-0 rounded-full opacity-0 transition group-hover:opacity-100"
850
- onPointerDown={(event) =>
851
- event.stopPropagation()
852
- }
853
- onClick={(event) => {
854
- event.stopPropagation();
855
- openEditTaskForm(task);
856
- }}
857
- >
858
- <Pencil className="size-3.5" />
859
- </Button>
860
- </div>
889
+ <span
890
+ className={[
891
+ 'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
892
+ getPriorityClassName(task.priority),
893
+ ].join(' ')}
894
+ >
895
+ {getTaskPriorityLabel(task.priority)}
896
+ </span>
861
897
  </div>
862
898
 
863
899
  {taskTags.length > 0 ? (
@@ -1199,6 +1235,7 @@ export default function OperationsMyTasksPage() {
1199
1235
  <TaskDetailSheet
1200
1236
  task={selectedTask}
1201
1237
  open={selectedTask !== null}
1238
+ defaultTab="comments"
1202
1239
  onOpenChange={(open) => {
1203
1240
  if (!open) setSelectedTask(null);
1204
1241
  }}
@@ -1291,7 +1328,7 @@ export default function OperationsMyTasksPage() {
1291
1328
  </DialogContent>
1292
1329
  </Dialog>
1293
1330
 
1294
- <Dialog
1331
+ <Sheet
1295
1332
  open={taskFormOpen}
1296
1333
  onOpenChange={(open) => {
1297
1334
  if (!taskFormLoading) {
@@ -1303,209 +1340,283 @@ export default function OperationsMyTasksPage() {
1303
1340
  }
1304
1341
  }}
1305
1342
  >
1306
- <DialogContent className="sm:max-w-2xl">
1307
- <DialogHeader>
1308
- <DialogTitle>
1343
+ <SheetContent className="flex w-full flex-col overflow-hidden sm:max-w-xl">
1344
+ <SheetHeader className="shrink-0">
1345
+ <SheetTitle>
1309
1346
  {editingTaskId
1310
1347
  ? detailT('taskForm.titleEdit')
1311
1348
  : detailT('taskForm.titleNew')}
1312
- </DialogTitle>
1313
- </DialogHeader>
1314
- <div className="space-y-4">
1315
- <div className="space-y-2">
1316
- <Label htmlFor="task-project">{commonT('labels.project')}</Label>
1317
- <Select
1318
- value={taskFormData.projectId}
1319
- onValueChange={(value) =>
1320
- setTaskFormData((prev) => ({
1321
- ...prev,
1322
- projectId: value,
1323
- }))
1324
- }
1325
- >
1326
- <SelectTrigger className="w-full" id="task-project">
1327
- <SelectValue
1328
- placeholder={t('dialogs.createProjectPlaceholder')}
1329
- />
1330
- </SelectTrigger>
1331
- <SelectContent>
1332
- {projectOptions.map((project) => (
1333
- <SelectItem key={project.id} value={String(project.id)}>
1334
- {project.label}
1335
- </SelectItem>
1336
- ))}
1337
- </SelectContent>
1338
- </Select>
1339
- </div>
1340
-
1341
- <div className="space-y-2">
1342
- <Label htmlFor="task-name">
1343
- {detailT('taskForm.nameLabel')} *
1344
- </Label>
1345
- <Input
1346
- id="task-name"
1347
- value={taskFormData.name}
1348
- onChange={(event) =>
1349
- setTaskFormData((prev) => ({
1350
- ...prev,
1351
- name: event.target.value,
1352
- }))
1353
- }
1354
- placeholder={detailT('taskForm.namePlaceholder')}
1355
- />
1356
- </div>
1357
-
1358
- <div className="space-y-2">
1359
- <Label htmlFor="task-description">
1360
- {detailT('taskForm.descriptionLabel')}
1361
- </Label>
1362
- <RichTextEditor
1363
- value={taskFormData.description}
1364
- onChange={(val) =>
1365
- setTaskFormData((prev) => ({
1366
- ...prev,
1367
- description: val,
1368
- }))
1369
- }
1370
- />
1371
- </div>
1349
+ </SheetTitle>
1350
+ </SheetHeader>
1351
+
1352
+ <Tabs defaultValue="info" className="flex min-h-0 flex-1 flex-col">
1353
+ <TabsList className="mx-4 grid w-[calc(100%-2rem)] shrink-0 grid-cols-2">
1354
+ <TabsTrigger value="info">
1355
+ {detailT('taskForm.tabInfo')}
1356
+ </TabsTrigger>
1357
+ {editingTaskId ? (
1358
+ <TabsTrigger value="comments">
1359
+ {detailT('taskForm.tabComments')}
1360
+ </TabsTrigger>
1361
+ ) : null}
1362
+ </TabsList>
1372
1363
 
1373
- <div className="grid grid-cols-2 gap-3">
1374
- <div className="space-y-2">
1375
- <Label>{detailT('taskForm.priorityLabel')}</Label>
1376
- <Select
1377
- value={taskFormData.priority}
1378
- onValueChange={(value) =>
1379
- setTaskFormData((prev) => ({
1380
- ...prev,
1381
- priority: value as TaskFormState['priority'],
1382
- }))
1383
- }
1384
- >
1385
- <SelectTrigger className="w-full">
1386
- <SelectValue />
1387
- </SelectTrigger>
1388
- <SelectContent>
1389
- <SelectItem value="low">
1390
- {getTaskPriorityLabel('low')}
1391
- </SelectItem>
1392
- <SelectItem value="medium">
1393
- {getTaskPriorityLabel('medium')}
1394
- </SelectItem>
1395
- <SelectItem value="high">
1396
- {getTaskPriorityLabel('high')}
1397
- </SelectItem>
1398
- </SelectContent>
1399
- </Select>
1400
- </div>
1364
+ <TabsContent
1365
+ value="info"
1366
+ className="flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
1367
+ >
1368
+ <div className="flex-1 space-y-4 overflow-y-auto px-4 py-2">
1369
+ <div className="space-y-2">
1370
+ <Label htmlFor="task-project">
1371
+ {commonT('labels.project')}
1372
+ </Label>
1373
+ <Select
1374
+ value={taskFormData.projectId}
1375
+ onValueChange={(value) =>
1376
+ setTaskFormData((prev) => ({
1377
+ ...prev,
1378
+ projectId: value,
1379
+ }))
1380
+ }
1381
+ >
1382
+ <SelectTrigger className="w-full" id="task-project">
1383
+ <SelectValue
1384
+ placeholder={t('dialogs.createProjectPlaceholder')}
1385
+ />
1386
+ </SelectTrigger>
1387
+ <SelectContent>
1388
+ {projectOptions.map((project) => (
1389
+ <SelectItem key={project.id} value={String(project.id)}>
1390
+ {project.label}
1391
+ </SelectItem>
1392
+ ))}
1393
+ </SelectContent>
1394
+ </Select>
1395
+ </div>
1396
+
1397
+ <div className="space-y-2">
1398
+ <Label htmlFor="task-name">
1399
+ {detailT('taskForm.nameLabel')} *
1400
+ </Label>
1401
+ <Input
1402
+ id="task-name"
1403
+ value={taskFormData.name}
1404
+ onChange={(event) =>
1405
+ setTaskFormData((prev) => ({
1406
+ ...prev,
1407
+ name: event.target.value,
1408
+ }))
1409
+ }
1410
+ placeholder={detailT('taskForm.namePlaceholder')}
1411
+ />
1412
+ </div>
1413
+
1414
+ <div className="space-y-2">
1415
+ <Label htmlFor="task-description">
1416
+ {detailT('taskForm.descriptionLabel')}
1417
+ </Label>
1418
+ <RichTextEditor
1419
+ value={taskFormData.description}
1420
+ onChange={(val) =>
1421
+ setTaskFormData((prev) => ({
1422
+ ...prev,
1423
+ description: val,
1424
+ }))
1425
+ }
1426
+ mentions={mentionItems}
1427
+ />
1428
+ </div>
1429
+
1430
+ <div className="grid grid-cols-2 gap-3">
1431
+ <div className="space-y-2">
1432
+ <Label>{detailT('taskForm.priorityLabel')}</Label>
1433
+ <Select
1434
+ value={taskFormData.priority}
1435
+ onValueChange={(value) =>
1436
+ setTaskFormData((prev) => ({
1437
+ ...prev,
1438
+ priority: value as TaskFormState['priority'],
1439
+ }))
1440
+ }
1441
+ >
1442
+ <SelectTrigger className="w-full">
1443
+ <SelectValue />
1444
+ </SelectTrigger>
1445
+ <SelectContent>
1446
+ <SelectItem value="low">
1447
+ {getTaskPriorityLabel('low')}
1448
+ </SelectItem>
1449
+ <SelectItem value="medium">
1450
+ {getTaskPriorityLabel('medium')}
1451
+ </SelectItem>
1452
+ <SelectItem value="high">
1453
+ {getTaskPriorityLabel('high')}
1454
+ </SelectItem>
1455
+ </SelectContent>
1456
+ </Select>
1457
+ </div>
1401
1458
 
1402
- <div className="space-y-2">
1403
- <Label>{detailT('taskForm.columnLabel')}</Label>
1404
- <Select
1405
- value={taskFormData.status}
1406
- onValueChange={(value) =>
1407
- setTaskFormData((prev) => ({
1408
- ...prev,
1409
- status: value as BoardColumnId,
1410
- }))
1411
- }
1412
- >
1413
- <SelectTrigger className="w-full">
1414
- <SelectValue />
1415
- </SelectTrigger>
1416
- <SelectContent>
1417
- {KANBAN_COLUMNS.map((column) => (
1418
- <SelectItem key={column.id} value={column.id}>
1419
- {column.label}
1420
- </SelectItem>
1421
- ))}
1422
- </SelectContent>
1423
- </Select>
1424
- </div>
1425
- </div>
1459
+ <div className="space-y-2">
1460
+ <Label>{detailT('taskForm.columnLabel')}</Label>
1461
+ <Select
1462
+ value={taskFormData.status}
1463
+ onValueChange={(value) =>
1464
+ setTaskFormData((prev) => ({
1465
+ ...prev,
1466
+ status: value as BoardColumnId,
1467
+ }))
1468
+ }
1469
+ >
1470
+ <SelectTrigger className="w-full">
1471
+ <SelectValue />
1472
+ </SelectTrigger>
1473
+ <SelectContent>
1474
+ {KANBAN_COLUMNS.map((column) => (
1475
+ <SelectItem key={column.id} value={column.id}>
1476
+ {column.label}
1477
+ </SelectItem>
1478
+ ))}
1479
+ </SelectContent>
1480
+ </Select>
1481
+ </div>
1482
+ </div>
1483
+
1484
+ <div className="grid grid-cols-2 gap-3">
1485
+ <div className="space-y-2">
1486
+ <Label htmlFor="task-due-date">
1487
+ {detailT('taskForm.deadlineLabel')}
1488
+ </Label>
1489
+ <Input
1490
+ id="task-due-date"
1491
+ type="date"
1492
+ value={taskFormData.dueDate}
1493
+ onChange={(event) =>
1494
+ setTaskFormData((prev) => ({
1495
+ ...prev,
1496
+ dueDate: event.target.value,
1497
+ }))
1498
+ }
1499
+ />
1500
+ </div>
1426
1501
 
1427
- <div className="grid grid-cols-2 gap-3">
1428
- <div className="space-y-2">
1429
- <Label htmlFor="task-due-date">
1430
- {detailT('taskForm.deadlineLabel')}
1431
- </Label>
1432
- <Input
1433
- id="task-due-date"
1434
- type="date"
1435
- value={taskFormData.dueDate}
1436
- onChange={(event) =>
1437
- setTaskFormData((prev) => ({
1438
- ...prev,
1439
- dueDate: event.target.value,
1440
- }))
1441
- }
1442
- />
1502
+ <div className="space-y-2">
1503
+ <Label htmlFor="task-estimate">
1504
+ {detailT('taskForm.estimateLabel')}
1505
+ </Label>
1506
+ <Input
1507
+ id="task-estimate"
1508
+ type="number"
1509
+ min="0"
1510
+ step="0.5"
1511
+ placeholder="0"
1512
+ value={taskFormData.estimateHours}
1513
+ onChange={(event) =>
1514
+ setTaskFormData((prev) => ({
1515
+ ...prev,
1516
+ estimateHours: event.target.value,
1517
+ }))
1518
+ }
1519
+ />
1520
+ </div>
1521
+ </div>
1522
+
1523
+ <div className="space-y-2">
1524
+ <Label htmlFor="task-tags">
1525
+ {detailT('taskForm.tagsLabel')}
1526
+ </Label>
1527
+ <Input
1528
+ id="task-tags"
1529
+ value={taskFormData.tags}
1530
+ onChange={(event) =>
1531
+ setTaskFormData((prev) => ({
1532
+ ...prev,
1533
+ tags: event.target.value,
1534
+ }))
1535
+ }
1536
+ placeholder={detailT('taskForm.tagsPlaceholder')}
1537
+ />
1538
+ </div>
1539
+
1540
+ {editingTaskId ? (
1541
+ <div className="space-y-1.5">
1542
+ <Label className="flex items-center gap-1.5">
1543
+ <Paperclip className="size-3.5" />
1544
+ {detailT('taskForm.attachmentsLabel')}
1545
+ </Label>
1546
+ <TaskFileAttachments taskId={editingTaskId} />
1547
+ </div>
1548
+ ) : null}
1443
1549
  </div>
1444
1550
 
1445
- <div className="space-y-2">
1446
- <Label htmlFor="task-estimate">
1447
- {detailT('taskForm.estimateLabel')}
1448
- </Label>
1449
- <Input
1450
- id="task-estimate"
1451
- type="number"
1452
- min="0"
1453
- step="0.5"
1454
- placeholder="0"
1455
- value={taskFormData.estimateHours}
1456
- onChange={(event) =>
1457
- setTaskFormData((prev) => ({
1458
- ...prev,
1459
- estimateHours: event.target.value,
1460
- }))
1461
- }
1462
- />
1551
+ <div className="mt-4 flex flex-wrap items-center justify-between gap-2 border-t px-4 pb-4 pt-4">
1552
+ <div className="flex gap-2">
1553
+ {editingTaskId ? (
1554
+ <Button
1555
+ type="button"
1556
+ variant="outline"
1557
+ disabled={
1558
+ taskFormLoading || archivingTaskId === editingTaskId
1559
+ }
1560
+ onClick={() => {
1561
+ if (!editingTaskId) return;
1562
+ const id = editingTaskId;
1563
+ setTaskFormOpen(false);
1564
+ setEditingTaskId(null);
1565
+ setTaskFormData(EMPTY_TASK_FORM);
1566
+ void handleArchiveTask(id);
1567
+ }}
1568
+ >
1569
+ {archivingTaskId === editingTaskId ? (
1570
+ <Loader2 className="mr-2 size-4 animate-spin" />
1571
+ ) : (
1572
+ <Archive className="mr-2 size-4" />
1573
+ )}
1574
+ {commonT('actions.archive')}
1575
+ </Button>
1576
+ ) : null}
1577
+ </div>
1578
+ <div className="flex gap-2">
1579
+ <Button
1580
+ variant="ghost"
1581
+ onClick={() => {
1582
+ setTaskFormOpen(false);
1583
+ setEditingTaskId(null);
1584
+ setTaskFormData(EMPTY_TASK_FORM);
1585
+ }}
1586
+ disabled={taskFormLoading}
1587
+ >
1588
+ {commonT('actions.cancel')}
1589
+ </Button>
1590
+ <Button
1591
+ onClick={() => void handleTaskFormSubmit()}
1592
+ disabled={
1593
+ taskFormLoading ||
1594
+ !taskFormData.projectId ||
1595
+ !taskFormData.name.trim()
1596
+ }
1597
+ >
1598
+ {taskFormLoading ? (
1599
+ <Loader2 className="size-4 animate-spin" />
1600
+ ) : null}
1601
+ {editingTaskId
1602
+ ? commonT('actions.save')
1603
+ : t('actions.create')}
1604
+ </Button>
1605
+ </div>
1463
1606
  </div>
1464
- </div>
1607
+ </TabsContent>
1465
1608
 
1466
- <div className="space-y-2">
1467
- <Label htmlFor="task-tags">{detailT('taskForm.tagsLabel')}</Label>
1468
- <Input
1469
- id="task-tags"
1470
- value={taskFormData.tags}
1471
- onChange={(event) =>
1472
- setTaskFormData((prev) => ({
1473
- ...prev,
1474
- tags: event.target.value,
1475
- }))
1476
- }
1477
- placeholder={detailT('taskForm.tagsPlaceholder')}
1478
- />
1479
- </div>
1480
- </div>
1481
- <DialogFooter className="gap-2 sm:justify-between">
1482
- <Button
1483
- variant="ghost"
1484
- onClick={() => {
1485
- setTaskFormOpen(false);
1486
- setEditingTaskId(null);
1487
- setTaskFormData(EMPTY_TASK_FORM);
1488
- }}
1489
- disabled={taskFormLoading}
1490
- >
1491
- {commonT('actions.cancel')}
1492
- </Button>
1493
- <Button
1494
- onClick={() => void handleTaskFormSubmit()}
1495
- disabled={
1496
- taskFormLoading ||
1497
- !taskFormData.projectId ||
1498
- !taskFormData.name.trim()
1499
- }
1500
- >
1501
- {taskFormLoading ? (
1502
- <Loader2 className="size-4 animate-spin" />
1503
- ) : null}
1504
- {editingTaskId ? commonT('actions.save') : t('actions.create')}
1505
- </Button>
1506
- </DialogFooter>
1507
- </DialogContent>
1508
- </Dialog>
1609
+ {editingTaskId ? (
1610
+ <TabsContent
1611
+ value="comments"
1612
+ className="min-h-0 flex-1 overflow-y-auto px-4 py-2 data-[state=inactive]:hidden"
1613
+ >
1614
+ <TaskCommentsSection taskId={editingTaskId} />
1615
+ </TabsContent>
1616
+ ) : null}
1617
+ </Tabs>
1618
+ </SheetContent>
1619
+ </Sheet>
1509
1620
  </Page>
1510
1621
  );
1511
1622
  }