@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.
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
@@ -1,34 +1,36 @@
1
1
  'use client';
2
2
 
3
- import type { ReactNode } from 'react';
4
- import { useState } from 'react';
3
+ import { RichTextEditor } from '@/components/rich-text-editor';
4
+ import { Button } from '@/components/ui/button';
5
+ import { CommentContent } from '@/components/ui/comment-rich-editor';
5
6
  import {
6
7
  Sheet,
7
8
  SheetContent,
8
9
  SheetHeader,
9
10
  SheetTitle,
10
11
  } from '@/components/ui/sheet';
11
- import { Textarea } from '@/components/ui/textarea';
12
- import { Button } from '@/components/ui/button';
12
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
13
13
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
14
14
  import {
15
15
  AlarmClock,
16
16
  Calendar,
17
17
  MessageSquare,
18
18
  Pencil,
19
- Send,
20
19
  Tag,
21
20
  Trash2,
22
21
  User,
23
22
  X,
24
23
  } from 'lucide-react';
25
24
  import { useTranslations } from 'next-intl';
25
+ import type { ReactNode } from 'react';
26
+ import { useEffect, useRef, useState } from 'react';
26
27
  import {
27
28
  createTaskComment,
28
29
  deleteTaskComment,
29
30
  fetchTaskComments,
30
31
  updateTaskComment,
31
32
  } from '../_lib/api';
33
+ import { useMentionItems } from '../_lib/hooks/use-mention-items';
32
34
  import type { OperationsTaskComment } from '../_lib/types';
33
35
  import { formatDate, getStatusBadgeClass } from '../_lib/utils/format';
34
36
  import { StatusBadge } from './status-badge';
@@ -55,6 +57,7 @@ type Props = {
55
57
  onOpenChange: (open: boolean) => void;
56
58
  statusLabel?: (status: string) => string;
57
59
  footer?: ReactNode;
60
+ defaultTab?: 'info' | 'comments';
58
61
  };
59
62
 
