@hed-hog/operations 0.0.300 → 0.0.302

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 (73) 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 +491 -46
  17. package/dist/operations.service.d.ts.map +1 -1
  18. package/dist/operations.service.js +2484 -121
  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 +35 -22
  25. package/hedhog/data/role_route.yaml +39 -0
  26. package/hedhog/data/route.yaml +130 -0
  27. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +8 -6
  28. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +1163 -327
  29. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -0
  30. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  31. package/hedhog/frontend/app/_components/contract-creation-wizard.tsx.ejs +631 -0
  32. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +353 -27
  33. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +1926 -87
  34. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +526 -0
  35. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -0
  36. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -0
  37. package/hedhog/frontend/app/_components/department-select-with-create.tsx.ejs +370 -0
  38. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +826 -0
  39. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +1251 -364
  40. package/hedhog/frontend/app/_components/section-card.tsx.ejs +48 -13
  41. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -5
  42. package/hedhog/frontend/app/_lib/types.ts.ejs +76 -33
  43. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +85 -8
  44. package/hedhog/frontend/app/approvals/page.tsx.ejs +90 -54
  45. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +2 -2
  46. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +2 -2
  47. package/hedhog/frontend/app/collaborators/page.tsx.ejs +597 -140
  48. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +2 -2
  49. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +2 -2
  50. package/hedhog/frontend/app/contracts/page.tsx.ejs +941 -262
  51. package/hedhog/frontend/app/contracts/templates/page.tsx.ejs +384 -0
  52. package/hedhog/frontend/app/departments/page.tsx.ejs +442 -0
  53. package/hedhog/frontend/app/page.tsx.ejs +3 -317
  54. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +2 -2
  55. package/hedhog/frontend/app/projects/new/page.tsx.ejs +2 -2
  56. package/hedhog/frontend/app/projects/page.tsx.ejs +264 -102
  57. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +50 -28
  58. package/hedhog/frontend/app/time-off/page.tsx.ejs +57 -31
  59. package/hedhog/frontend/app/timesheets/page.tsx.ejs +85 -42
  60. package/hedhog/frontend/messages/en.json +473 -12
  61. package/hedhog/frontend/messages/pt.json +528 -66
  62. package/hedhog/table/operations_collaborator.yaml +20 -0
  63. package/hedhog/table/operations_contract.yaml +22 -1
  64. package/hedhog/table/operations_contract_document.yaml +33 -16
  65. package/hedhog/table/operations_contract_template.yaml +58 -0
  66. package/hedhog/table/operations_department.yaml +24 -0
  67. package/package.json +7 -5
  68. package/src/operations.controller.ts +122 -0
  69. package/src/operations.module.ts +6 -2
  70. package/src/operations.proposal.subscriber.spec.ts +121 -0
  71. package/src/operations.proposal.subscriber.ts +86 -0
  72. package/src/operations.service.spec.ts +210 -0
  73. package/src/operations.service.ts +4026 -241
