@hed-hog/operations 0.0.332 → 0.0.347
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 +55 -36
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-projects.controller.d.ts +3 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/operations.service.d.ts +58 -36
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +34 -34
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +6 -0
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +5 -3
- package/hedhog/data/route.yaml +7 -7
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +3 -1
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
- package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
- package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +5 -4
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +0 -6
- package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +23 -50
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -28
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +23 -6
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
- package/hedhog/frontend/app/_lib/api.ts.ejs +2 -2
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +1 -1
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +2 -16
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +86 -24
- package/hedhog/frontend/app/projects/page.tsx.ejs +6 -42
- package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
- package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
- package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/index.ts.ejs +25 -25
- package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
- package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
- package/hedhog/table/operations_collaborator.yaml +8 -8
- package/hedhog/table/operations_task.yaml +76 -76
- package/hedhog/table/operations_task_activity.yaml +51 -51
- package/package.json +6 -6
- package/src/controllers/operations-collaborators.controller.ts +9 -9
- package/src/controllers/operations-tasks.controller.ts +156 -156
- package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
- package/src/dto/create-collaborator.dto.ts +4 -4
- package/src/operations.service.spec.ts +1006 -988
- package/src/operations.service.ts +40 -42
|
@@ -1,358 +1,358 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Button } from '@/components/ui/button';
|
|
4
|
-
import {
|
|
5
|
-
Tooltip,
|
|
6
|
-
TooltipContent,
|
|
7
|
-
TooltipTrigger,
|
|
8
|
-
} from '@/components/ui/tooltip';
|
|
9
|
-
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
10
|
-
import {
|
|
11
|
-
AlignLeft,
|
|
12
|
-
CalendarClock,
|
|
13
|
-
Flag,
|
|
14
|
-
MessageSquare,
|
|
15
|
-
Paperclip,
|
|
16
|
-
Plus,
|
|
17
|
-
} from 'lucide-react';
|
|
18
|
-
import { useTranslations } from 'next-intl';
|
|
19
|
-
import { useState } from 'react';
|
|
20
|
-
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
21
|
-
import type { OperationsTaskOption, PaginatedResponse } from '../_lib/types';
|
|
22
|
-
import { formatDate } from '../_lib/utils/format';
|
|
23
|
-
import { TaskFormSheet } from './task-form-sheet';
|
|
24
|
-
|
|
25
|
-
const PAGE_SIZE = 10;
|
|
26
|
-
|
|
27
|
-
const TASK_COLUMNS = [
|
|
28
|
-
{ id: 'todo', label: 'Backlog' },
|
|
29
|
-
{ id: 'doing', label: 'Em execução' },
|
|
30
|
-
{ id: 'review', label: 'Revisão' },
|
|
31
|
-
{ id: 'done', label: 'Concluído' },
|
|
32
|
-
] as const;
|
|
33
|
-
|
|
34
|
-
function getPriorityLabel(value?: string | null) {
|
|
35
|
-
const map: Record<string, string> = {
|
|
36
|
-
low: 'Baixa',
|
|
37
|
-
medium: 'Média',
|
|
38
|
-
high: 'Alta',
|
|
39
|
-
};
|
|
40
|
-
return map[value ?? ''] ?? value ?? '—';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function getPriorityStripe(value?: string | null) {
|
|
44
|
-
if (value === 'high') return 'bg-rose-500';
|
|
45
|
-
if (value === 'medium') return 'bg-amber-500';
|
|
46
|
-
if (value === 'low') return 'bg-emerald-500';
|
|
47
|
-
return 'bg-border';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getPriorityChip(value?: string | null) {
|
|
51
|
-
if (value === 'high')
|
|
52
|
-
return 'bg-rose-500/10 text-rose-600 dark:text-rose-400 ring-1 ring-rose-500/20';
|
|
53
|
-
if (value === 'medium')
|
|
54
|
-
return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-1 ring-amber-500/20';
|
|
55
|
-
return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-1 ring-emerald-500/20';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function getStatusDot(value?: string | null) {
|
|
59
|
-
if (value === 'doing') return 'bg-blue-500';
|
|
60
|
-
if (value === 'review') return 'bg-amber-500';
|
|
61
|
-
if (value === 'done') return 'bg-emerald-500';
|
|
62
|
-
return 'bg-muted-foreground/50';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isDueDateOverdue(dueDate?: string | null) {
|
|
66
|
-
if (!dueDate) return false;
|
|
67
|
-
return new Date(dueDate) < new Date();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type CollaboratorTasksTabProps = {
|
|
71
|
-
collaboratorId: number | null | undefined;
|
|
72
|
-
collaboratorName?: string | null;
|
|
73
|
-
request: Parameters<typeof mutateOperations>[0];
|
|
74
|
-
disabled?: boolean;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function CollaboratorTasksTab({
|
|
78
|
-
collaboratorId,
|
|
79
|
-
request,
|
|
80
|
-
disabled,
|
|
81
|
-
}: CollaboratorTasksTabProps) {
|
|
82
|
-
const { getSettingValue, currentLocaleCode } = useApp();
|
|
83
|
-
const commonT = useTranslations('operations.Common');
|
|
84
|
-
const taskT = useTranslations('operations.ProjectDetailsPage');
|
|
85
|
-
|
|
86
|
-
const [page, setPage] = useState(0);
|
|
87
|
-
const [taskFormOpen, setTaskFormOpen] = useState(false);
|
|
88
|
-
const [editingTask, setEditingTask] = useState<OperationsTaskOption | null>(
|
|
89
|
-
null
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const tasksParams = new URLSearchParams({
|
|
93
|
-
page: String(page + 1),
|
|
94
|
-
pageSize: String(PAGE_SIZE),
|
|
95
|
-
});
|
|
96
|
-
if (collaboratorId) tasksParams.set('collaboratorId', String(collaboratorId));
|
|
97
|
-
|
|
98
|
-
const { data: tasksResponse, refetch: refetchTasks } = useQuery<
|
|
99
|
-
PaginatedResponse<OperationsTaskOption>
|
|
100
|
-
>({
|
|
101
|
-
queryKey: ['collaborator-tasks', collaboratorId, page],
|
|
102
|
-
enabled: Boolean(collaboratorId),
|
|
103
|
-
staleTime: 0,
|
|
104
|
-
refetchOnMount: 'always',
|
|
105
|
-
queryFn: () =>
|
|
106
|
-
fetchOperations<PaginatedResponse<OperationsTaskOption>>(
|
|
107
|
-
request,
|
|
108
|
-
`/operations/tasks?${tasksParams.toString()}`
|
|
109
|
-
),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const tasks = tasksResponse?.data ?? [];
|
|
113
|
-
const total = tasksResponse?.total ?? 0;
|
|
114
|
-
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
115
|
-
|
|
116
|
-
const openCreate = () => {
|
|
117
|
-
setEditingTask(null);
|
|
118
|
-
setTaskFormOpen(true);
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const openEdit = (task: OperationsTaskOption) => {
|
|
122
|
-
setEditingTask(task);
|
|
123
|
-
setTaskFormOpen(true);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
if (!collaboratorId) {
|
|
127
|
-
return (
|
|
128
|
-
<p className="text-sm text-muted-foreground">
|
|
129
|
-
{commonT('states.saveBefore')}
|
|
130
|
-
</p>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return (
|
|
135
|
-
<div className="space-y-3">
|
|
136
|
-
{!disabled ? (
|
|
137
|
-
<div className="flex items-center justify-between">
|
|
138
|
-
<span className="text-sm text-muted-foreground">
|
|
139
|
-
{total > 0
|
|
140
|
-
? `${total} ${total === 1 ? 'tarefa' : 'tarefas'}`
|
|
141
|
-
: null}
|
|
142
|
-
</span>
|
|
143
|
-
<Button
|
|
144
|
-
type="button"
|
|
145
|
-
variant="outline"
|
|
146
|
-
size="sm"
|
|
147
|
-
className="gap-1.5"
|
|
148
|
-
onClick={openCreate}
|
|
149
|
-
>
|
|
150
|
-
<Plus className="size-3.5" />
|
|
151
|
-
{taskT('taskForm.titleNew')}
|
|
152
|
-
</Button>
|
|
153
|
-
</div>
|
|
154
|
-
) : null}
|
|
155
|
-
|
|
156
|
-
{tasks.length === 0 ? (
|
|
157
|
-
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
158
|
-
<AlignLeft className="size-8 text-muted-foreground/40" />
|
|
159
|
-
<p className="text-sm text-muted-foreground">
|
|
160
|
-
Nenhuma tarefa encontrada.
|
|
161
|
-
</p>
|
|
162
|
-
</div>
|
|
163
|
-
) : (
|
|
164
|
-
<>
|
|
165
|
-
<div className="space-y-1.5">
|
|
166
|
-
{tasks.map((task) => {
|
|
167
|
-
const overdue =
|
|
168
|
-
isDueDateOverdue(task.dueDate) && task.status !== 'done';
|
|
169
|
-
const columnLabel =
|
|
170
|
-
TASK_COLUMNS.find((c) => c.id === task.status)?.label ??
|
|
171
|
-
task.status;
|
|
172
|
-
const hasMeta =
|
|
173
|
-
(task.fileCount ?? 0) > 0 || (task.commentCount ?? 0) > 0;
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<button
|
|
177
|
-
key={task.id}
|
|
178
|
-
type="button"
|
|
179
|
-
className="group flex w-full cursor-pointer items-stretch overflow-hidden rounded-lg border border-border/50 bg-card text-left shadow-sm transition-all duration-150 hover:border-border hover:shadow-md"
|
|
180
|
-
onClick={() => openEdit(task)}
|
|
181
|
-
>
|
|
182
|
-
{/* Priority stripe */}
|
|
183
|
-
<div
|
|
184
|
-
className={`w-0.75 shrink-0 transition-opacity ${getPriorityStripe(task.priority)}`}
|
|
185
|
-
/>
|
|
186
|
-
|
|
187
|
-
{/* Content */}
|
|
188
|
-
<div className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3">
|
|
189
|
-
{/* Left: name + project */}
|
|
190
|
-
<div className="min-w-0 flex-1">
|
|
191
|
-
<p className="truncate text-sm font-semibold leading-5 text-foreground">
|
|
192
|
-
{task.name}
|
|
193
|
-
</p>
|
|
194
|
-
{task.projectName ? (
|
|
195
|
-
<p className="mt-0.5 truncate text-[11px] leading-none text-muted-foreground/70">
|
|
196
|
-
{[task.projectCode, task.projectName]
|
|
197
|
-
.filter(Boolean)
|
|
198
|
-
.join(' · ')}
|
|
199
|
-
</p>
|
|
200
|
-
) : null}
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
{/* Right: indicators */}
|
|
204
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
205
|
-
{/* Priority chip */}
|
|
206
|
-
{task.priority ? (
|
|
207
|
-
<Tooltip>
|
|
208
|
-
<TooltipTrigger asChild>
|
|
209
|
-
<span
|
|
210
|
-
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium ${getPriorityChip(task.priority)}`}
|
|
211
|
-
>
|
|
212
|
-
<Flag className="size-2.5" />
|
|
213
|
-
{getPriorityLabel(task.priority)}
|
|
214
|
-
</span>
|
|
215
|
-
</TooltipTrigger>
|
|
216
|
-
<TooltipContent side="top">
|
|
217
|
-
Prioridade: {getPriorityLabel(task.priority)}
|
|
218
|
-
</TooltipContent>
|
|
219
|
-
</Tooltip>
|
|
220
|
-
) : null}
|
|
221
|
-
|
|
222
|
-
{/* Status / column */}
|
|
223
|
-
<Tooltip>
|
|
224
|
-
<TooltipTrigger asChild>
|
|
225
|
-
<span className="inline-flex items-center gap-1.5 rounded-md bg-muted/60 px-2 py-0.5 text-[11px] font-medium text-foreground/80 ring-1 ring-border/40">
|
|
226
|
-
<span
|
|
227
|
-
className={`size-1.5 shrink-0 rounded-full ${getStatusDot(task.status)}`}
|
|
228
|
-
/>
|
|
229
|
-
{columnLabel}
|
|
230
|
-
</span>
|
|
231
|
-
</TooltipTrigger>
|
|
232
|
-
<TooltipContent side="top">
|
|
233
|
-
Coluna: {columnLabel}
|
|
234
|
-
</TooltipContent>
|
|
235
|
-
</Tooltip>
|
|
236
|
-
|
|
237
|
-
{/* Divider before date/meta */}
|
|
238
|
-
{task.dueDate || hasMeta ? (
|
|
239
|
-
<div className="h-3.5 w-px bg-border/60" />
|
|
240
|
-
) : null}
|
|
241
|
-
|
|
242
|
-
{/* Due date chip */}
|
|
243
|
-
{task.dueDate ? (
|
|
244
|
-
<Tooltip>
|
|
245
|
-
<TooltipTrigger asChild>
|
|
246
|
-
<span
|
|
247
|
-
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium ${
|
|
248
|
-
overdue
|
|
249
|
-
? 'bg-rose-500/10 text-rose-600 ring-1 ring-rose-500/20 dark:text-rose-400'
|
|
250
|
-
: 'bg-muted/60 text-muted-foreground ring-1 ring-border/40'
|
|
251
|
-
}`}
|
|
252
|
-
>
|
|
253
|
-
<CalendarClock className="size-2.5 shrink-0" />
|
|
254
|
-
{formatDate(
|
|
255
|
-
task.dueDate,
|
|
256
|
-
getSettingValue,
|
|
257
|
-
currentLocaleCode
|
|
258
|
-
)}
|
|
259
|
-
</span>
|
|
260
|
-
</TooltipTrigger>
|
|
261
|
-
<TooltipContent side="top">
|
|
262
|
-
{overdue ? 'Prazo vencido: ' : 'Prazo: '}
|
|
263
|
-
{formatDate(
|
|
264
|
-
task.dueDate,
|
|
265
|
-
getSettingValue,
|
|
266
|
-
currentLocaleCode
|
|
267
|
-
)}
|
|
268
|
-
</TooltipContent>
|
|
269
|
-
</Tooltip>
|
|
270
|
-
) : null}
|
|
271
|
-
|
|
272
|
-
{/* Attachment + comment counts */}
|
|
273
|
-
{hasMeta ? (
|
|
274
|
-
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
275
|
-
{(task.fileCount ?? 0) > 0 ? (
|
|
276
|
-
<Tooltip>
|
|
277
|
-
<TooltipTrigger asChild>
|
|
278
|
-
<span className="inline-flex items-center gap-0.5">
|
|
279
|
-
<Paperclip className="size-3 shrink-0" />
|
|
280
|
-
{task.fileCount}
|
|
281
|
-
</span>
|
|
282
|
-
</TooltipTrigger>
|
|
283
|
-
<TooltipContent side="top">
|
|
284
|
-
{task.fileCount}{' '}
|
|
285
|
-
{task.fileCount === 1 ? 'anexo' : 'anexos'}
|
|
286
|
-
</TooltipContent>
|
|
287
|
-
</Tooltip>
|
|
288
|
-
) : null}
|
|
289
|
-
{(task.commentCount ?? 0) > 0 ? (
|
|
290
|
-
<Tooltip>
|
|
291
|
-
<TooltipTrigger asChild>
|
|
292
|
-
<span className="inline-flex items-center gap-0.5">
|
|
293
|
-
<MessageSquare className="size-3 shrink-0" />
|
|
294
|
-
{task.commentCount}
|
|
295
|
-
</span>
|
|
296
|
-
</TooltipTrigger>
|
|
297
|
-
<TooltipContent side="top">
|
|
298
|
-
{task.commentCount}{' '}
|
|
299
|
-
{task.commentCount === 1
|
|
300
|
-
? 'comentário'
|
|
301
|
-
: 'comentários'}
|
|
302
|
-
</TooltipContent>
|
|
303
|
-
</Tooltip>
|
|
304
|
-
) : null}
|
|
305
|
-
</div>
|
|
306
|
-
) : null}
|
|
307
|
-
</div>
|
|
308
|
-
</div>
|
|
309
|
-
</button>
|
|
310
|
-
);
|
|
311
|
-
})}
|
|
312
|
-
</div>
|
|
313
|
-
|
|
314
|
-
{totalPages > 1 ? (
|
|
315
|
-
<div className="flex items-center justify-between pt-1">
|
|
316
|
-
<span className="text-xs text-muted-foreground">
|
|
317
|
-
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)}{' '}
|
|
318
|
-
/ {total}
|
|
319
|
-
</span>
|
|
320
|
-
<div className="flex gap-1">
|
|
321
|
-
<Button
|
|
322
|
-
type="button"
|
|
323
|
-
variant="outline"
|
|
324
|
-
size="sm"
|
|
325
|
-
className="h-6 px-2 text-xs"
|
|
326
|
-
disabled={page === 0}
|
|
327
|
-
onClick={() => setPage((p) => p - 1)}
|
|
328
|
-
>
|
|
329
|
-
‹
|
|
330
|
-
</Button>
|
|
331
|
-
<Button
|
|
332
|
-
type="button"
|
|
333
|
-
variant="outline"
|
|
334
|
-
size="sm"
|
|
335
|
-
className="h-6 px-2 text-xs"
|
|
336
|
-
disabled={page >= totalPages - 1}
|
|
337
|
-
onClick={() => setPage((p) => p + 1)}
|
|
338
|
-
>
|
|
339
|
-
›
|
|
340
|
-
</Button>
|
|
341
|
-
</div>
|
|
342
|
-
</div>
|
|
343
|
-
) : null}
|
|
344
|
-
</>
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
|
-
<TaskFormSheet
|
|
348
|
-
open={taskFormOpen}
|
|
349
|
-
onOpenChange={setTaskFormOpen}
|
|
350
|
-
request={request}
|
|
351
|
-
editingTask={editingTask}
|
|
352
|
-
defaultAssigneeCollaboratorId={collaboratorId}
|
|
353
|
-
projectRequired={false}
|
|
354
|
-
onSaved={() => void refetchTasks()}
|
|
355
|
-
/>
|
|
356
|
-
</div>
|
|
357
|
-
);
|
|
358
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '@/components/ui/tooltip';
|
|
9
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
10
|
+
import {
|
|
11
|
+
AlignLeft,
|
|
12
|
+
CalendarClock,
|
|
13
|
+
Flag,
|
|
14
|
+
MessageSquare,
|
|
15
|
+
Paperclip,
|
|
16
|
+
Plus,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { useTranslations } from 'next-intl';
|
|
19
|
+
import { useState } from 'react';
|
|
20
|
+
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
21
|
+
import type { OperationsTaskOption, PaginatedResponse } from '../_lib/types';
|
|
22
|
+
import { formatDate } from '../_lib/utils/format';
|
|
23
|
+
import { TaskFormSheet } from './task-form-sheet';
|
|
24
|
+
|
|
25
|
+
const PAGE_SIZE = 10;
|
|
26
|
+
|
|
27
|
+
const TASK_COLUMNS = [
|
|
28
|
+
{ id: 'todo', label: 'Backlog' },
|
|
29
|
+
{ id: 'doing', label: 'Em execução' },
|
|
30
|
+
{ id: 'review', label: 'Revisão' },
|
|
31
|
+
{ id: 'done', label: 'Concluído' },
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
function getPriorityLabel(value?: string | null) {
|
|
35
|
+
const map: Record<string, string> = {
|
|
36
|
+
low: 'Baixa',
|
|
37
|
+
medium: 'Média',
|
|
38
|
+
high: 'Alta',
|
|
39
|
+
};
|
|
40
|
+
return map[value ?? ''] ?? value ?? '—';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getPriorityStripe(value?: string | null) {
|
|
44
|
+
if (value === 'high') return 'bg-rose-500';
|
|
45
|
+
if (value === 'medium') return 'bg-amber-500';
|
|
46
|
+
if (value === 'low') return 'bg-emerald-500';
|
|
47
|
+
return 'bg-border';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPriorityChip(value?: string | null) {
|
|
51
|
+
if (value === 'high')
|
|
52
|
+
return 'bg-rose-500/10 text-rose-600 dark:text-rose-400 ring-1 ring-rose-500/20';
|
|
53
|
+
if (value === 'medium')
|
|
54
|
+
return 'bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-1 ring-amber-500/20';
|
|
55
|
+
return 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 ring-1 ring-emerald-500/20';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getStatusDot(value?: string | null) {
|
|
59
|
+
if (value === 'doing') return 'bg-blue-500';
|
|
60
|
+
if (value === 'review') return 'bg-amber-500';
|
|
61
|
+
if (value === 'done') return 'bg-emerald-500';
|
|
62
|
+
return 'bg-muted-foreground/50';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isDueDateOverdue(dueDate?: string | null) {
|
|
66
|
+
if (!dueDate) return false;
|
|
67
|
+
return new Date(dueDate) < new Date();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type CollaboratorTasksTabProps = {
|
|
71
|
+
collaboratorId: number | null | undefined;
|
|
72
|
+
collaboratorName?: string | null;
|
|
73
|
+
request: Parameters<typeof mutateOperations>[0];
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function CollaboratorTasksTab({
|
|
78
|
+
collaboratorId,
|
|
79
|
+
request,
|
|
80
|
+
disabled,
|
|
81
|
+
}: CollaboratorTasksTabProps) {
|
|
82
|
+
const { getSettingValue, currentLocaleCode } = useApp();
|
|
83
|
+
const commonT = useTranslations('operations.Common');
|
|
84
|
+
const taskT = useTranslations('operations.ProjectDetailsPage');
|
|
85
|
+
|
|
86
|
+
const [page, setPage] = useState(0);
|
|
87
|
+
const [taskFormOpen, setTaskFormOpen] = useState(false);
|
|
88
|
+
const [editingTask, setEditingTask] = useState<OperationsTaskOption | null>(
|
|
89
|
+
null
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const tasksParams = new URLSearchParams({
|
|
93
|
+
page: String(page + 1),
|
|
94
|
+
pageSize: String(PAGE_SIZE),
|
|
95
|
+
});
|
|
96
|
+
if (collaboratorId) tasksParams.set('collaboratorId', String(collaboratorId));
|
|
97
|
+
|
|
98
|
+
const { data: tasksResponse, refetch: refetchTasks } = useQuery<
|
|
99
|
+
PaginatedResponse<OperationsTaskOption>
|
|
100
|
+
>({
|
|
101
|
+
queryKey: ['collaborator-tasks', collaboratorId, page],
|
|
102
|
+
enabled: Boolean(collaboratorId),
|
|
103
|
+
staleTime: 0,
|
|
104
|
+
refetchOnMount: 'always',
|
|
105
|
+
queryFn: () =>
|
|
106
|
+
fetchOperations<PaginatedResponse<OperationsTaskOption>>(
|
|
107
|
+
request,
|
|
108
|
+
`/operations/tasks?${tasksParams.toString()}`
|
|
109
|
+
),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const tasks = tasksResponse?.data ?? [];
|
|
113
|
+
const total = tasksResponse?.total ?? 0;
|
|
114
|
+
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
115
|
+
|
|
116
|
+
const openCreate = () => {
|
|
117
|
+
setEditingTask(null);
|
|
118
|
+
setTaskFormOpen(true);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const openEdit = (task: OperationsTaskOption) => {
|
|
122
|
+
setEditingTask(task);
|
|
123
|
+
setTaskFormOpen(true);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (!collaboratorId) {
|
|
127
|
+
return (
|
|
128
|
+
<p className="text-sm text-muted-foreground">
|
|
129
|
+
{commonT('states.saveBefore')}
|
|
130
|
+
</p>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="space-y-3">
|
|
136
|
+
{!disabled ? (
|
|
137
|
+
<div className="flex items-center justify-between">
|
|
138
|
+
<span className="text-sm text-muted-foreground">
|
|
139
|
+
{total > 0
|
|
140
|
+
? `${total} ${total === 1 ? 'tarefa' : 'tarefas'}`
|
|
141
|
+
: null}
|
|
142
|
+
</span>
|
|
143
|
+
<Button
|
|
144
|
+
type="button"
|
|
145
|
+
variant="outline"
|
|
146
|
+
size="sm"
|
|
147
|
+
className="gap-1.5"
|
|
148
|
+
onClick={openCreate}
|
|
149
|
+
>
|
|
150
|
+
<Plus className="size-3.5" />
|
|
151
|
+
{taskT('taskForm.titleNew')}
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
) : null}
|
|
155
|
+
|
|
156
|
+
{tasks.length === 0 ? (
|
|
157
|
+
<div className="flex flex-col items-center gap-2 py-8 text-center">
|
|
158
|
+
<AlignLeft className="size-8 text-muted-foreground/40" />
|
|
159
|
+
<p className="text-sm text-muted-foreground">
|
|
160
|
+
Nenhuma tarefa encontrada.
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
<div className="space-y-1.5">
|
|
166
|
+
{tasks.map((task) => {
|
|
167
|
+
const overdue =
|
|
168
|
+
isDueDateOverdue(task.dueDate) && task.status !== 'done';
|
|
169
|
+
const columnLabel =
|
|
170
|
+
TASK_COLUMNS.find((c) => c.id === task.status)?.label ??
|
|
171
|
+
task.status;
|
|
172
|
+
const hasMeta =
|
|
173
|
+
(task.fileCount ?? 0) > 0 || (task.commentCount ?? 0) > 0;
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<button
|
|
177
|
+
key={task.id}
|
|
178
|
+
type="button"
|
|
179
|
+
className="group flex w-full cursor-pointer items-stretch overflow-hidden rounded-lg border border-border/50 bg-card text-left shadow-sm transition-all duration-150 hover:border-border hover:shadow-md"
|
|
180
|
+
onClick={() => openEdit(task)}
|
|
181
|
+
>
|
|
182
|
+
{/* Priority stripe */}
|
|
183
|
+
<div
|
|
184
|
+
className={`w-0.75 shrink-0 transition-opacity ${getPriorityStripe(task.priority)}`}
|
|
185
|
+
/>
|
|
186
|
+
|
|
187
|
+
{/* Content */}
|
|
188
|
+
<div className="flex min-w-0 flex-1 items-center gap-3 px-4 py-3">
|
|
189
|
+
{/* Left: name + project */}
|
|
190
|
+
<div className="min-w-0 flex-1">
|
|
191
|
+
<p className="truncate text-sm font-semibold leading-5 text-foreground">
|
|
192
|
+
{task.name}
|
|
193
|
+
</p>
|
|
194
|
+
{task.projectName ? (
|
|
195
|
+
<p className="mt-0.5 truncate text-[11px] leading-none text-muted-foreground/70">
|
|
196
|
+
{[task.projectCode, task.projectName]
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.join(' · ')}
|
|
199
|
+
</p>
|
|
200
|
+
) : null}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Right: indicators */}
|
|
204
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
205
|
+
{/* Priority chip */}
|
|
206
|
+
{task.priority ? (
|
|
207
|
+
<Tooltip>
|
|
208
|
+
<TooltipTrigger asChild>
|
|
209
|
+
<span
|
|
210
|
+
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium ${getPriorityChip(task.priority)}`}
|
|
211
|
+
>
|
|
212
|
+
<Flag className="size-2.5" />
|
|
213
|
+
{getPriorityLabel(task.priority)}
|
|
214
|
+
</span>
|
|
215
|
+
</TooltipTrigger>
|
|
216
|
+
<TooltipContent side="top">
|
|
217
|
+
Prioridade: {getPriorityLabel(task.priority)}
|
|
218
|
+
</TooltipContent>
|
|
219
|
+
</Tooltip>
|
|
220
|
+
) : null}
|
|
221
|
+
|
|
222
|
+
{/* Status / column */}
|
|
223
|
+
<Tooltip>
|
|
224
|
+
<TooltipTrigger asChild>
|
|
225
|
+
<span className="inline-flex items-center gap-1.5 rounded-md bg-muted/60 px-2 py-0.5 text-[11px] font-medium text-foreground/80 ring-1 ring-border/40">
|
|
226
|
+
<span
|
|
227
|
+
className={`size-1.5 shrink-0 rounded-full ${getStatusDot(task.status)}`}
|
|
228
|
+
/>
|
|
229
|
+
{columnLabel}
|
|
230
|
+
</span>
|
|
231
|
+
</TooltipTrigger>
|
|
232
|
+
<TooltipContent side="top">
|
|
233
|
+
Coluna: {columnLabel}
|
|
234
|
+
</TooltipContent>
|
|
235
|
+
</Tooltip>
|
|
236
|
+
|
|
237
|
+
{/* Divider before date/meta */}
|
|
238
|
+
{task.dueDate || hasMeta ? (
|
|
239
|
+
<div className="h-3.5 w-px bg-border/60" />
|
|
240
|
+
) : null}
|
|
241
|
+
|
|
242
|
+
{/* Due date chip */}
|
|
243
|
+
{task.dueDate ? (
|
|
244
|
+
<Tooltip>
|
|
245
|
+
<TooltipTrigger asChild>
|
|
246
|
+
<span
|
|
247
|
+
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] font-medium ${
|
|
248
|
+
overdue
|
|
249
|
+
? 'bg-rose-500/10 text-rose-600 ring-1 ring-rose-500/20 dark:text-rose-400'
|
|
250
|
+
: 'bg-muted/60 text-muted-foreground ring-1 ring-border/40'
|
|
251
|
+
}`}
|
|
252
|
+
>
|
|
253
|
+
<CalendarClock className="size-2.5 shrink-0" />
|
|
254
|
+
{formatDate(
|
|
255
|
+
task.dueDate,
|
|
256
|
+
getSettingValue,
|
|
257
|
+
currentLocaleCode
|
|
258
|
+
)}
|
|
259
|
+
</span>
|
|
260
|
+
</TooltipTrigger>
|
|
261
|
+
<TooltipContent side="top">
|
|
262
|
+
{overdue ? 'Prazo vencido: ' : 'Prazo: '}
|
|
263
|
+
{formatDate(
|
|
264
|
+
task.dueDate,
|
|
265
|
+
getSettingValue,
|
|
266
|
+
currentLocaleCode
|
|
267
|
+
)}
|
|
268
|
+
</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
270
|
+
) : null}
|
|
271
|
+
|
|
272
|
+
{/* Attachment + comment counts */}
|
|
273
|
+
{hasMeta ? (
|
|
274
|
+
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
275
|
+
{(task.fileCount ?? 0) > 0 ? (
|
|
276
|
+
<Tooltip>
|
|
277
|
+
<TooltipTrigger asChild>
|
|
278
|
+
<span className="inline-flex items-center gap-0.5">
|
|
279
|
+
<Paperclip className="size-3 shrink-0" />
|
|
280
|
+
{task.fileCount}
|
|
281
|
+
</span>
|
|
282
|
+
</TooltipTrigger>
|
|
283
|
+
<TooltipContent side="top">
|
|
284
|
+
{task.fileCount}{' '}
|
|
285
|
+
{task.fileCount === 1 ? 'anexo' : 'anexos'}
|
|
286
|
+
</TooltipContent>
|
|
287
|
+
</Tooltip>
|
|
288
|
+
) : null}
|
|
289
|
+
{(task.commentCount ?? 0) > 0 ? (
|
|
290
|
+
<Tooltip>
|
|
291
|
+
<TooltipTrigger asChild>
|
|
292
|
+
<span className="inline-flex items-center gap-0.5">
|
|
293
|
+
<MessageSquare className="size-3 shrink-0" />
|
|
294
|
+
{task.commentCount}
|
|
295
|
+
</span>
|
|
296
|
+
</TooltipTrigger>
|
|
297
|
+
<TooltipContent side="top">
|
|
298
|
+
{task.commentCount}{' '}
|
|
299
|
+
{task.commentCount === 1
|
|
300
|
+
? 'comentário'
|
|
301
|
+
: 'comentários'}
|
|
302
|
+
</TooltipContent>
|
|
303
|
+
</Tooltip>
|
|
304
|
+
) : null}
|
|
305
|
+
</div>
|
|
306
|
+
) : null}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</button>
|
|
310
|
+
);
|
|
311
|
+
})}
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{totalPages > 1 ? (
|
|
315
|
+
<div className="flex items-center justify-between pt-1">
|
|
316
|
+
<span className="text-xs text-muted-foreground">
|
|
317
|
+
{page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)}{' '}
|
|
318
|
+
/ {total}
|
|
319
|
+
</span>
|
|
320
|
+
<div className="flex gap-1">
|
|
321
|
+
<Button
|
|
322
|
+
type="button"
|
|
323
|
+
variant="outline"
|
|
324
|
+
size="sm"
|
|
325
|
+
className="h-6 px-2 text-xs"
|
|
326
|
+
disabled={page === 0}
|
|
327
|
+
onClick={() => setPage((p) => p - 1)}
|
|
328
|
+
>
|
|
329
|
+
‹
|
|
330
|
+
</Button>
|
|
331
|
+
<Button
|
|
332
|
+
type="button"
|
|
333
|
+
variant="outline"
|
|
334
|
+
size="sm"
|
|
335
|
+
className="h-6 px-2 text-xs"
|
|
336
|
+
disabled={page >= totalPages - 1}
|
|
337
|
+
onClick={() => setPage((p) => p + 1)}
|
|
338
|
+
>
|
|
339
|
+
›
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
) : null}
|
|
344
|
+
</>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
<TaskFormSheet
|
|
348
|
+
open={taskFormOpen}
|
|
349
|
+
onOpenChange={setTaskFormOpen}
|
|
350
|
+
request={request}
|
|
351
|
+
editingTask={editingTask}
|
|
352
|
+
defaultAssigneeCollaboratorId={collaboratorId}
|
|
353
|
+
projectRequired={false}
|
|
354
|
+
onSaved={() => void refetchTasks()}
|
|
355
|
+
/>
|
|
356
|
+
</div>
|
|
357
|
+
);
|
|
358
|
+
}
|