@hed-hog/operations 0.0.300 → 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.
- package/dist/operations.controller.d.ts +713 -31
- package/dist/operations.controller.d.ts.map +1 -1
- package/dist/operations.controller.js +157 -0
- package/dist/operations.controller.js.map +1 -1
- package/dist/operations.module.d.ts.map +1 -1
- package/dist/operations.module.js +5 -1
- package/dist/operations.module.js.map +1 -1
- package/dist/operations.proposal.subscriber.d.ts +11 -0
- package/dist/operations.proposal.subscriber.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.js +80 -0
- package/dist/operations.proposal.subscriber.js.map +1 -0
- package/dist/operations.proposal.subscriber.spec.d.ts +2 -0
- package/dist/operations.proposal.subscriber.spec.d.ts.map +1 -0
- package/dist/operations.proposal.subscriber.spec.js +88 -0
- package/dist/operations.proposal.subscriber.spec.js.map +1 -0
- package/dist/operations.service.d.ts +490 -46
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +2442 -119
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.d.ts +2 -0
- package/dist/operations.service.spec.d.ts.map +1 -0
- package/dist/operations.service.spec.js +159 -0
- package/dist/operations.service.spec.js.map +1 -0
- package/hedhog/data/menu.yaml +34 -0
- package/hedhog/data/role_route.yaml +39 -0
- package/hedhog/data/route.yaml +130 -0
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
- package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
- package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
- package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
- package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
- package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
- package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
- package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
- package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
- package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
- package/hedhog/frontend/app/page.tsx.ejs +36 -12
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
- package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
- package/hedhog/frontend/messages/en.json +473 -12
- package/hedhog/frontend/messages/pt.json +528 -66
- package/hedhog/table/operations_collaborator.yaml +20 -0
- package/hedhog/table/operations_contract.yaml +22 -1
- package/hedhog/table/operations_contract_document.yaml +33 -16
- package/hedhog/table/operations_contract_template.yaml +58 -0
- package/hedhog/table/operations_department.yaml +24 -0
- package/package.json +6 -4
- package/src/operations.controller.ts +122 -0
- package/src/operations.module.ts +6 -2
- package/src/operations.proposal.subscriber.spec.ts +121 -0
- package/src/operations.proposal.subscriber.ts +86 -0
- package/src/operations.service.spec.ts +210 -0
- package/src/operations.service.ts +3934 -212
|
@@ -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: () =>
|
|
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: () =>
|
|
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">
|
|
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">
|
|
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">
|
|
162
|
+
<dt className="text-muted-foreground">
|
|
163
|
+
{t('scope.collaborator')}
|
|
164
|
+
</dt>
|
|
149
165
|
<dd className="font-medium">
|
|
150
|
-
{dashboard.actor.collaboratorName ??
|
|
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">
|
|
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(
|
|
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">
|
|
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 {
|
|
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
|
-
|
|
10
|
+
redirect(`/operations/projects?edit=${id}`);
|
|
11
11
|
}
|