@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,34 +1,36 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
|
158
|
-
if (!
|
|
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
|
-
|
|
198
|
+
newComment
|
|
165
199
|
)) as OperationsTaskComment;
|
|
166
200
|
setLocalComments([...(localComments ?? fetchedComments), created]);
|
|
167
201
|
setNewComment('');
|
|
168
202
|
} catch {
|
|
169
|
-
showToastHandler?.('error', '
|
|
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
|
-
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
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 ?? '
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
<
|
|
337
|
+
<RichTextEditor
|
|
296
338
|
value={editContent}
|
|
297
|
-
onChange={
|
|
298
|
-
|
|
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={
|
|
346
|
+
disabled={
|
|
347
|
+
isSaving ||
|
|
348
|
+
!editContent.replace(/<[^>]*>/g, '').trim()
|
|
349
|
+
}
|
|
310
350
|
onClick={() => void handleSaveEdit(comment)}
|
|
311
351
|
>
|
|
312
|
-
|
|
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
|
-
|
|
362
|
+
{ct('cancelButton')}
|
|
323
363
|
</Button>
|
|
324
364
|
</div>
|
|
325
365
|
</div>
|
|
326
366
|
) : (
|
|
327
|
-
<
|
|
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">
|
|
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
|
-
<
|
|
342
|
-
placeholder="Escreva um comentário..."
|
|
379
|
+
<RichTextEditor
|
|
343
380
|
value={newComment}
|
|
344
|
-
onChange={
|
|
345
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
<
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
<div>
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
567
|
+
</TabsContent>
|
|
505
568
|
|
|
506
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|