@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,370 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Command,
6
+ CommandEmpty,
7
+ CommandGroup,
8
+ CommandInput,
9
+ CommandItem,
10
+ CommandList,
11
+ } from '@/components/ui/command';
12
+ import { Input } from '@/components/ui/input';
13
+ import { Label } from '@/components/ui/label';
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from '@/components/ui/popover';
19
+ import {
20
+ Sheet,
21
+ SheetContent,
22
+ SheetDescription,
23
+ SheetHeader,
24
+ SheetTitle,
25
+ } from '@/components/ui/sheet';
26
+ import { ChevronsUpDown, Plus, X } from 'lucide-react';
27
+ import { useTranslations } from 'next-intl';
28
+ import { useMemo, useRef, useState } from 'react';
29
+
30
+ type DepartmentOption = {
31
+ id?: number | null;
32
+ name: string;
33
+ code?: string | null;
34
+ description?: string | null;
35
+ };
36
+
37
+ type DepartmentSelectWithCreateProps = {
38
+ label: string;
39
+ value?: string | null;
40
+ options: DepartmentOption[];
41
+ onChange: (department: DepartmentOption) => void;
42
+ selectPlaceholder: string;
43
+ createDescription?: string;
44
+ createPlaceholder?: string;
45
+ disabled?: boolean;
46
+ };
47
+
48
+ export function DepartmentSelectWithCreate({
49
+ label,
50
+ value,
51
+ options,
52
+ onChange,
53
+ selectPlaceholder,
54
+ createDescription,
55
+ createPlaceholder,
56
+ disabled = false,
57
+ }: DepartmentSelectWithCreateProps) {
58
+ const commonT = useTranslations('operations.Common');
59
+ const [open, setOpen] = useState(false);
60
+ const [search, setSearch] = useState('');
61
+ const [createOpen, setCreateOpen] = useState(false);
62
+ const [newDepartment, setNewDepartment] = useState('');
63
+ const [visibleCount, setVisibleCount] = useState(20);
64
+ const parentScrollContainerRef = useRef<HTMLElement | null>(null);
65
+ const parentScrollTopRef = useRef(0);
66
+
67
+ const normalizedValue = value?.trim() ?? '';
68
+
69
+ const normalizedOptions = useMemo(() => {
70
+ const values = new Map<string, DepartmentOption>();
71
+
72
+ for (const option of options) {
73
+ const normalizedName = option.name.trim();
74
+
75
+ if (!normalizedName) {
76
+ continue;
77
+ }
78
+
79
+ values.set(normalizedName.toLowerCase(), {
80
+ ...option,
81
+ name: normalizedName,
82
+ });
83
+ }
84
+
85
+ if (normalizedValue && !values.has(normalizedValue.toLowerCase())) {
86
+ values.set(normalizedValue.toLowerCase(), {
87
+ id: null,
88
+ name: normalizedValue,
89
+ });
90
+ }
91
+
92
+ return Array.from(values.values()).sort((left, right) =>
93
+ left.name.localeCompare(right.name)
94
+ );
95
+ }, [options, normalizedValue]);
96
+
97
+ const filteredOptions = useMemo(() => {
98
+ const normalizedSearch = search.trim().toLowerCase();
99
+
100
+ if (!normalizedSearch) {
101
+ return normalizedOptions;
102
+ }
103
+
104
+ return normalizedOptions.filter((option) =>
105
+ [option.name, option.code, option.description]
106
+ .filter(Boolean)
107
+ .some((field) => String(field).toLowerCase().includes(normalizedSearch))
108
+ );
109
+ }, [normalizedOptions, search]);
110
+
111
+ const visibleOptions = filteredOptions.slice(0, visibleCount);
112
+
113
+ const captureParentScrollPosition = (trigger: HTMLElement) => {
114
+ const parentSheetContent = trigger.closest(
115
+ '[data-radix-dialog-content]'
116
+ ) as HTMLElement | null;
117
+
118
+ if (!parentSheetContent) {
119
+ parentScrollContainerRef.current = null;
120
+ parentScrollTopRef.current = 0;
121
+ return;
122
+ }
123
+
124
+ parentScrollContainerRef.current = parentSheetContent;
125
+ parentScrollTopRef.current = parentSheetContent.scrollTop;
126
+ };
127
+
128
+ const restoreParentScrollPosition = () => {
129
+ const fallbackOpenDialog = (
130
+ Array.from(
131
+ document.querySelectorAll(
132
+ '[data-radix-dialog-content][data-state="open"]'
133
+ )
134
+ ) as HTMLElement[]
135
+ ).at(-1);
136
+
137
+ const container =
138
+ parentScrollContainerRef.current &&
139
+ document.body.contains(parentScrollContainerRef.current)
140
+ ? parentScrollContainerRef.current
141
+ : fallbackOpenDialog || null;
142
+
143
+ if (!container) {
144
+ return;
145
+ }
146
+
147
+ const restore = () => {
148
+ container.scrollTop = parentScrollTopRef.current;
149
+ };
150
+
151
+ requestAnimationFrame(restore);
152
+ setTimeout(restore, 0);
153
+ setTimeout(restore, 120);
154
+ };
155
+
156
+ const saveDepartment = () => {
157
+ const normalizedDepartment = newDepartment.trim();
158
+
159
+ if (!normalizedDepartment) {
160
+ return;
161
+ }
162
+
163
+ onChange({
164
+ id: null,
165
+ name: normalizedDepartment,
166
+ });
167
+ setNewDepartment('');
168
+ setSearch(normalizedDepartment);
169
+ setCreateOpen(false);
170
+ setOpen(false);
171
+ };
172
+
173
+ return (
174
+ <>
175
+ <div className="grid gap-2">
176
+ <Label>{label}</Label>
177
+
178
+ <div className="flex w-full min-w-0 items-center gap-2">
179
+ <Popover
180
+ open={!disabled && open}
181
+ onOpenChange={(nextOpen) => {
182
+ if (!disabled) {
183
+ setOpen(nextOpen);
184
+ }
185
+ }}
186
+ >
187
+ <PopoverTrigger asChild>
188
+ <Button
189
+ type="button"
190
+ variant="outline"
191
+ role="combobox"
192
+ disabled={disabled}
193
+ className="h-10 flex-1 min-w-0 justify-between overflow-hidden"
194
+ >
195
+ <span className="truncate text-left">
196
+ {normalizedValue || selectPlaceholder}
197
+ </span>
198
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
199
+ </Button>
200
+ </PopoverTrigger>
201
+ <PopoverContent
202
+ className="p-0"
203
+ style={{ width: 'var(--radix-popover-trigger-width)' }}
204
+ >
205
+ <Command shouldFilter={false}>
206
+ <CommandInput
207
+ placeholder={selectPlaceholder}
208
+ value={search}
209
+ onValueChange={(value) => {
210
+ setSearch(value);
211
+ setVisibleCount(20);
212
+ }}
213
+ />
214
+ <CommandList>
215
+ <CommandEmpty>
216
+ <div className="space-y-2 p-2 text-center">
217
+ <p className="text-sm text-muted-foreground">
218
+ {commonT('states.emptyDescription')}
219
+ </p>
220
+ <Button
221
+ type="button"
222
+ variant="outline"
223
+ className="w-full"
224
+ onClick={(event) => {
225
+ captureParentScrollPosition(event.currentTarget);
226
+ setNewDepartment(search.trim());
227
+ setOpen(false);
228
+ setCreateOpen(true);
229
+ }}
230
+ >
231
+ {commonT('actions.create')} {label.toLowerCase()}
232
+ </Button>
233
+ </div>
234
+ </CommandEmpty>
235
+
236
+ <CommandGroup>
237
+ {visibleOptions.map((option) => (
238
+ <CommandItem
239
+ key={`${option.id ?? 'new'}-${option.name}`}
240
+ value={`${option.name} ${option.code ?? ''} ${option.description ?? ''}`}
241
+ onSelect={() => {
242
+ onChange(option);
243
+ setSearch(option.name);
244
+ setOpen(false);
245
+ }}
246
+ >
247
+ <div className="min-w-0">
248
+ <div className="truncate">{option.name}</div>
249
+ {option.code || option.description ? (
250
+ <div className="truncate text-xs text-muted-foreground">
251
+ {[option.code, option.description]
252
+ .filter(Boolean)
253
+ .join(' • ')}
254
+ </div>
255
+ ) : null}
256
+ </div>
257
+ </CommandItem>
258
+ ))}
259
+ </CommandGroup>
260
+
261
+ {filteredOptions.length > visibleCount ? (
262
+ <div className="border-t p-2">
263
+ <Button
264
+ type="button"
265
+ variant="ghost"
266
+ className="w-full"
267
+ onClick={() =>
268
+ setVisibleCount((current) => current + 20)
269
+ }
270
+ >
271
+ {commonT('actions.loadMore')}
272
+ </Button>
273
+ </div>
274
+ ) : null}
275
+ </CommandList>
276
+ </Command>
277
+ </PopoverContent>
278
+ </Popover>
279
+
280
+ {normalizedValue ? (
281
+ <Button
282
+ type="button"
283
+ variant="outline"
284
+ size="icon"
285
+ className="h-10 w-10 shrink-0"
286
+ disabled={disabled}
287
+ onClick={() => {
288
+ onChange({ id: null, name: '' });
289
+ setSearch('');
290
+ setVisibleCount(20);
291
+ setOpen(false);
292
+ }}
293
+ aria-label={commonT('actions.cancel')}
294
+ >
295
+ <X className="h-4 w-4" />
296
+ </Button>
297
+ ) : null}
298
+
299
+ <Button
300
+ type="button"
301
+ variant="outline"
302
+ size="icon"
303
+ className="h-10 w-10 shrink-0"
304
+ disabled={disabled}
305
+ onClick={(event) => {
306
+ captureParentScrollPosition(event.currentTarget);
307
+ setNewDepartment(normalizedValue);
308
+ setOpen(false);
309
+ setCreateOpen(true);
310
+ }}
311
+ aria-label={`${commonT('actions.create')} ${label.toLowerCase()}`}
312
+ >
313
+ <Plus className="h-4 w-4" />
314
+ </Button>
315
+ </div>
316
+ </div>
317
+
318
+ <Sheet
319
+ open={createOpen}
320
+ onOpenChange={(nextOpen) => {
321
+ setCreateOpen(nextOpen);
322
+ if (!nextOpen) {
323
+ setNewDepartment('');
324
+ restoreParentScrollPosition();
325
+ }
326
+ }}
327
+ >
328
+ <SheetContent
329
+ className="w-full overflow-y-auto sm:max-w-md px-2"
330
+ onCloseAutoFocus={(event) => event.preventDefault()}
331
+ >
332
+ <SheetHeader>
333
+ <SheetTitle>
334
+ {commonT('actions.create')} {label.toLowerCase()}
335
+ </SheetTitle>
336
+ <SheetDescription>
337
+ {createDescription || selectPlaceholder}
338
+ </SheetDescription>
339
+ </SheetHeader>
340
+
341
+ <div className="mt-6 space-y-4">
342
+ <div className="space-y-2">
343
+ <Label>{label}</Label>
344
+ <Input
345
+ value={newDepartment}
346
+ placeholder={createPlaceholder || label}
347
+ onChange={(event) => setNewDepartment(event.target.value)}
348
+ onKeyDown={(event) => {
349
+ if (event.key === 'Enter') {
350
+ event.preventDefault();
351
+ saveDepartment();
352
+ }
353
+ }}
354
+ />
355
+ </div>
356
+
357
+ <Button
358
+ type="button"
359
+ className="w-full"
360
+ disabled={!newDepartment.trim()}
361
+ onClick={saveDepartment}
362
+ >
363
+ {commonT('actions.save')}
364
+ </Button>
365
+ </div>
366
+ </SheetContent>
367
+ </Sheet>
368
+ </>
369
+ );
370
+ }