@hed-hog/operations 0.0.299 → 0.0.301

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 (97) hide show
  1. package/dist/operations.controller.d.ts +713 -31
  2. package/dist/operations.controller.d.ts.map +1 -1
  3. package/dist/operations.controller.js +157 -0
  4. package/dist/operations.controller.js.map +1 -1
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +5 -1
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.proposal.subscriber.d.ts +11 -0
  9. package/dist/operations.proposal.subscriber.d.ts.map +1 -0
  10. package/dist/operations.proposal.subscriber.js +80 -0
  11. package/dist/operations.proposal.subscriber.js.map +1 -0
  12. package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
  13. package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
  14. package/dist/operations.proposal.subscriber.spec.js +88 -0
  15. package/dist/operations.proposal.subscriber.spec.js.map +1 -0
  16. package/dist/operations.service.d.ts +490 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +3590 -1267
  19. package/dist/operations.service.js.map +1 -1
  20. package/dist/operations.service.spec.d.ts +2 -0
  21. package/dist/operations.service.spec.d.ts.map +1 -0
  22. package/dist/operations.service.spec.js +159 -0
  23. package/dist/operations.service.spec.js.map +1 -0
  24. package/hedhog/data/menu.yaml +232 -198
  25. package/hedhog/data/role.yaml +23 -23
  26. package/hedhog/data/role_route.yaml +39 -0
  27. package/hedhog/data/route.yaml +447 -317
  28. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  29. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  30. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  31. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  32. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  33. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  34. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  35. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  36. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  37. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  38. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  39. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  40. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  41. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  42. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  43. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  44. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  45. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  46. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  48. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  49. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  51. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  52. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  53. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  54. package/hedhog/frontend/app/page.tsx.ejs +36 -12
  55. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  59. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  60. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  61. package/hedhog/frontend/messages/en.json +473 -12
  62. package/hedhog/frontend/messages/pt.json +528 -66
  63. package/hedhog/table/operations_approval.yaml +49 -49
  64. package/hedhog/table/operations_approval_history.yaml +29 -29
  65. package/hedhog/table/operations_collaborator.yaml +87 -67
  66. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -34
  67. package/hedhog/table/operations_contract.yaml +121 -100
  68. package/hedhog/table/operations_contract_document.yaml +40 -23
  69. package/hedhog/table/operations_contract_financial_term.yaml +40 -40
  70. package/hedhog/table/operations_contract_history.yaml +27 -27
  71. package/hedhog/table/operations_contract_party.yaml +46 -46
  72. package/hedhog/table/operations_contract_revision.yaml +38 -38
  73. package/hedhog/table/operations_contract_signature.yaml +38 -38
  74. package/hedhog/table/operations_contract_template.yaml +58 -0
  75. package/hedhog/table/operations_department.yaml +24 -0
  76. package/hedhog/table/operations_project.yaml +54 -54
  77. package/hedhog/table/operations_project_assignment.yaml +55 -55
  78. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -34
  79. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -53
  80. package/hedhog/table/operations_time_off_request.yaml +57 -57
  81. package/hedhog/table/operations_timesheet.yaml +41 -41
  82. package/hedhog/table/operations_timesheet_entry.yaml +40 -40
  83. package/package.json +5 -3
  84. package/src/operations.controller.ts +304 -182
  85. package/src/operations.module.ts +26 -22
  86. package/src/operations.proposal.subscriber.spec.ts +121 -0
  87. package/src/operations.proposal.subscriber.ts +86 -0
  88. package/src/operations.service.spec.ts +210 -0
  89. package/src/operations.service.ts +7317 -3595
  90. package/dist/operations-data.controller.d.ts +0 -139
  91. package/dist/operations-data.controller.d.ts.map +0 -1
  92. package/dist/operations-data.controller.js +0 -113
  93. package/dist/operations-data.controller.js.map +0 -1
  94. package/dist/operations-growth.controller.d.ts +0 -48
  95. package/dist/operations-growth.controller.d.ts.map +0 -1
  96. package/dist/operations-growth.controller.js +0 -90
  97. package/dist/operations-growth.controller.js.map +0 -1