@@ -0,0 +1,384 @@
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 { FileStack, Pencil, Sparkles } from 'lucide-react';
24
+ import { useTranslations } from 'next-intl';
25
+ import Link from 'next/link';
26
+ import { useMemo, useState } from 'react';
27
+ import { ContractTemplateFormScreen } from '../../_components/contract-template-form-screen';
28
+ import { OperationsHeader } from '../../_components/operations-header';
29
+ import { StatusBadge } from '../../_components/status-badge';
30
+ import { fetchOperations, mutateOperations } from '../../_lib/api';
31
+ import { useOperationsAccess } from '../../_lib/hooks/use-operations-access';
32
+ import type { OperationsContractTemplate } from '../../_lib/types';
33
+ import {
34
+ formatDate,
35
+ formatEnumLabel,
36
+ getStatusBadgeClass,
37
+ } from '../../_lib/utils/format';
38
+
39
+ export default function OperationsContractTemplatesPage() {
40
+ const t = useTranslations('operations.ContractTemplatesPage');
41
+ const commonT = useTranslations('operations.Common');
42
+ const { request, showToastHandler, currentLocaleCode } = useApp();
43
+ const access = useOperationsAccess();
44
+ const [search, setSearch] = useState('');
45
+ const [statusFilter, setStatusFilter] = useState('all');
46
+ const [typeFilter, setTypeFilter] = useState('all');
47
+ const [editingTemplate, setEditingTemplate] =
48
+ useState<OperationsContractTemplate | null>(null);
49
+ const [isSheetOpen, setIsSheetOpen] = useState(false);
50
+
51
+ const {
52
+ data: templates = [],
53
+ isLoading,
54
+ refetch,
55
+ } = useQuery<OperationsContractTemplate[]>({
56
+ queryKey: ['operations-contract-templates-list', currentLocaleCode],
57
+ enabled: access.isDirector,
58
+ queryFn: () =>
59
+ fetchOperations<OperationsContractTemplate[]>(
60
+ request,
61
+ '/operations/contract-templates'
62
+ ),
63
+ });
64
+
65
+ const filteredTemplates = useMemo(
66
+ () =>
67
+ templates.filter((template) => {
68
+ const matchesSearch = !search.trim()
69
+ ? true
70
+ : [
71
+ template.name,
72
+ template.code,
73
+ template.description,
74
+ template.contractType,
75
+ ]
76
+ .filter(Boolean)
77
+ .some((value) =>
78
+ String(value)
79
+ .toLowerCase()
80
+ .includes(search.trim().toLowerCase())
81
+ );
82
+
83
+ const matchesStatus =
84
+ statusFilter === 'all' ? true : template.status === statusFilter;
85
+ const matchesType =
86
+ typeFilter === 'all' ? true : template.contractType === typeFilter;
87
+
88
+ return matchesSearch && matchesStatus && matchesType;
89
+ }),
90
+ [search, statusFilter, templates, typeFilter]
91
+ );
92
+
93
+ const statsCards = useMemo(
94
+ () => [
95
+ {
96
+ key: 'total',
97
+ title: t('cards.total'),
98
+ value: templates.length,
99
+ icon: FileStack,
100
+ },
101
+ {
102
+ key: 'active',
103
+ title: t('cards.active'),
104
+ value: templates.filter((item) => item.status === 'active').length,
105
+ icon: Sparkles,
106
+ },
107
+ {
108
+ key: 'draft',
109
+ title: t('cards.draft'),
110
+ value: templates.filter((item) => item.status === 'draft').length,
111
+ icon: FileStack,
112
+ },
113
+ {
114
+ key: 'archived',
115
+ title: t('cards.archived'),
116
+ value: templates.filter((item) => item.status === 'archived').length,
117
+ icon: FileStack,
118
+ },
119
+ ],
120
+ [t, templates]
121
+ );
122
+
123
+ const openCreateSheet = () => {
124
+ setEditingTemplate(null);
125
+ setIsSheetOpen(true);
126
+ };
127
+
128
+ const openEditSheet = (template: OperationsContractTemplate) => {
129
+ setEditingTemplate(template);
130
+ setIsSheetOpen(true);
131
+ };
132
+
133
+ const toggleTemplateStatus = async (template: OperationsContractTemplate) => {
134
+ const nextStatus =
135
+ template.status === 'active'
136
+ ? 'inactive'
137
+ : template.status === 'archived'
138
+ ? 'active'
139
+ : 'active';
140
+
141
+ try {
142
+ await mutateOperations(
143
+ request,
144
+ `/operations/contract-templates/${template.id}`,
145
+ 'PATCH',
146
+ {
147
+ status: nextStatus,
148
+ isActive: nextStatus === 'active',
149
+ }
150
+ );
151
+ showToastHandler?.('success', t('messages.statusSuccess'));
152
+ await refetch();
153
+ } catch {
154
+ showToastHandler?.('error', t('messages.statusError'));
155
+ }
156
+ };
157
+
158
+ if (!access.isLoading && !access.isDirector) {
159
+ return (
160
+ <Page>
161
+ <OperationsHeader
162
+ title={t('title')}
163
+ description={t('description')}
164
+ current={t('breadcrumb')}
165
+ actions={
166
+ <Button variant="outline" size="sm" asChild>
167
+ <Link href="/operations/contracts">
168
+ {commonT('actions.back')}
169
+ </Link>
170
+ </Button>
171
+ }
172
+ />
173
+
174
+ <EmptyState
175
+ icon={<FileStack className="size-12" />}
176
+ title={commonT('states.noAccessTitle')}
177
+ description={t('noAccessDescription')}
178
+ actionLabel={commonT('actions.back')}
179
+ onAction={() => {
180
+ window.location.href = '/operations/contracts';
181
+ }}
182
+ />
183
+ </Page>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <Page>
189
+ <OperationsHeader
190
+ title={t('title')}
191
+ description={t('description')}
192
+ current={t('breadcrumb')}
193
+ actions={
194
+ <div className="flex flex-wrap gap-2">
195
+ <Button variant="outline" size="sm" asChild>
196
+ <Link href="/operations/contracts">
197
+ {commonT('actions.back')}
198
+ </Link>
199
+ </Button>
200
+ <Button
201
+ size="sm"
202
+ className="cursor-pointer"
203
+ onClick={openCreateSheet}
204
+ >
205
+ {commonT('actions.create')}
206
+ </Button>
207
+ </div>
208
+ }
209
+ />
210
+
211
+ <KpiCardsGrid items={statsCards} columns={4} />
212
+
213
+ <SearchBar
214
+ searchQuery={search}
215
+ onSearchChange={setSearch}
216
+ onSearch={() => undefined}
217
+ placeholder={t('searchPlaceholder')}
218
+ controls={[
219
+ {
220
+ id: 'type',
221
+ type: 'select',
222
+ value: typeFilter,
223
+ onChange: setTypeFilter,
224
+ placeholder: t('filters.type'),
225
+ options: [
226
+ { value: 'all', label: commonT('filters.allTypes') },
227
+ ...[
228
+ 'clt',
229
+ 'pj',
230
+ 'freelancer_agreement',
231
+ 'service_agreement',
232
+ 'fixed_term',
233
+ 'recurring_service',
234
+ 'nda',
235
+ 'amendment',
236
+ 'addendum',
237
+ 'other',
238
+ ].map((value) => ({ value, label: formatEnumLabel(value) })),
239
+ ],
240
+ },
241
+ {
242
+ id: 'status',
243
+ type: 'select',
244
+ value: statusFilter,
245
+ onChange: setStatusFilter,
246
+ placeholder: commonT('labels.status'),
247
+ options: [
248
+ { value: 'all', label: commonT('filters.allStatuses') },
249
+ ...['active', 'draft', 'inactive', 'archived'].map((value) => ({
250
+ value,
251
+ label: formatEnumLabel(value),
252
+ })),
253
+ ],
254
+ },
255
+ ]}
256
+ />
257
+
258
+ {isLoading ? (
259
+ <Card className="py-0">
260
+ <CardContent className="p-6 text-sm text-muted-foreground">
261
+ {commonT('actions.refresh')}...
262
+ </CardContent>
263
+ </Card>
264
+ ) : filteredTemplates.length ? (
265
+ <Card className="py-0">
266
+ <CardContent className="p-0">
267
+ <div className="overflow-x-auto rounded-md border">
268
+ <Table>
269
+ <TableHeader>
270
+ <TableRow>
271
+ <TableHead>{t('columns.name')}</TableHead>
272
+ <TableHead>{t('columns.code')}</TableHead>
273
+ <TableHead>{t('columns.type')}</TableHead>
274
+ <TableHead>{t('columns.usageCount')}</TableHead>
275
+ <TableHead>{commonT('labels.billingModel')}</TableHead>
276
+ <TableHead>{t('columns.updatedAt')}</TableHead>
277
+ <TableHead>{commonT('labels.status')}</TableHead>
278
+ <TableHead className="text-right">
279
+ {commonT('labels.actions')}
280
+ </TableHead>
281
+ </TableRow>
282
+ </TableHeader>
283
+ <TableBody>
284
+ {filteredTemplates.map((template) => (
285
+ <TableRow key={template.id}>
286
+ <TableCell>
287
+ <div className="space-y-1">
288
+ <div className="font-medium">{template.name}</div>
289
+ <div className="max-w-md text-xs text-muted-foreground">
290
+ {template.description ||
291
+ commonT('labels.notAvailable')}
292
+ </div>
293
+ </div>
294
+ </TableCell>
295
+ <TableCell>{template.code || '—'}</TableCell>
296
+ <TableCell>
297
+ {formatEnumLabel(template.contractType)}
298
+ </TableCell>
299
+ <TableCell>{template.usageCount ?? 0}</TableCell>
300
+ <TableCell>
301
+ {formatEnumLabel(template.billingModel)}
302
+ </TableCell>
303
+ <TableCell>{formatDate(template.updatedAt)}</TableCell>
304
+ <TableCell>
305
+ <StatusBadge
306
+ label={formatEnumLabel(template.status)}
307
+ className={getStatusBadgeClass(template.status)}
308
+ />
309
+ </TableCell>
310
+ <TableCell>
311
+ <div className="flex justify-end gap-2">
312
+ <Button
313
+ type="button"
314
+ variant="outline"
315
+ size="sm"
316
+ className="cursor-pointer"
317
+ asChild
318
+ >
319
+ <Link href={`/operations/contracts?template=${template.id}`}>
320
+ {commonT('actions.useTemplate')}
321
+ </Link>
322
+ </Button>
323
+ <Button
324
+ type="button"
325
+ variant="outline"
326
+ size="icon"
327
+ className="cursor-pointer"
328
+ onClick={() => openEditSheet(template)}
329
+ >
330
+ <Pencil className="size-4" />
331
+ </Button>
332
+ <Button
333
+ type="button"
334
+ variant="outline"
335
+ size="sm"
336
+ className="cursor-pointer"
337
+ onClick={() => void toggleTemplateStatus(template)}
338
+ >
339
+ {template.status === 'active'
340
+ ? commonT('actions.deactivate')
341
+ : commonT('actions.activate')}
342
+ </Button>
343
+ </div>
344
+ </TableCell>
345
+ </TableRow>
346
+ ))}
347
+ </TableBody>
348
+ </Table>
349
+ </div>
350
+ </CardContent>
351
+ </Card>
352
+ ) : (
353
+ <EmptyState
354
+ icon={<FileStack className="size-12" />}
355
+ title={commonT('states.emptyTitle')}
356
+ description={t('emptyDescription')}
357
+ actionLabel={commonT('actions.create')}
358
+ onAction={openCreateSheet}
359
+ />
360
+ )}
361
+
362
+ <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
363
+ <SheetContent className="w-full overflow-y-auto sm:max-w-6xl">
364
+ <SheetHeader>
365
+ <SheetTitle>
366
+ {editingTemplate ? t('sheet.editTitle') : t('sheet.createTitle')}
367
+ </SheetTitle>
368
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
369
+ </SheetHeader>
370
+
371
+ <ContractTemplateFormScreen
372
+ templateId={editingTemplate?.id}
373
+ onCancel={() => setIsSheetOpen(false)}
374
+ onSaved={async () => {
375
+ setIsSheetOpen(false);
376
+ setEditingTemplate(null);
377
+ await refetch();
378
+ }}
379
+ />
380
+ </SheetContent>
381
+ </Sheet>
382
+ </Page>
383
+ );
384
+ }