60
63
  function getPriorityLabel(value?: string | null) {
@@ -127,12 +130,16 @@ function formatCommentDate(value?: string | null) {
127
130
  return date.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' });
128
131
  }
129
132
 
130
- type TaskCommentsSectionProps = {
133
+ export type TaskCommentsSectionProps = {
131
134
  taskId: number;
132
135
  };
133
136
 
134
- function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
135
- const { request, showToastHandler } = useApp();
137
+ export function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
138
+ const { request, showToastHandler, getSettingValue } = useApp();
139
+ const ct = useTranslations('operations.ProjectDetailsPage.commentsSection');
140
+ const editWindowMinutes = Number(
141
+ getSettingValue('operations.comment-edit-window') ?? 5
142
+ );
136
143
  const [newComment, setNewComment] = useState('');
137
144
  const [submitting, setSubmitting] = useState(false);
138
145
  const [editingId, setEditingId] = useState<number | null>(null);
@@ -143,6 +150,12 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
143
150
  OperationsTaskComment[] | null
144
151
  >(null);
145
152
 
153
+ // Force re-render precisely when each comment's edit window expires
154
+ const [, setTick] = useState(0);
155
+ const comments_ref = useRef<OperationsTaskComment[]>([]);
156
+
157
+ const mentionItems = useMentionItems(request);
158
+
146
159
  const { data: fetchedComments = [], refetch } = useQuery<
147
160
  OperationsTaskComment[]
148
161
  >({
@@ -153,20 +166,41 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
153
166
 
154
167
  const comments = localComments ?? fetchedComments;
155
168
 
169
+ // Keep ref in sync so the timeout effect always sees current comments
170
+ comments_ref.current = comments;
171
+
172
+ // Schedule one precise re-render for each comment that is still within the
173
+ // edit window, so the buttons disappear at the exact moment they expire.
174
+ useEffect(() => {
175
+ if (editWindowMinutes <= 0) return;
176
+ const windowMs = editWindowMinutes * 60_000;
177
+ const timers: ReturnType<typeof setTimeout>[] = [];
178
+ for (const c of comments_ref.current) {
179
+ const ageMs = Date.now() - new Date(c.createdAt).getTime();
180
+ const msUntilExpiry = windowMs - ageMs;
181
+ if (msUntilExpiry > 0) {
182
+ timers.push(setTimeout(() => setTick((t) => t + 1), msUntilExpiry));
183
+ }
184
+ }
185
+ return () => timers.forEach(clearTimeout);
186
+ // Re-schedule whenever the comment list or the window setting changes
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ }, [comments, editWindowMinutes]);
189
+
156
190
  const handleSubmit = async () => {
157
- const trimmed = newComment.trim();
158
- if (!trimmed) return;
191
+ const stripped = newComment.replace(/<[^>]*>/g, '').trim();
192
+ if (!stripped) return;
159
193
  setSubmitting(true);
160
194
  try {
161
195
  const created = (await createTaskComment(
162
196
  request,
163
197
  taskId,
164
- trimmed
198
+ newComment
165
199
  )) as OperationsTaskComment;
166
200
  setLocalComments([...(localComments ?? fetchedComments), created]);
167
201
  setNewComment('');
168
202
  } catch {
169
- showToastHandler?.('error', 'Erro ao adicionar comentário.');
203
+ showToastHandler?.('error', ct('errors.addComment'));
170
204
  } finally {
171
205
  setSubmitting(false);
172
206
  }
@@ -183,7 +217,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
183
217
  };
184
218
 
185
219
  const handleSaveEdit = async (comment: OperationsTaskComment) => {
186
- const trimmed = editContent.trim();
220
+ const trimmed = editContent.replace(/<[^>]*>/g, '').trim();
187
221
  if (!trimmed) return;
188
222
  setSavingEditId(comment.id);
189
223
  try {
@@ -191,7 +225,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
191
225
  request,
192
226
  taskId,
193
227
  comment.id,
194
- trimmed
228
+ editContent
195
229
  )) as OperationsTaskComment;
196
230
  if (updated) {
197
231
  setLocalComments(
@@ -203,7 +237,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
203
237
  setEditingId(null);
204
238
  setEditContent('');
205
239
  } catch {
206
- showToastHandler?.('error', 'Não foi possível editar este comentário.');
240
+ showToastHandler?.('error', ct('errors.editComment'));
207
241
  } finally {
208
242
  setSavingEditId(null);
209
243
  }
@@ -217,7 +251,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
217
251
  (localComments ?? fetchedComments).filter((c) => c.id !== comment.id)
218
252
  );
219
253
  } catch {
220
- showToastHandler?.('error', 'Não foi possível excluir este comentário.');
254
+ showToastHandler?.('error', ct('errors.deleteComment'));
221
255
  } finally {
222
256
  setDeletingId(null);
223
257
  }
@@ -227,7 +261,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
227
261
  <div className="flex flex-col gap-3">
228
262
  <div className="flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
229
263
  <MessageSquare className="size-3" />
230
- Comentários
264
+ {ct('title')}
231
265
  {comments.length > 0 ? (
232
266
  <span className="ml-0.5 rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground">
233
267
  {comments.length}
@@ -242,6 +276,10 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
242
276
  const isEditing = editingId === comment.id;
243
277
  const isDeleting = deletingId === comment.id;
244
278
  const isSaving = savingEditId === comment.id;
279
+ const ageMinutes =
280
+ (Date.now() - new Date(comment.createdAt).getTime()) / 60000;
281
+ const canModify =
282
+ editWindowMinutes === 0 || ageMinutes < editWindowMinutes;
245
283
 
246
284
  return (
247
285
  <div key={comment.id} className="flex gap-2.5">
@@ -260,7 +298,7 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
260
298
  <div className="min-w-0 flex-1">
261
299
  <div className="flex items-center justify-between gap-2">
262
300
  <span className="truncate text-xs font-semibold">
263
- {comment.actorName ?? 'Usuário'}
301
+ {comment.actorName ?? ct('defaultUser')}
264
302
  </span>
265
303
  <div className="flex shrink-0 items-center gap-1">
266
304
  <span className="text-[10px] text-muted-foreground">
@@ -268,48 +306,50 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
268
306
  </span>
269
307
  {!isEditing ? (
270
308
  <>
271
- <button
272
- type="button"
273
- aria-label="Editar comentário"
274
- disabled={isDeleting}
275
- onClick={() => handleStartEdit(comment)}
276
- className="rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
277
- >
278
- <Pencil className="size-3" />
279
- </button>
280
- <button
281
- type="button"
282
- aria-label="Excluir comentário"
283
- disabled={isDeleting}
284
- onClick={() => void handleDelete(comment)}
285
- className="rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-50"
286
- >
287
- <Trash2 className="size-3" />
288
- </button>
309
+ {canModify ? (
310
+ <>
311
+ <button
312
+ type="button"
313
+ aria-label={ct('ariaEditComment')}
314
+ disabled={isDeleting}
315
+ onClick={() => handleStartEdit(comment)}
316
+ className="rounded p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
317
+ >
318
+ <Pencil className="size-3" />
319
+ </button>
320
+ <button
321
+ type="button"
322
+ aria-label={ct('ariaDeleteComment')}
323
+ disabled={isDeleting}
324
+ onClick={() => void handleDelete(comment)}
325
+ className="rounded p-0.5 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-50"
326
+ >
327
+ <Trash2 className="size-3" />
328
+ </button>
329
+ </>
330
+ ) : null}
289
331
  </>
290
332
  ) : null}
291
333
  </div>
292
334
  </div>
293
335
  {isEditing ? (
294
336
  <div className="mt-1 flex flex-col gap-1.5">
295
- <Textarea
337
+ <RichTextEditor
296
338
  value={editContent}
297
- onChange={(e) => setEditContent(e.target.value)}
298
- rows={3}
299
- className="resize-none text-sm"
300
- disabled={isSaving}
301
- onKeyDown={(e) => {
302
- if (e.key === 'Escape') handleCancelEdit();
303
- }}
339
+ onChange={setEditContent}
340
+ mentions={mentionItems}
304
341
  />
305
342
  <div className="flex gap-1.5">
306
343
  <Button
307
344
  size="sm"
308
345
  className="h-7 px-2 text-xs"
309
- disabled={isSaving || !editContent.trim()}
346
+ disabled={
347
+ isSaving ||
348
+ !editContent.replace(/<[^>]*>/g, '').trim()
349
+ }
310
350
  onClick={() => void handleSaveEdit(comment)}
311
351
  >
312
- Salvar
352
+ {ct('saveButton')}
313
353
  </Button>
314
354
  <Button
315
355
  size="sm"
@@ -319,14 +359,12 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
319
359
  onClick={handleCancelEdit}
320
360
  >
321
361
  <X className="size-3" />
322
- Cancelar
362
+ {ct('cancelButton')}
323
363
  </Button>
324
364
  </div>
325
365
  </div>
326
366
  ) : (
327
- <p className="mt-0.5 text-sm leading-relaxed text-foreground">
328
- {comment.content}
329
- </p>
367
+ <CommentContent content={comment.content} />
330
368
  )}
331
369
  </div>
332
370
  </div>
@@ -334,32 +372,23 @@ function TaskCommentsSection({ taskId }: TaskCommentsSectionProps) {
334
372
  })}
335
373
  </div>
336
374
  ) : (
337
- <p className="text-xs text-muted-foreground">Nenhum comentário ainda.</p>
375
+ <p className="text-xs text-muted-foreground">{ct('noComments')}</p>
338
376
  )}
339
377
 
340
378
  <div className="flex flex-col gap-1.5 pt-1">
341
- <Textarea
342
- placeholder="Escreva um comentário..."
379
+ <RichTextEditor
343
380
  value={newComment}
344
- onChange={(e) => setNewComment(e.target.value)}
345
- rows={3}
346
- className="resize-none text-sm"
347
- disabled={submitting}
348
- onKeyDown={(e) => {
349
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
350
- void handleSubmit();
351
- }
352
- }}
381
+ onChange={setNewComment}
382
+ mentions={mentionItems}
353
383
  />
354
384
  <div className="flex justify-end">
355
385
  <Button
356
386
  size="sm"
357
387
  className="gap-1.5"
358
- disabled={submitting || !newComment.trim()}
388
+ disabled={submitting || !newComment.replace(/<[^>]*>/g, '').trim()}
359
389
  onClick={() => void handleSubmit()}
360
390
  >
361
- <Send className="size-3.5" />
362
- Comentar
391
+ {ct('submitButton')}
363
392
  </Button>
364
393
  </div>
365
394
  </div>
@@ -373,19 +402,26 @@ export function TaskDetailSheet({
373
402
  onOpenChange,
374
403
  statusLabel,
375
404
  footer,
405
+ defaultTab = 'comments',
376
406
  }: Props) {
377
407
  const commonT = useTranslations('operations.Common');
378
408
  const detailT = useTranslations('operations.ProjectDetailsPage');
379
409
  const { getSettingValue, currentLocaleCode } = useApp();
380
410
 
411
+ const [tab, setTab] = useState<'info' | 'comments'>(defaultTab);
412
+
413
+ useEffect(() => {
414
+ if (open) setTab(defaultTab);
415
+ }, [open, defaultTab]);
416
+
381
417
  const avatarSrc = getAvatarSrc(task);
382
418
 
383
419
  return (
384
420
  <Sheet open={open} onOpenChange={onOpenChange}>
385
- <SheetContent className="flex w-full flex-col gap-0 overflow-y-auto sm:max-w-md">
421
+ <SheetContent className="flex w-full flex-col gap-0 overflow-hidden sm:max-w-md">
386
422
  {task ? (
387
423
  <>
388
- <SheetHeader className="pb-2">
424
+ <SheetHeader className="shrink-0 px-4 pb-2 pt-4">
389
425
  <SheetTitle className="pr-6 text-base font-semibold leading-snug">
390
426
  {task.name}
391
427
  </SheetTitle>
@@ -404,115 +440,142 @@ export function TaskDetailSheet({
404
440
  </div>
405
441
  </SheetHeader>
406
442
 
407
- <div className="flex flex-col gap-5 p-4 pt-3">
408
- {task.description ? (
409
- <div>
410
- <p className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
411
- {detailT('taskForm.descriptionLabel')}
412
- </p>
413
- <p className="text-sm leading-relaxed">{task.description}</p>
414
- </div>
415
- ) : null}
416
-
417
- {task.dueDate || task.estimateHours != null ? (
418
- <div className="grid grid-cols-2 gap-3">
419
- {task.dueDate ? (
420
- <div className="flex items-start gap-2 text-sm">
421
- <Calendar className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
422
- <div>
423
- <p className="text-[11px] text-muted-foreground">
424
- {detailT('taskForm.deadlineLabel')}
425
- </p>
426
- <p className="font-medium">
427
- {formatDate(
428
- task.dueDate,
429
- getSettingValue,
430
- currentLocaleCode
431
- )}
432
- </p>
433
- </div>
443
+ <Tabs
444
+ value={tab}
445
+ onValueChange={(v) => setTab(v as 'info' | 'comments')}
446
+ className="flex min-h-0 flex-1 flex-col"
447
+ >
448
+ <TabsList className="mx-4 mb-1 mt-2 grid shrink-0 grid-cols-2">
449
+ <TabsTrigger value="info">
450
+ {detailT('taskForm.tabInfo')}
451
+ </TabsTrigger>
452
+ <TabsTrigger value="comments">
453
+ <MessageSquare className="mr-1.5 size-3.5" />
454
+ {detailT('taskForm.tabComments')}
455
+ </TabsTrigger>
456
+ </TabsList>
457
+
458
+ <TabsContent
459
+ value="info"
460
+ className="flex-1 overflow-y-auto px-4 pb-4 pt-2 data-[state=inactive]:hidden"
461
+ >
462
+ <div className="flex flex-col gap-5">
463
+ {task.description ? (
464
+ <div>
465
+ <p className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
466
+ {detailT('taskForm.descriptionLabel')}
467
+ </p>
468
+ <p className="text-sm leading-relaxed">
469
+ {task.description}
470
+ </p>
471
+ </div>
472
+ ) : null}
473
+
474
+ {task.dueDate || task.estimateHours != null ? (
475
+ <div className="grid grid-cols-2 gap-3">
476
+ {task.dueDate ? (
477
+ <div className="flex items-start gap-2 text-sm">
478
+ <Calendar className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
479
+ <div>
480
+ <p className="text-[11px] text-muted-foreground">
481
+ {detailT('taskForm.deadlineLabel')}
482
+ </p>
483
+ <p className="font-medium">
484
+ {formatDate(
485
+ task.dueDate,
486
+ getSettingValue,
487
+ currentLocaleCode
488
+ )}
489
+ </p>
490
+ </div>
491
+ </div>
492
+ ) : null}
493
+ {task.estimateHours != null ? (
494
+ <div className="flex items-start gap-2 text-sm">
495
+ <AlarmClock className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
496
+ <div>
497
+ <p className="text-[11px] text-muted-foreground">
498
+ {detailT('taskForm.estimateLabel')}
499
+ </p>
500
+ <p className="font-medium">{task.estimateHours}h</p>
501
+ </div>
502
+ </div>
503
+ ) : null}
434
504
  </div>
435
505
  ) : null}
436
- {task.estimateHours != null ? (
437
- <div className="flex items-start gap-2 text-sm">
438
- <AlarmClock className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
439
- <div>
440
- <p className="text-[11px] text-muted-foreground">
441
- {detailT('taskForm.estimateLabel')}
442
- </p>
443
- <p className="font-medium">{task.estimateHours}h</p>
506
+
507
+ {task.tags ? (
508
+ <div>
509
+ <div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
510
+ <Tag className="size-3" />
511
+ {detailT('taskForm.tagsLabel')}
512
+ </div>
513
+ <div className="flex flex-wrap gap-1.5">
514
+ {task.tags.split(',').map((tag) => (
515
+ <span
516
+ key={tag.trim()}
517
+ className="rounded-md bg-muted px-2 py-0.5 text-xs text-muted-foreground"
518
+ >
519
+ {tag.trim()}
520
+ </span>
521
+ ))}
444
522
  </div>
445
523
  </div>
446
524
  ) : null}
447
- </div>
448
- ) : null}
449
525
 
450
- {task.tags ? (
451
- <div>
452
- <div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
453
- <Tag className="size-3" />
454
- {detailT('taskForm.tagsLabel')}
455
- </div>
456
- <div className="flex flex-wrap gap-1.5">
457
- {task.tags.split(',').map((tag) => (
458
- <span
459
- key={tag.trim()}
460
- className="rounded-md bg-muted px-2 py-0.5 text-xs text-muted-foreground"
461
- >
462
- {tag.trim()}
463
- </span>
464
- ))}
465
- </div>
466
- </div>
467
- ) : null}
526
+ {task.assigneeName ? (
527
+ <div>
528
+ <div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
529
+ <User className="size-3" />
530
+ {commonT('labels.collaborator')}
531
+ </div>
532
+ <div className="flex items-center gap-2.5">
533
+ <div className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-xs font-semibold uppercase text-muted-foreground ring-1 ring-border">
534
+ {avatarSrc ? (
535
+ // eslint-disable-next-line @next/next/no-img-element
536
+ <img
537
+ src={avatarSrc}
538
+ alt={task.assigneeName}
539
+ className="size-full object-cover"
540
+ />
541
+ ) : (
542
+ getInitials(task.assigneeName)
543
+ )}
544
+ </div>
545
+ <span className="text-sm">{task.assigneeName}</span>
546
+ </div>
547
+ </div>
548
+ ) : null}
468
549
 
469
- {task.assigneeName ? (
470
- <div>
471
- <div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
472
- <User className="size-3" />
473
- {commonT('labels.collaborator')}
474
- </div>
475
- <div className="flex items-center gap-2.5">
476
- <div className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-xs font-semibold uppercase text-muted-foreground ring-1 ring-border">
477
- {avatarSrc ? (
478
- // eslint-disable-next-line @next/next/no-img-element
479
- <img
480
- src={avatarSrc}
481
- alt={task.assigneeName}
482
- className="size-full object-cover"
483
- />
484
- ) : (
485
- getInitials(task.assigneeName)
486
- )}
550
+ {task.projectName || task.projectCode ? (
551
+ <div className="rounded-lg border bg-muted/20 px-3 py-2.5">
552
+ <p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
553
+ {commonT('labels.project')}
554
+ </p>
555
+ <p className="mt-0.5 text-sm font-medium">
556
+ {[task.projectName, task.projectCode]
557
+ .filter(Boolean)
558
+ .join(' ')}
559
+ </p>
487
560
  </div>
488
- <span className="text-sm">{task.assigneeName}</span>
489
- </div>
490
- </div>
491
- ) : null}
492
-
493
- {task.projectName || task.projectCode ? (
494
- <div className="rounded-lg border bg-muted/20 px-3 py-2.5">
495
- <p className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
496
- {commonT('labels.project')}
497
- </p>
498
- <p className="mt-0.5 text-sm font-medium">
499
- {[task.projectName, task.projectCode]
500
- .filter(Boolean)
501
- .join(' • ')}
502
- </p>
561
+ ) : null}
562
+
563
+ {footer ? (
564
+ <div className="border-t pt-5">{footer}</div>
565
+ ) : null}
503
566
  </div>
504
- ) : null}
567
+ </TabsContent>
505
568
 
506
- <div className="border-t pt-4">
569
+ <TabsContent
570
+ value="comments"
571
+ className="flex-1 overflow-y-auto px-4 pb-4 pt-2 data-[state=inactive]:hidden"
572
+ >
507
573
  <TaskCommentsSection taskId={task.id} />
508
- </div>
509
-
510
- {footer ? <div className="border-t pt-5">{footer}</div> : null}
511
- </div>
574
+ </TabsContent>
575
+ </Tabs>
512
576
  </>
513
577
  ) : null}
514
578
  </SheetContent>
515
579
  </Sheet>
516
580
  );
517
581
  }
518
-