@hed-hog/operations 0.0.321 → 0.0.322
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-contracts.controller.d.ts +9 -9
- package/dist/controllers/operations-tasks.controller.d.ts +22 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +37 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/dto/create-task.dto.d.ts.map +1 -1
- package/dist/dto/create-task.dto.js +0 -1
- package/dist/dto/create-task.dto.js.map +1 -1
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +0 -1
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.service.d.ts +22 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +77 -22
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/route.yaml +39 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +49 -22
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2968 -624
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -68
- package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +121 -11
- package/hedhog/frontend/app/projects/page.tsx.ejs +105 -22
- package/hedhog/frontend/messages/en.json +143 -2
- package/hedhog/frontend/messages/pt.json +143 -2
- package/hedhog/table/operations_task_file.yaml +23 -0
- package/package.json +5 -5
- package/src/controllers/operations-tasks.controller.ts +43 -9
- package/src/dto/create-task.dto.ts +0 -1
- package/src/dto/update-task.dto.ts +0 -1
- package/src/operations.service.ts +144 -22
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PaginationFooter,
|
|
7
7
|
SearchBar,
|
|
8
8
|
} from '@/components/entity-list';
|
|
9
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
9
10
|
import { Button } from '@/components/ui/button';
|
|
10
11
|
import { Card, CardContent } from '@/components/ui/card';
|
|
11
12
|
import {
|
|
@@ -33,7 +34,6 @@ import {
|
|
|
33
34
|
TableHeader,
|
|
34
35
|
TableRow,
|
|
35
36
|
} from '@/components/ui/table';
|
|
36
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
37
37
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
38
38
|
import {
|
|
39
39
|
closestCenter,
|
|
@@ -278,6 +278,10 @@ export default function OperationsMyTasksPage() {
|
|
|
278
278
|
const [taskFormLoading, setTaskFormLoading] = useState(false);
|
|
279
279
|
const [taskFormData, setTaskFormData] =
|
|
280
280
|
useState<TaskFormState>(EMPTY_TASK_FORM);
|
|
281
|
+
const [inlineCreateColumn, setInlineCreateColumn] =
|
|
282
|
+
useState<BoardColumnId | null>(null);
|
|
283
|
+
const [inlineCreateName, setInlineCreateName] = useState('');
|
|
284
|
+
const [inlineCreateLoading, setInlineCreateLoading] = useState(false);
|
|
281
285
|
|
|
282
286
|
// Paginated query for table/cards views
|
|
283
287
|
const { data: tasksResponse, refetch } = useQuery<
|
|
@@ -492,6 +496,32 @@ export default function OperationsMyTasksPage() {
|
|
|
492
496
|
[request, refetchAll]
|
|
493
497
|
);
|
|
494
498
|
|
|
499
|
+
const handleInlineCreateTask = useCallback(
|
|
500
|
+
async (column: BoardColumnId) => {
|
|
501
|
+
const trimmed = inlineCreateName.trim();
|
|
502
|
+
if (!trimmed) {
|
|
503
|
+
setInlineCreateColumn(null);
|
|
504
|
+
setInlineCreateName('');
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
setInlineCreateLoading(true);
|
|
508
|
+
try {
|
|
509
|
+
await mutateOperations(request, '/operations/tasks', 'POST', {
|
|
510
|
+
name: trimmed,
|
|
511
|
+
status: column,
|
|
512
|
+
priority: 'medium',
|
|
513
|
+
});
|
|
514
|
+
setBoardOverride(null);
|
|
515
|
+
setInlineCreateColumn(null);
|
|
516
|
+
setInlineCreateName('');
|
|
517
|
+
await refetchBoard();
|
|
518
|
+
} finally {
|
|
519
|
+
setInlineCreateLoading(false);
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
[inlineCreateName, request, refetchBoard]
|
|
523
|
+
);
|
|
524
|
+
|
|
495
525
|
const openCreateTaskForm = useCallback(() => {
|
|
496
526
|
setEditingTaskId(null);
|
|
497
527
|
setTaskFormData(EMPTY_TASK_FORM);
|
|
@@ -700,9 +730,21 @@ export default function OperationsMyTasksPage() {
|
|
|
700
730
|
<Rows3 className="size-4 text-muted-foreground" />
|
|
701
731
|
{column.label}
|
|
702
732
|
</div>
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
|
|
733
|
+
<div className="flex items-center gap-1">
|
|
734
|
+
<span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">
|
|
735
|
+
{boardColumns[column.id].length}
|
|
736
|
+
</span>
|
|
737
|
+
<button
|
|
738
|
+
type="button"
|
|
739
|
+
className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
740
|
+
onClick={() => {
|
|
741
|
+
setInlineCreateColumn(column.id);
|
|
742
|
+
setInlineCreateName('');
|
|
743
|
+
}}
|
|
744
|
+
>
|
|
745
|
+
<Plus className="size-3.5" />
|
|
746
|
+
</button>
|
|
747
|
+
</div>
|
|
706
748
|
</div>
|
|
707
749
|
|
|
708
750
|
<div className="space-y-2">
|
|
@@ -829,6 +871,77 @@ export default function OperationsMyTasksPage() {
|
|
|
829
871
|
</DraggableTaskCard>
|
|
830
872
|
))}
|
|
831
873
|
</div>
|
|
874
|
+
|
|
875
|
+
{inlineCreateColumn === column.id ? (
|
|
876
|
+
<div className="space-y-1.5 rounded-lg border bg-card p-2">
|
|
877
|
+
<Input
|
|
878
|
+
autoFocus
|
|
879
|
+
placeholder={detailT('taskForm.namePlaceholder')}
|
|
880
|
+
value={inlineCreateName}
|
|
881
|
+
onChange={(e) =>
|
|
882
|
+
setInlineCreateName(e.target.value)
|
|
883
|
+
}
|
|
884
|
+
onKeyDown={(e) => {
|
|
885
|
+
if (e.key === 'Enter') {
|
|
886
|
+
e.preventDefault();
|
|
887
|
+
void handleInlineCreateTask(column.id);
|
|
888
|
+
} else if (e.key === 'Escape') {
|
|
889
|
+
setInlineCreateColumn(null);
|
|
890
|
+
setInlineCreateName('');
|
|
891
|
+
}
|
|
892
|
+
}}
|
|
893
|
+
onBlur={() => {
|
|
894
|
+
if (!inlineCreateName.trim()) {
|
|
895
|
+
setInlineCreateColumn(null);
|
|
896
|
+
setInlineCreateName('');
|
|
897
|
+
}
|
|
898
|
+
}}
|
|
899
|
+
disabled={inlineCreateLoading}
|
|
900
|
+
className="h-8 text-sm"
|
|
901
|
+
/>
|
|
902
|
+
<div className="flex gap-1">
|
|
903
|
+
<Button
|
|
904
|
+
type="button"
|
|
905
|
+
size="sm"
|
|
906
|
+
className="h-7 px-2 text-xs"
|
|
907
|
+
disabled={
|
|
908
|
+
!inlineCreateName.trim() || inlineCreateLoading
|
|
909
|
+
}
|
|
910
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
911
|
+
onClick={() =>
|
|
912
|
+
void handleInlineCreateTask(column.id)
|
|
913
|
+
}
|
|
914
|
+
>
|
|
915
|
+
{t('actions.create')}
|
|
916
|
+
</Button>
|
|
917
|
+
<Button
|
|
918
|
+
type="button"
|
|
919
|
+
variant="ghost"
|
|
920
|
+
size="sm"
|
|
921
|
+
className="h-7 px-2 text-xs"
|
|
922
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
923
|
+
onClick={() => {
|
|
924
|
+
setInlineCreateColumn(null);
|
|
925
|
+
setInlineCreateName('');
|
|
926
|
+
}}
|
|
927
|
+
>
|
|
928
|
+
{commonT('actions.cancel')}
|
|
929
|
+
</Button>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
) : (
|
|
933
|
+
<button
|
|
934
|
+
type="button"
|
|
935
|
+
className="flex w-full cursor-pointer items-center gap-1 rounded-md px-1 py-1 text-xs text-muted-foreground transition hover:bg-muted hover:text-foreground"
|
|
936
|
+
onClick={() => {
|
|
937
|
+
setInlineCreateColumn(column.id);
|
|
938
|
+
setInlineCreateName('');
|
|
939
|
+
}}
|
|
940
|
+
>
|
|
941
|
+
<Plus className="size-3" />
|
|
942
|
+
{t('actions.create')}
|
|
943
|
+
</button>
|
|
944
|
+
)}
|
|
832
945
|
</div>
|
|
833
946
|
)}
|
|
834
947
|
</DroppableColumn>
|
|
@@ -1093,7 +1206,7 @@ export default function OperationsMyTasksPage() {
|
|
|
1093
1206
|
}
|
|
1094
1207
|
}}
|
|
1095
1208
|
>
|
|
1096
|
-
<DialogContent className="sm:max-w-
|
|
1209
|
+
<DialogContent className="sm:max-w-2xl">
|
|
1097
1210
|
<DialogHeader>
|
|
1098
1211
|
<DialogTitle>
|
|
1099
1212
|
{editingTaskId
|
|
@@ -1149,17 +1262,14 @@ export default function OperationsMyTasksPage() {
|
|
|
1149
1262
|
<Label htmlFor="task-description">
|
|
1150
1263
|
{detailT('taskForm.descriptionLabel')}
|
|
1151
1264
|
</Label>
|
|
1152
|
-
<
|
|
1153
|
-
id="task-description"
|
|
1265
|
+
<RichTextEditor
|
|
1154
1266
|
value={taskFormData.description}
|
|
1155
|
-
onChange={(
|
|
1267
|
+
onChange={(val) =>
|
|
1156
1268
|
setTaskFormData((prev) => ({
|
|
1157
1269
|
...prev,
|
|
1158
|
-
description:
|
|
1270
|
+
description: val,
|
|
1159
1271
|
}))
|
|
1160
1272
|
}
|
|
1161
|
-
rows={4}
|
|
1162
|
-
placeholder={detailT('taskForm.descriptionPlaceholder')}
|
|
1163
1273
|
/>
|
|
1164
1274
|
</div>
|
|
1165
1275
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PaginationFooter,
|
|
7
7
|
SearchBar,
|
|
8
8
|
} from '@/components/entity-list';
|
|
9
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
9
10
|
import { Button } from '@/components/ui/button';
|
|
10
11
|
import { Card, CardContent } from '@/components/ui/card';
|
|
11
12
|
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
28
29
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
29
30
|
import {
|
|
31
|
+
Archive,
|
|
32
|
+
ArchiveRestore,
|
|
30
33
|
CalendarDays,
|
|
31
34
|
Eye,
|
|
32
35
|
FileText,
|
|
@@ -53,6 +56,22 @@ const PROJECT_VIEW_STORAGE_KEY = 'operations-projects-view-mode';
|
|
|
53
56
|
|
|
54
57
|
type ProjectViewMode = 'table' | 'cards';
|
|
55
58
|
|
|
59
|
+
function getPersonAvatarUrl(avatarId?: number | null): string {
|
|
60
|
+
return typeof avatarId === 'number' && avatarId > 0
|
|
61
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
62
|
+
: '/placeholder.png';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getInitials(value?: string | null): string {
|
|
66
|
+
if (!value) return '?';
|
|
67
|
+
return value
|
|
68
|
+
.split(' ')
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.slice(0, 2)
|
|
71
|
+
.map((w) => w[0]!.toUpperCase())
|
|
72
|
+
.join('');
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
function parseEditProjectId(value: string | null) {
|
|
57
76
|
const parsed = Number(value);
|
|
58
77
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
@@ -335,6 +354,9 @@ export default function OperationsProjectsPage() {
|
|
|
335
354
|
<Card
|
|
336
355
|
key={project.id}
|
|
337
356
|
className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
|
|
357
|
+
onDoubleClick={() =>
|
|
358
|
+
router.push(`/operations/projects/${project.id}`)
|
|
359
|
+
}
|
|
338
360
|
>
|
|
339
361
|
<CardContent className="space-y-4 p-4">
|
|
340
362
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
@@ -359,17 +381,39 @@ export default function OperationsProjectsPage() {
|
|
|
359
381
|
</div>
|
|
360
382
|
|
|
361
383
|
<div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
|
|
362
|
-
<div>
|
|
363
|
-
<span className="font-medium text-foreground">
|
|
384
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
385
|
+
<span className="shrink-0 font-medium text-foreground">
|
|
364
386
|
{commonT('labels.client')}:
|
|
365
|
-
</span>
|
|
366
|
-
|
|
387
|
+
</span>
|
|
388
|
+
<Avatar className="h-5 w-5 shrink-0">
|
|
389
|
+
<AvatarImage
|
|
390
|
+
src={getPersonAvatarUrl(project.clientAvatarId)}
|
|
391
|
+
alt={project.clientName ?? ''}
|
|
392
|
+
/>
|
|
393
|
+
<AvatarFallback className="text-[9px] font-medium">
|
|
394
|
+
{getInitials(project.clientName)}
|
|
395
|
+
</AvatarFallback>
|
|
396
|
+
</Avatar>
|
|
397
|
+
<span className="truncate">
|
|
398
|
+
{project.clientName || commonT('labels.notAvailable')}
|
|
399
|
+
</span>
|
|
367
400
|
</div>
|
|
368
|
-
<div>
|
|
369
|
-
<span className="font-medium text-foreground">
|
|
401
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
402
|
+
<span className="shrink-0 font-medium text-foreground">
|
|
370
403
|
{commonT('labels.manager')}:
|
|
371
|
-
</span>
|
|
372
|
-
|
|
404
|
+
</span>
|
|
405
|
+
<Avatar className="h-5 w-5 shrink-0">
|
|
406
|
+
<AvatarImage
|
|
407
|
+
src={getPersonAvatarUrl(project.managerAvatarId)}
|
|
408
|
+
alt={project.managerName ?? ''}
|
|
409
|
+
/>
|
|
410
|
+
<AvatarFallback className="text-[9px] font-medium">
|
|
411
|
+
{getInitials(project.managerName)}
|
|
412
|
+
</AvatarFallback>
|
|
413
|
+
</Avatar>
|
|
414
|
+
<span className="truncate">
|
|
415
|
+
{project.managerName || commonT('labels.notAssigned')}
|
|
416
|
+
</span>
|
|
373
417
|
</div>
|
|
374
418
|
<div>
|
|
375
419
|
<span className="font-medium text-foreground">
|
|
@@ -414,7 +458,7 @@ export default function OperationsProjectsPage() {
|
|
|
414
458
|
</div>
|
|
415
459
|
</div>
|
|
416
460
|
|
|
417
|
-
<div className="flex
|
|
461
|
+
<div className="flex items-center justify-end gap-2 border-t border-border/60 pt-3">
|
|
418
462
|
<Button variant="outline" size="icon" asChild>
|
|
419
463
|
<Link href={`/operations/projects/${project.id}`}>
|
|
420
464
|
<Eye className="size-4" />
|
|
@@ -451,13 +495,20 @@ export default function OperationsProjectsPage() {
|
|
|
451
495
|
{access.isDirector ? (
|
|
452
496
|
<Button
|
|
453
497
|
variant="outline"
|
|
454
|
-
size="
|
|
498
|
+
size="icon"
|
|
455
499
|
className="cursor-pointer"
|
|
500
|
+
title={
|
|
501
|
+
project.status === 'archived'
|
|
502
|
+
? commonT('actions.activate')
|
|
503
|
+
: t('actions.archive')
|
|
504
|
+
}
|
|
456
505
|
onClick={() => void toggleArchived(project)}
|
|
457
506
|
>
|
|
458
|
-
{project.status === 'archived'
|
|
459
|
-
|
|
460
|
-
|
|
507
|
+
{project.status === 'archived' ? (
|
|
508
|
+
<ArchiveRestore className="size-4" />
|
|
509
|
+
) : (
|
|
510
|
+
<Archive className="size-4" />
|
|
511
|
+
)}
|
|
461
512
|
</Button>
|
|
462
513
|
) : null}
|
|
463
514
|
</div>
|
|
@@ -500,6 +551,9 @@ export default function OperationsProjectsPage() {
|
|
|
500
551
|
<TableRow
|
|
501
552
|
key={project.id}
|
|
502
553
|
className="cursor-pointer hover:bg-muted/30"
|
|
554
|
+
onDoubleClick={() =>
|
|
555
|
+
router.push(`/operations/projects/${project.id}`)
|
|
556
|
+
}
|
|
503
557
|
>
|
|
504
558
|
<TableCell>
|
|
505
559
|
<div className="min-w-0">
|
|
@@ -518,8 +572,19 @@ export default function OperationsProjectsPage() {
|
|
|
518
572
|
</div>
|
|
519
573
|
</TableCell>
|
|
520
574
|
<TableCell>
|
|
521
|
-
<div className="
|
|
522
|
-
|
|
575
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
576
|
+
<Avatar className="h-6 w-6 shrink-0">
|
|
577
|
+
<AvatarImage
|
|
578
|
+
src={getPersonAvatarUrl(project.clientAvatarId)}
|
|
579
|
+
alt={project.clientName ?? ''}
|
|
580
|
+
/>
|
|
581
|
+
<AvatarFallback className="text-[9px] font-medium">
|
|
582
|
+
{getInitials(project.clientName)}
|
|
583
|
+
</AvatarFallback>
|
|
584
|
+
</Avatar>
|
|
585
|
+
<span className="truncate">
|
|
586
|
+
{project.clientName || commonT('labels.notAvailable')}
|
|
587
|
+
</span>
|
|
523
588
|
</div>
|
|
524
589
|
</TableCell>
|
|
525
590
|
<TableCell>
|
|
@@ -529,8 +594,19 @@ export default function OperationsProjectsPage() {
|
|
|
529
594
|
/>
|
|
530
595
|
</TableCell>
|
|
531
596
|
<TableCell className="hidden lg:table-cell">
|
|
532
|
-
<div className="
|
|
533
|
-
|
|
597
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
598
|
+
<Avatar className="h-6 w-6 shrink-0">
|
|
599
|
+
<AvatarImage
|
|
600
|
+
src={getPersonAvatarUrl(project.managerAvatarId)}
|
|
601
|
+
alt={project.managerName ?? ''}
|
|
602
|
+
/>
|
|
603
|
+
<AvatarFallback className="text-[9px] font-medium">
|
|
604
|
+
{getInitials(project.managerName)}
|
|
605
|
+
</AvatarFallback>
|
|
606
|
+
</Avatar>
|
|
607
|
+
<span className="truncate">
|
|
608
|
+
{project.managerName || commonT('labels.notAssigned')}
|
|
609
|
+
</span>
|
|
534
610
|
</div>
|
|
535
611
|
</TableCell>
|
|
536
612
|
<TableCell className="hidden md:table-cell">
|
|
@@ -563,7 +639,7 @@ export default function OperationsProjectsPage() {
|
|
|
563
639
|
)}
|
|
564
640
|
</TableCell>
|
|
565
641
|
<TableCell>
|
|
566
|
-
<div className="flex
|
|
642
|
+
<div className="flex items-center justify-end gap-1.5">
|
|
567
643
|
<Button variant="outline" size="icon" asChild>
|
|
568
644
|
<Link href={`/operations/projects/${project.id}`}>
|
|
569
645
|
<Eye className="size-4" />
|
|
@@ -600,13 +676,20 @@ export default function OperationsProjectsPage() {
|
|
|
600
676
|
{access.isDirector ? (
|
|
601
677
|
<Button
|
|
602
678
|
variant="outline"
|
|
603
|
-
size="
|
|
679
|
+
size="icon"
|
|
604
680
|
className="cursor-pointer"
|
|
681
|
+
title={
|
|
682
|
+
project.status === 'archived'
|
|
683
|
+
? commonT('actions.activate')
|
|
684
|
+
: t('actions.archive')
|
|
685
|
+
}
|
|
605
686
|
onClick={() => void toggleArchived(project)}
|
|
606
687
|
>
|
|
607
|
-
{project.status === 'archived'
|
|
608
|
-
|
|
609
|
-
|
|
688
|
+
{project.status === 'archived' ? (
|
|
689
|
+
<ArchiveRestore className="size-4" />
|
|
690
|
+
) : (
|
|
691
|
+
<Archive className="size-4" />
|
|
692
|
+
)}
|
|
610
693
|
</Button>
|
|
611
694
|
) : null}
|
|
612
695
|
</div>
|
|
@@ -723,7 +723,69 @@
|
|
|
723
723
|
"loggedHours": "Logged hours",
|
|
724
724
|
"loggedHoursDescription": "Total hours linked to this project.",
|
|
725
725
|
"allocation": "Average allocation",
|
|
726
|
-
"allocationDescription": "Average assignment allocation across linked collaborators."
|
|
726
|
+
"allocationDescription": "Average assignment allocation across linked collaborators.",
|
|
727
|
+
"lateTasks": "Late tasks",
|
|
728
|
+
"lateTasksDescription": "Open tasks with overdue deadlines.",
|
|
729
|
+
"weeklyVelocity": "Weekly velocity",
|
|
730
|
+
"weeklyVelocityDescription": "Recent or planned hours for the week.",
|
|
731
|
+
"projectHealth": "Project health",
|
|
732
|
+
"pendingTasks": "Pending tasks",
|
|
733
|
+
"pendingTasksDescription": "{overdue} overdue task(s) right now.",
|
|
734
|
+
"activeCollaborators": "Active collaborators",
|
|
735
|
+
"activeCollaboratorsDescription": "People with active allocation on this project."
|
|
736
|
+
},
|
|
737
|
+
"kpi": {
|
|
738
|
+
"indicator": "Indicator",
|
|
739
|
+
"subtitles": {
|
|
740
|
+
"health": "Visual blend of progress, pendencies, and allocation."
|
|
741
|
+
},
|
|
742
|
+
"trends": {
|
|
743
|
+
"hours": "{count} timesheet(s)",
|
|
744
|
+
"health": {
|
|
745
|
+
"good": "Positive",
|
|
746
|
+
"warning": "Warning",
|
|
747
|
+
"danger": "Critical"
|
|
748
|
+
},
|
|
749
|
+
"velocity": {
|
|
750
|
+
"active": "Moving",
|
|
751
|
+
"empty": "No recent pace"
|
|
752
|
+
},
|
|
753
|
+
"allocation": {
|
|
754
|
+
"good": "Balanced",
|
|
755
|
+
"warning": "High",
|
|
756
|
+
"critical": "Overloaded"
|
|
757
|
+
},
|
|
758
|
+
"tasks": {
|
|
759
|
+
"good": "No pendencies",
|
|
760
|
+
"warning": "Active backlog",
|
|
761
|
+
"critical": "{count} late"
|
|
762
|
+
},
|
|
763
|
+
"collaborators": {
|
|
764
|
+
"active": "Active team",
|
|
765
|
+
"empty": "No team"
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
"quickActions": {
|
|
770
|
+
"timesheet": "Timesheet",
|
|
771
|
+
"reports": "Reports",
|
|
772
|
+
"more": "More actions"
|
|
773
|
+
},
|
|
774
|
+
"executive": {
|
|
775
|
+
"team": "Team",
|
|
776
|
+
"activeTasks": "Active tasks",
|
|
777
|
+
"completedTasks": "Completed",
|
|
778
|
+
"fallbackDescription": "Track delivery, staffing, contract, and operational signals for this project.",
|
|
779
|
+
"membersCount": "{count} member(s)"
|
|
780
|
+
},
|
|
781
|
+
"breadcrumbTrail": {
|
|
782
|
+
"operations": "Operations",
|
|
783
|
+
"projects": "Projects"
|
|
784
|
+
},
|
|
785
|
+
"health": {
|
|
786
|
+
"good": "Healthy",
|
|
787
|
+
"warning": "Attention",
|
|
788
|
+
"danger": "Critical"
|
|
727
789
|
},
|
|
728
790
|
"sections": {
|
|
729
791
|
"overview": "Overview",
|
|
@@ -752,14 +814,92 @@
|
|
|
752
814
|
"deliveryHealthDescription": "Visual overview of team allocation and operational pace.",
|
|
753
815
|
"quickRadar": "Quick radar",
|
|
754
816
|
"quickRadarDescription": "Short-term decision signals.",
|
|
817
|
+
"timeline": "Operational timeline",
|
|
818
|
+
"timelineDescription": "Recent activity grouped by day, ready for filters and incremental loading.",
|
|
755
819
|
"taskBoard": "Task board",
|
|
756
820
|
"taskBoardDescription": "Kanban-style board with drag between columns and task side panel.",
|
|
757
821
|
"archivedTasks": "Archived tasks",
|
|
758
822
|
"archivedTasksDescription": "Review archived project tasks, open details, restore them, or delete them permanently."
|
|
759
823
|
},
|
|
760
824
|
"charts": {
|
|
825
|
+
"projectProgress": "Progress",
|
|
826
|
+
"burnup": "Burnup / progress",
|
|
827
|
+
"burnupDescription": "Accumulated logged hours against the planned operating pace.",
|
|
761
828
|
"allocationByCollaborator": "Allocation by collaborator",
|
|
762
|
-
"
|
|
829
|
+
"allocationDescription": "Capacity distribution across project collaborators.",
|
|
830
|
+
"weeklyVelocity": "Weekly velocity",
|
|
831
|
+
"weeklyVelocityDescription": "Recent pace of hours logged by the team.",
|
|
832
|
+
"taskDistribution": "Task distribution",
|
|
833
|
+
"taskDistributionDescription": "Task volume by board stage.",
|
|
834
|
+
"operationalHealth": "Operational health",
|
|
835
|
+
"operationalHealthDescription": "Visual score calculated from progress, pendencies, and allocation.",
|
|
836
|
+
"healthScore": "Operational score",
|
|
837
|
+
"start": "Start",
|
|
838
|
+
"current": "Current",
|
|
839
|
+
"emptyTitle": "Not enough data",
|
|
840
|
+
"emptyBurnup": "The burnup will appear once there is progress or logged hours.",
|
|
841
|
+
"emptyVelocity": "No weekly velocity has been returned for this project yet.",
|
|
842
|
+
"emptyAllocation": "Allocation will appear when collaborators are assigned.",
|
|
843
|
+
"emptyTasks": "Tasks will appear once the board has items."
|
|
844
|
+
},
|
|
845
|
+
"kanban": {
|
|
846
|
+
"items": "items",
|
|
847
|
+
"progress": "Progress",
|
|
848
|
+
"searchPlaceholder": "Search task, tag, assignee...",
|
|
849
|
+
"filters": "Filters",
|
|
850
|
+
"allPriorities": "All priorities",
|
|
851
|
+
"groupStatus": "Group by status",
|
|
852
|
+
"noEstimate": "No est.",
|
|
853
|
+
"emptyColumn": "No tasks in this column.",
|
|
854
|
+
"noFilteredTasks": "No tasks match the filters."
|
|
855
|
+
},
|
|
856
|
+
"timeline": {
|
|
857
|
+
"projectStarted": "Project started",
|
|
858
|
+
"projectStartedDescription": "{project} entered execution.",
|
|
859
|
+
"taskCreated": "Task created",
|
|
860
|
+
"taskCompleted": "Task completed",
|
|
861
|
+
"commentAdded": "Comment recorded",
|
|
862
|
+
"timesheetLogged": "Timesheet logged",
|
|
863
|
+
"timesheetLoggedDescription": "{count} timesheet(s), totaling {hours}.",
|
|
864
|
+
"approvalPending": "Approval pending",
|
|
865
|
+
"approvalPendingDescription": "{count} item(s) waiting for review.",
|
|
866
|
+
"completedTasks": "Completed tasks",
|
|
867
|
+
"completedTasksDescription": "{count} task(s) completed",
|
|
868
|
+
"pendingTimesheets": "Pending timesheets",
|
|
869
|
+
"pendingTimesheetsDescription": "{count} timesheet(s) waiting for review",
|
|
870
|
+
"targetDate": "Target date",
|
|
871
|
+
"targetDateDescription": "Operational project deadline.",
|
|
872
|
+
"loadMore": "Load more events",
|
|
873
|
+
"emptyTitle": "No activity to show",
|
|
874
|
+
"empty": "There is not enough operational activity yet to build a timeline.",
|
|
875
|
+
"filters": {
|
|
876
|
+
"all": "All events",
|
|
877
|
+
"task": "Tasks",
|
|
878
|
+
"status": "Status",
|
|
879
|
+
"timesheet": "Timesheets",
|
|
880
|
+
"approval": "Approvals",
|
|
881
|
+
"comment": "Comments"
|
|
882
|
+
},
|
|
883
|
+
"types": {
|
|
884
|
+
"task": "Task",
|
|
885
|
+
"timesheet": "Timesheet",
|
|
886
|
+
"approval": "Approval",
|
|
887
|
+
"comment": "Comment",
|
|
888
|
+
"status": "Status"
|
|
889
|
+
}
|
|
890
|
+
},
|
|
891
|
+
"teamPanel": {
|
|
892
|
+
"available": "Available",
|
|
893
|
+
"highAllocation": "High allocation",
|
|
894
|
+
"overload": "Overload",
|
|
895
|
+
"usedHours": "Used hours",
|
|
896
|
+
"availability": "Availability",
|
|
897
|
+
"overloadWarning": "{value}% above recommended capacity.",
|
|
898
|
+
"status": {
|
|
899
|
+
"available": "Available",
|
|
900
|
+
"high": "High load",
|
|
901
|
+
"overload": "Overload"
|
|
902
|
+
}
|
|
763
903
|
},
|
|
764
904
|
"quickRadar": {
|
|
765
905
|
"activeAssignments": "Active assignments",
|
|
@@ -779,6 +919,7 @@
|
|
|
779
919
|
"estimateLabel": "Estimate (h)",
|
|
780
920
|
"tagsLabel": "Tags",
|
|
781
921
|
"tagsPlaceholder": "planning, client, design (comma-separated)",
|
|
922
|
+
"attachmentsLabel": "Attachments",
|
|
782
923
|
"saving": "Saving..."
|
|
783
924
|
},
|
|
784
925
|
"dialogs": {
|