@@ -0,0 +1,442 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent } from '@/components/ui/card';
6
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
7
+ import {
8
+ Sheet,
9
+ SheetContent,
10
+ SheetDescription,
11
+ SheetHeader,
12
+ SheetTitle,
13
+ } from '@/components/ui/sheet';
14
+ import {
15
+ Table,
16
+ TableBody,
17
+ TableCell,
18
+ TableHead,
19
+ TableHeader,
20
+ TableRow,
21
+ } from '@/components/ui/table';
22
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
23
+ import { Building2, Pencil, Users } from 'lucide-react';
24
+ import { useTranslations } from 'next-intl';
25
+ import Link from 'next/link';
26
+ import { useMemo, useState } from 'react';
27
+ import { OperationsHeader } from '../_components/operations-header';
28
+ import { StatusBadge } from '../_components/status-badge';
29
+ import { fetchOperations, mutateOperations } from '../_lib/api';
30
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
31
+ import type { OperationsDepartment } from '../_lib/types';
32
+ import { formatEnumLabel, getStatusBadgeClass } from '../_lib/utils/format';
33
+
34
+ type DepartmentFormState = {
35
+ name: string;
36
+ code: string;
37
+ description: string;
38
+ status: 'active' | 'inactive';
39
+ };
40
+
41
+ const EMPTY_FORM_STATE: DepartmentFormState = {
42
+ name: '',
43
+ code: '',
44
+ description: '',
45
+ status: 'active',
46
+ };
47
+
48
+ export default function OperationsDepartmentsPage() {
49
+ const t = useTranslations('operations.DepartmentsPage');
50
+ const commonT = useTranslations('operations.Common');
51
+ const { request, showToastHandler, currentLocaleCode } = useApp();
52
+ const access = useOperationsAccess();
53
+ const [search, setSearch] = useState('');
54
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all');
55
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
56
+ const [isSaving, setIsSaving] = useState(false);
57
+ const [editingDepartment, setEditingDepartment] = useState<OperationsDepartment | null>(null);
58
+ const [form, setForm] = useState<DepartmentFormState>(EMPTY_FORM_STATE);
59
+
60
+ const {
61
+ data: departments = [],
62
+ isLoading,
63
+ refetch,
64
+ } = useQuery<OperationsDepartment[]>({
65
+ queryKey: ['operations-departments-list', currentLocaleCode],
66
+ enabled: access.isDirector,
67
+ queryFn: () =>
68
+ fetchOperations<OperationsDepartment[]>(request, '/operations/departments'),
69
+ });
70
+
71
+ const filteredDepartments = useMemo(
72
+ () =>
73
+ departments.filter((department) => {
74
+ const matchesSearch = !search.trim()
75
+ ? true
76
+ : [department.name, department.code, department.description]
77
+ .filter(Boolean)
78
+ .some((value) =>
79
+ String(value)
80
+ .toLowerCase()
81
+ .includes(search.trim().toLowerCase())
82
+ );
83
+
84
+ const matchesStatus =
85
+ statusFilter === 'all' ? true : department.status === statusFilter;
86
+
87
+ return matchesSearch && matchesStatus;
88
+ }),
89
+ [departments, search, statusFilter]
90
+ );
91
+
92
+ const statsCards = useMemo(
93
+ () => [
94
+ {
95
+ key: 'total',
96
+ title: t('cards.total'),
97
+ value: departments.length,
98
+ icon: Building2,
99
+ },
100
+ {
101
+ key: 'active',
102
+ title: t('cards.active'),
103
+ value: departments.filter((item) => item.status === 'active').length,
104
+ icon: Building2,
105
+ },
106
+ {
107
+ key: 'linkedCollaborators',
108
+ title: t('cards.linkedCollaborators'),
109
+ value: departments.reduce(
110
+ (total, item) => total + Number(item.collaboratorCount ?? 0),
111
+ 0
112
+ ),
113
+ icon: Users,
114
+ },
115
+ {
116
+ key: 'inactive',
117
+ title: t('cards.inactive'),
118
+ value: departments.filter((item) => item.status === 'inactive').length,
119
+ icon: Building2,
120
+ },
121
+ ],
122
+ [departments, t]
123
+ );
124
+
125
+ const updateForm = <K extends keyof DepartmentFormState>(
126
+ field: K,
127
+ value: DepartmentFormState[K]
128
+ ) => {
129
+ setForm((current) => ({
130
+ ...current,
131
+ [field]: value,
132
+ }));
133
+ };
134
+
135
+ const openCreateSheet = () => {
136
+ setEditingDepartment(null);
137
+ setForm(EMPTY_FORM_STATE);
138
+ setIsSheetOpen(true);
139
+ };
140
+
141
+ const openEditSheet = (department: OperationsDepartment) => {
142
+ setEditingDepartment(department);
143
+ setForm({
144
+ name: department.name ?? '',
145
+ code: department.code ?? '',
146
+ description: department.description ?? '',
147
+ status: department.status ?? 'active',
148
+ });
149
+ setIsSheetOpen(true);
150
+ };
151
+
152
+ const saveDepartment = async () => {
153
+ if (!form.name.trim()) {
154
+ showToastHandler?.('error', t('messages.requiredFields'));
155
+ return;
156
+ }
157
+
158
+ setIsSaving(true);
159
+
160
+ try {
161
+ await mutateOperations(
162
+ request,
163
+ editingDepartment
164
+ ? `/operations/departments/${editingDepartment.id}`
165
+ : '/operations/departments',
166
+ editingDepartment ? 'PATCH' : 'POST',
167
+ {
168
+ name: form.name.trim(),
169
+ code: form.code.trim() || null,
170
+ description: form.description.trim() || null,
171
+ status: form.status,
172
+ }
173
+ );
174
+
175
+ showToastHandler?.('success', t('messages.saveSuccess'));
176
+ setIsSheetOpen(false);
177
+ setEditingDepartment(null);
178
+ setForm(EMPTY_FORM_STATE);
179
+ await refetch();
180
+ } catch {
181
+ showToastHandler?.('error', t('messages.saveError'));
182
+ } finally {
183
+ setIsSaving(false);
184
+ }
185
+ };
186
+
187
+ const toggleStatus = async (department: OperationsDepartment) => {
188
+ const nextStatus = department.status === 'inactive' ? 'active' : 'inactive';
189
+
190
+ try {
191
+ await mutateOperations(
192
+ request,
193
+ `/operations/departments/${department.id}`,
194
+ 'PATCH',
195
+ { status: nextStatus }
196
+ );
197
+ showToastHandler?.('success', t('messages.statusSuccess'));
198
+ await refetch();
199
+ } catch {
200
+ showToastHandler?.('error', t('messages.statusError'));
201
+ }
202
+ };
203
+
204
+ if (!access.isLoading && !access.isDirector) {
205
+ return (
206
+ <Page>
207
+ <OperationsHeader
208
+ title={t('title')}
209
+ description={t('description')}
210
+ current={t('breadcrumb')}
211
+ actions={
212
+ <Button variant="outline" size="sm" asChild>
213
+ <Link href="/operations/collaborators">{commonT('actions.back')}</Link>
214
+ </Button>
215
+ }
216
+ />
217
+
218
+ <EmptyState
219
+ icon={<Building2 className="size-12" />}
220
+ title={commonT('states.noAccessTitle')}
221
+ description={t('noAccessDescription')}
222
+ actionLabel={commonT('actions.back')}
223
+ onAction={() => {
224
+ window.location.href = '/operations';
225
+ }}
226
+ />
227
+ </Page>
228
+ );
229
+ }
230
+
231
+ return (
232
+ <Page>
233
+ <OperationsHeader
234
+ title={t('title')}
235
+ description={t('description')}
236
+ current={t('breadcrumb')}
237
+ actions={
238
+ <div className="flex flex-wrap gap-2">
239
+ <Button variant="outline" size="sm" asChild>
240
+ <Link href="/operations/collaborators">{commonT('actions.back')}</Link>
241
+ </Button>
242
+ <Button size="sm" onClick={openCreateSheet}>
243
+ {commonT('actions.create')}
244
+ </Button>
245
+ </div>
246
+ }
247
+ />
248
+
249
+ <KpiCardsGrid items={statsCards} columns={4} />
250
+
251
+ <div className="flex flex-col gap-4">
252
+ <SearchBar
253
+ searchQuery={search}
254
+ onSearchChange={setSearch}
255
+ onSearch={() => undefined}
256
+ placeholder={t('searchPlaceholder')}
257
+ controls={[
258
+ {
259
+ id: 'status',
260
+ type: 'select',
261
+ value: statusFilter,
262
+ onChange: (value) =>
263
+ setStatusFilter((value as 'all' | 'active' | 'inactive') ?? 'all'),
264
+ placeholder: commonT('labels.status'),
265
+ options: [
266
+ { value: 'all', label: commonT('filters.allStatuses') },
267
+ { value: 'active', label: formatEnumLabel('active') },
268
+ { value: 'inactive', label: formatEnumLabel('inactive') },
269
+ ],
270
+ },
271
+ ]}
272
+ />
273
+ </div>
274
+
275
+ {isLoading ? (
276
+ <Card className="py-0">
277
+ <CardContent className="p-6 text-sm text-muted-foreground">
278
+ {commonT('actions.refresh')}...
279
+ </CardContent>
280
+ </Card>
281
+ ) : filteredDepartments.length > 0 ? (
282
+ <Card className="py-0">
283
+ <CardContent className="p-0">
284
+ <div className="overflow-x-auto rounded-md border">
285
+ <Table>
286
+ <TableHeader>
287
+ <TableRow>
288
+ <TableHead>{t('columns.name')}</TableHead>
289
+ <TableHead>{t('columns.code')}</TableHead>
290
+ <TableHead>{t('columns.description')}</TableHead>
291
+ <TableHead>{t('columns.collaborators')}</TableHead>
292
+ <TableHead>{commonT('labels.status')}</TableHead>
293
+ <TableHead className="text-right">{commonT('labels.actions')}</TableHead>
294
+ </TableRow>
295
+ </TableHeader>
296
+ <TableBody>
297
+ {filteredDepartments.map((department) => (
298
+ <TableRow key={department.id}>
299
+ <TableCell className="font-medium">{department.name}</TableCell>
300
+ <TableCell>{department.code || '—'}</TableCell>
301
+ <TableCell className="max-w-md text-sm text-muted-foreground">
302
+ {department.description || commonT('labels.notAvailable')}
303
+ </TableCell>
304
+ <TableCell>{Number(department.collaboratorCount ?? 0)}</TableCell>
305
+ <TableCell>
306
+ <StatusBadge
307
+ label={formatEnumLabel(department.status)}
308
+ className={getStatusBadgeClass(department.status)}
309
+ />
310
+ </TableCell>
311
+ <TableCell>
312
+ <div className="flex justify-end gap-2">
313
+ <Button
314
+ variant="outline"
315
+ size="icon"
316
+ className="cursor-pointer"
317
+ onClick={() => openEditSheet(department)}
318
+ >
319
+ <Pencil className="size-4" />
320
+ </Button>
321
+ <Button
322
+ variant="outline"
323
+ size="sm"
324
+ className="cursor-pointer"
325
+ onClick={() => void toggleStatus(department)}
326
+ >
327
+ {department.status === 'inactive'
328
+ ? commonT('actions.activate')
329
+ : commonT('actions.deactivate')}
330
+ </Button>
331
+ </div>
332
+ </TableCell>
333
+ </TableRow>
334
+ ))}
335
+ </TableBody>
336
+ </Table>
337
+ </div>
338
+ </CardContent>
339
+ </Card>
340
+ ) : (
341
+ <EmptyState
342
+ icon={<Building2 className="size-12" />}
343
+ title={commonT('states.emptyTitle')}
344
+ description={t('emptyDescription')}
345
+ actionLabel={commonT('actions.create')}
346
+ onAction={openCreateSheet}
347
+ />
348
+ )}
349
+
350
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
351
+ <SheetContent className="w-full overflow-y-auto sm:max-w-xl">
352
+ <SheetHeader>
353
+ <SheetTitle>
354
+ {editingDepartment ? t('sheet.editTitle') : t('sheet.createTitle')}
355
+ </SheetTitle>
356
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
357
+ </SheetHeader>
358
+
359
+ <form
360
+ className="mt-6 space-y-4"
361
+ onSubmit={(event) => {
362
+ event.preventDefault();
363
+ void saveDepartment();
364
+ }}
365
+ >
366
+ <div className="space-y-2">
367
+ <label className="text-sm font-medium" htmlFor="department-name">
368
+ {t('form.name')}
369
+ </label>
370
+ <input
371
+ id="department-name"
372
+ value={form.name}
373
+ onChange={(event) => updateForm('name', event.target.value)}
374
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
375
+ placeholder={t('form.name')}
376
+ />
377
+ </div>
378
+
379
+ <div className="space-y-2">
380
+ <label className="text-sm font-medium" htmlFor="department-code">
381
+ {t('form.code')}
382
+ </label>
383
+ <input
384
+ id="department-code"
385
+ value={form.code}
386
+ onChange={(event) => updateForm('code', event.target.value)}
387
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
388
+ placeholder={t('form.code')}
389
+ />
390
+ </div>
391
+
392
+ <div className="space-y-2">
393
+ <label className="text-sm font-medium" htmlFor="department-description">
394
+ {t('form.description')}
395
+ </label>
396
+ <textarea
397
+ id="department-description"
398
+ value={form.description}
399
+ onChange={(event) => updateForm('description', event.target.value)}
400
+ className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
401
+ placeholder={t('form.description')}
402
+ />
403
+ </div>
404
+
405
+ <div className="space-y-2">
406
+ <label className="text-sm font-medium" htmlFor="department-status">
407
+ {t('form.status')}
408
+ </label>
409
+ <select
410
+ id="department-status"
411
+ value={form.status}
412
+ onChange={(event) =>
413
+ updateForm(
414
+ 'status',
415
+ event.target.value === 'inactive' ? 'inactive' : 'active'
416
+ )
417
+ }
418
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
419
+ >
420
+ <option value="active">{formatEnumLabel('active')}</option>
421
+ <option value="inactive">{formatEnumLabel('inactive')}</option>
422
+ </select>
423
+ </div>
424
+
425
+ <div className="flex justify-end gap-2 pt-2">
426
+ <Button
427
+ type="button"
428
+ variant="outline"
429
+ onClick={() => setIsSheetOpen(false)}
430
+ >
431
+ {commonT('actions.cancel')}
432
+ </Button>
433
+ <Button type="submit" disabled={isSaving}>
434
+ {commonT('actions.save')}
435
+ </Button>
436
+ </div>
437
+ </form>
438
+ </SheetContent>
439
+ </Sheet>
440
+ </Page>
441
+ );
442
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from '@/components/ui/table';
15
15
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
16
16
  import {
17
+ Building2,
17
18
  CheckCircle2,
18
19
  ClipboardList,
19
20
  FolderKanban,
@@ -22,8 +23,8 @@ import {
22
23
  RefreshCcw,
23
24
  Users,
24
25
  } from 'lucide-react';
25
- import Link from 'next/link';
26
26
  import { useTranslations } from 'next-intl';
27
+ import Link from 'next/link';
27
28
  import { OperationsHeader } from './_components/operations-header';
28
29
  import { SectionCard } from './_components/section-card';
29
30
  import { StatusBadge } from './_components/status-badge';
@@ -31,7 +32,6 @@ import { fetchOperations } from './_lib/api';
31
32
  import { useOperationsAccess } from './_lib/hooks/use-operations-access';
32
33
  import type { OperationsDashboard, OperationsTeamOverview } from './_lib/types';
33
34
  import {
34
- formatDate,
35
35
  formatDateRange,
36
36
  formatEnumLabel,
37
37
  formatHours,
@@ -50,13 +50,15 @@ export default function OperationsDashboardPage() {
50
50
  refetch,
51
51
  } = useQuery<OperationsDashboard>({
52
52
  queryKey: ['operations-dashboard', currentLocaleCode],
53
- queryFn: () => fetchOperations<OperationsDashboard>(request, '/operations/dashboard'),
53
+ queryFn: () =>
54
+ fetchOperations<OperationsDashboard>(request, '/operations/dashboard'),
54
55
  });
55
56
 
56
57
  const { data: team } = useQuery<OperationsTeamOverview>({
57
58
  queryKey: ['operations-team-overview', currentLocaleCode],
58
59
  enabled: access.isSupervisor,
59
- queryFn: () => fetchOperations<OperationsTeamOverview>(request, '/operations/team'),
60
+ queryFn: () =>
61
+ fetchOperations<OperationsTeamOverview>(request, '/operations/team'),
60
62
  });
61
63
 
62
64
  const cards = dashboard
@@ -103,13 +105,23 @@ export default function OperationsDashboardPage() {
103
105
  description={t('description')}
104
106
  current={t('breadcrumb')}
105
107
  actions={
106
- <div className="flex gap-2">
108
+ <div className="flex flex-wrap gap-2">
107
109
  <Button variant="outline" size="sm" onClick={() => void refetch()}>
108
110
  <RefreshCcw className="size-4" />
109
111
  {commonT('actions.refresh')}
110
112
  </Button>
113
+ {access.isDirector ? (
114
+ <Button variant="outline" size="sm" asChild>
115
+ <Link href="/operations/departments">
116
+ <Building2 className="size-4" />
117
+ {commonT('actions.manageDepartments')}
118
+ </Link>
119
+ </Button>
120
+ ) : null}
111
121
  <Button asChild size="sm">
112
- <Link href="/operations/timesheets">{t('actions.openTimesheets')}</Link>
122
+ <Link href="/operations/timesheets">
123
+ {t('actions.openTimesheets')}
124
+ </Link>
113
125
  </Button>
114
126
  </div>
115
127
  }
@@ -139,19 +151,26 @@ export default function OperationsDashboardPage() {
139
151
  >
140
152
  <dl className="space-y-3 text-sm">
141
153
  <div className="flex items-center justify-between gap-4">
142
- <dt className="text-muted-foreground">{t('scope.roleScope')}</dt>
154
+ <dt className="text-muted-foreground">
155
+ {t('scope.roleScope')}
156
+ </dt>
143
157
  <dd className="font-medium">
144
158
  {formatEnumLabel(dashboard.actor.roleScope)}
145
159
  </dd>
146
160
  </div>
147
161
  <div className="flex items-center justify-between gap-4">
148
- <dt className="text-muted-foreground">{t('scope.collaborator')}</dt>
162
+ <dt className="text-muted-foreground">
163
+ {t('scope.collaborator')}
164
+ </dt>
149
165
  <dd className="font-medium">
150
- {dashboard.actor.collaboratorName ?? commonT('labels.notAvailable')}
166
+ {dashboard.actor.collaboratorName ??
167
+ commonT('labels.notAvailable')}
151
168
  </dd>
152
169
  </div>
153
170
  <div className="flex items-center justify-between gap-4">
154
- <dt className="text-muted-foreground">{t('scope.teamSize')}</dt>
171
+ <dt className="text-muted-foreground">
172
+ {t('scope.teamSize')}
173
+ </dt>
155
174
  <dd className="font-medium">{dashboard.actor.teamSize}</dd>
156
175
  </div>
157
176
  </dl>
@@ -190,7 +209,10 @@ export default function OperationsDashboardPage() {
190
209
  {item.collaboratorName}
191
210
  </TableCell>
192
211
  <TableCell>
193
- {formatDateRange(item.weekStartDate, item.weekEndDate)}
212
+ {formatDateRange(
213
+ item.weekStartDate,
214
+ item.weekEndDate
215
+ )}
194
216
  </TableCell>
195
217
  <TableCell>{formatHours(item.totalHours)}</TableCell>
196
218
  <TableCell>
@@ -270,7 +292,9 @@ export default function OperationsDashboardPage() {
270
292
  className="flex items-center justify-between rounded-lg border px-4 py-3 text-sm"
271
293
  >
272
294
  <div>
273
- <div className="font-medium">{member.displayName}</div>
295
+ <div className="font-medium">
296
+ {member.displayName}
297
+ </div>
274
298
  <div className="text-muted-foreground">
275
299
  {[member.department, member.title]
276
300
  .filter(Boolean)
@@ -1,4 +1,4 @@
1
- import { ProjectFormScreen } from '../../../_components/project-form-screen';
1
+ import { redirect } from 'next/navigation';
2
2
 
3
3
  export default async function OperationsProjectEditPage({
4
4
  params,
@@ -7,5 +7,5 @@ export default async function OperationsProjectEditPage({
7
7
  }) {
8
8
  const { id } = await params;
9
9
 
10
- return <ProjectFormScreen projectId={Number(id)} />;
10
+ redirect(`/operations/projects?edit=${id}`);
11
11
  }
@@ -1,5 +1,5 @@
1
- import { ProjectFormScreen } from '../../_components/project-form-screen';
1
+ import { redirect } from 'next/navigation';
2
2
 
3
3
  export default function OperationsProjectCreatePage() {
4
- return <ProjectFormScreen />;
4
+ redirect('/operations/projects?create=1');
5
5
  }