@hed-hog/operations 0.0.331 → 0.0.332

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 (62) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +54 -0
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +100 -0
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  6. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  7. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  8. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  9. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  10. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  11. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  12. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  13. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  14. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  15. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  16. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  17. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  18. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  19. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  20. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  21. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  22. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  23. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  24. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  25. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  26. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  27. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  28. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  29. package/dist/operations.service.d.ts +76 -0
  30. package/dist/operations.service.d.ts.map +1 -1
  31. package/dist/operations.service.js +235 -5
  32. package/dist/operations.service.js.map +1 -1
  33. package/hedhog/data/menu.yaml +27 -8
  34. package/hedhog/data/route.yaml +72 -0
  35. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
  36. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  37. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  38. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +86 -87
  39. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
  40. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
  41. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
  42. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
  43. package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
  44. package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
  45. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
  46. package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
  47. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
  48. package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
  49. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  50. package/hedhog/frontend/messages/en.json +96 -2
  51. package/hedhog/frontend/messages/pt.json +96 -2
  52. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  53. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  54. package/package.json +5 -5
  55. package/src/controllers/operations-collaborators.controller.ts +117 -8
  56. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  57. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  58. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  59. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  60. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  61. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  62. package/src/operations.service.ts +328 -5
@@ -18,6 +18,7 @@ import {
18
18
  CommandItem,
19
19
  CommandList,
20
20
  } from '@/components/ui/command';
21
+ import { EntityPicker } from '@/components/ui/entity-picker';
21
22
  import {
22
23
  Form,
23
24
  FormControl,
@@ -49,12 +50,7 @@ import {
49
50
  SheetHeader,
50
51
  SheetTitle,
51
52
  } from '@/components/ui/sheet';
52
- import {
53
- Tabs,
54
- TabsContent,
55
- TabsList,
56
- TabsTrigger,
57
- } from '@/components/ui/tabs';
53
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
58
54
  import { Textarea } from '@/components/ui/textarea';
59
55
  import {
60
56
  Tooltip,
@@ -100,12 +96,36 @@ import {
100
96
  trimToNull,
101
97
  } from '../_lib/utils/forms';
102
98
  import { ContractFormScreen } from './contract-form-screen';
103
- import { DepartmentPicker } from './department-picker';
104
99
  import { OperationsHeader } from './operations-header';
105
- import { ProjectFileAttachments } from './project-file-attachments';
106
100
 
107
101
  const OPTION_PAGE_SIZE = 12;
108
102
 
103
+ function getPersonAvatarUrl(avatarId?: number | null) {
104
+ return typeof avatarId === 'number' && avatarId > 0
105
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
106
+ : undefined;
107
+ }
108
+
109
+ function getUserPhotoUrl(photoId?: number | null) {
110
+ return typeof photoId === 'number' && photoId > 0
111
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
112
+ : undefined;
113
+ }
114
+
115
+ function getInitials(value?: string | null) {
116
+ const parts = String(value ?? '')
117
+ .trim()
118
+ .split(/\s+/)
119
+ .filter(Boolean)
120
+ .slice(0, 2);
121
+
122
+ if (!parts.length) {
123
+ return '??';
124
+ }
125
+
126
+ return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
127
+ }
128
+
109
129
  type TeamAssignmentState = {
110
130
  collaboratorId: number;
111
131
  selected: boolean;
@@ -645,6 +665,7 @@ export function ProjectFormScreen({
645
665
  const t = useTranslations('operations.ProjectFormPage');
646
666
  const commonT = useTranslations('operations.Common');
647
667
  const contractT = useTranslations('operations.ContractFormPage');
668
+ const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
648
669
  const { request, showToastHandler, currentLocaleCode } = useApp();
649
670
  const access = useOperationsAccess();
650
671
  const router = useRouter();
@@ -652,6 +673,8 @@ export function ProjectFormScreen({
652
673
  const personSheetModeRef = useRef<'create' | 'edit'>('edit');
653
674
  const [personSheetOpen, setPersonSheetOpen] = useState(false);
654
675
  const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
676
+ const [createdManagerCollaborators, setCreatedManagerCollaborators] =
677
+ useState<OperationsCollaborator[]>([]);
655
678
  const isSheetMode = Boolean(onCancel);
656
679
  const isCreateMode = !projectId;
657
680
  const [codeAutoMode, setCodeAutoMode] = useState(isCreateMode);
@@ -746,6 +769,37 @@ export function ProjectFormScreen({
746
769
  [rawCollaborators]
747
770
  );
748
771
 
772
+ const createManagerCollaborator = async (values: Record<string, string>) => {
773
+ const displayName = values.displayName?.trim() ?? '';
774
+ const weeklyCapacityHours = parseNumberInput(
775
+ values.weeklyCapacityHours ?? ''
776
+ );
777
+
778
+ if (!displayName) {
779
+ return null;
780
+ }
781
+
782
+ const created = await mutateOperations<OperationsCollaborator>(
783
+ request,
784
+ '/operations/collaborators',
785
+ 'POST',
786
+ {
787
+ displayName,
788
+ weeklyCapacityHours,
789
+ status: 'active',
790
+ autoGenerateContractDraft: false,
791
+ }
792
+ );
793
+
794
+ setCreatedManagerCollaborators((current) => {
795
+ const next = current.filter((item) => item.id !== created.id);
796
+ next.unshift(created);
797
+ return next;
798
+ });
799
+
800
+ return created;
801
+ };
802
+
749
803
  const { data: contracts = [], refetch: refetchContracts } = useQuery<
750
804
  OperationsContract[]
751
805
  >({
@@ -819,6 +873,10 @@ export function ProjectFormScreen({
819
873
  byId.set(collaborator.id, collaborator);
820
874
  }
821
875
 
876
+ for (const collaborator of createdManagerCollaborators) {
877
+ byId.set(collaborator.id, collaborator);
878
+ }
879
+
822
880
  if (
823
881
  project?.managerCollaboratorId &&
824
882
  !byId.has(project.managerCollaboratorId)
@@ -847,7 +905,7 @@ export function ProjectFormScreen({
847
905
  }
848
906
 
849
907
  return Array.from(byId.values());
850
- }, [collaborators, project]);
908
+ }, [collaborators, createdManagerCollaborators, project]);
851
909
 
852
910
  const availableContracts = useMemo(() => {
853
911
  if (!project?.relatedContract) {
@@ -880,10 +938,9 @@ export function ProjectFormScreen({
880
938
  .filter(Boolean)
881
939
  .join(' • '),
882
940
  avatarUrl:
883
- typeof collaborator.personAvatarId === 'number' &&
884
- collaborator.personAvatarId > 0
885
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${collaborator.personAvatarId}`
886
- : null,
941
+ getUserPhotoUrl(collaborator.userPhotoId) ??
942
+ getPersonAvatarUrl(collaborator.personAvatarId) ??
943
+ null,
887
944
  })),
888
945
  [availableCollaborators]
889
946
  );
@@ -1264,20 +1321,100 @@ export function ProjectFormScreen({
1264
1321
  )}
1265
1322
  </p>
1266
1323
  </div>
1267
- <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-5">
1324
+ <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-3">
1268
1325
  <div className="min-w-0 space-y-2">
1269
1326
  <FieldLabel label={commonT('labels.manager')} />
1270
- <SearchableSelect
1271
- label=""
1272
- value={form.managerCollaboratorId}
1327
+ <EntityPicker
1328
+ value={
1329
+ form.managerCollaboratorId === 'none'
1330
+ ? null
1331
+ : Number(form.managerCollaboratorId)
1332
+ }
1273
1333
  options={managerOptions}
1334
+ getOptionValue={(option) => option.id}
1335
+ getOptionLabel={(option) => option.title}
1336
+ getOptionDescription={(option) =>
1337
+ option.description || undefined
1338
+ }
1339
+ renderOption={({ option }) => (
1340
+ <div className="flex min-w-0 items-center gap-2.5">
1341
+ <Avatar className="size-6 shrink-0">
1342
+ <AvatarImage
1343
+ src={option.avatarUrl ?? undefined}
1344
+ alt={option.title}
1345
+ />
1346
+ <AvatarFallback className="text-[10px]">
1347
+ {getInitials(option.title)}
1348
+ </AvatarFallback>
1349
+ </Avatar>
1350
+ <div className="min-w-0">
1351
+ <div className="truncate text-sm">{option.title}</div>
1352
+ {option.description ? (
1353
+ <div className="truncate text-xs text-muted-foreground">
1354
+ {option.description}
1355
+ </div>
1356
+ ) : null}
1357
+ </div>
1358
+ </div>
1359
+ )}
1360
+ renderSelectedValue={({ option, label }) =>
1361
+ option ? (
1362
+ <div className="flex min-w-0 items-center gap-2">
1363
+ <Avatar className="size-5 shrink-0">
1364
+ <AvatarImage
1365
+ src={option.avatarUrl ?? undefined}
1366
+ alt={option.title}
1367
+ />
1368
+ <AvatarFallback className="text-[10px]">
1369
+ {getInitials(option.title)}
1370
+ </AvatarFallback>
1371
+ </Avatar>
1372
+ <span className="truncate">{option.title}</span>
1373
+ </div>
1374
+ ) : (
1375
+ <span className="text-muted-foreground">{label}</span>
1376
+ )
1377
+ }
1274
1378
  placeholder={commonT('labels.notAssigned')}
1275
1379
  searchPlaceholder={t('placeholders.managerSearch')}
1276
- emptyLabel={commonT('labels.notAssigned')}
1380
+ emptySelectionLabel={commonT('labels.notAssigned')}
1381
+ valueType="number"
1382
+ clearable
1383
+ allowEmptySelection
1384
+ showCreateButton
1385
+ entityLabel={commonT('labels.manager').toLowerCase()}
1386
+ createActionLabel={`${commonT('actions.create')} ${commonT(
1387
+ 'labels.manager'
1388
+ ).toLowerCase()}`}
1389
+ createTitle={`${commonT('actions.create')} ${commonT(
1390
+ 'labels.manager'
1391
+ ).toLowerCase()}`}
1392
+ createDescription={collaboratorFormT(
1393
+ 'sections.employmentInfoCreateDescription'
1394
+ )}
1395
+ createFields={[
1396
+ {
1397
+ name: 'displayName',
1398
+ label: collaboratorFormT('fields.displayName'),
1399
+ placeholder: collaboratorFormT('fields.displayName'),
1400
+ required: true,
1401
+ },
1402
+ {
1403
+ name: 'weeklyCapacityHours',
1404
+ label: collaboratorFormT('fields.weeklyCapacityHours'),
1405
+ placeholder: '40',
1406
+ type: 'number',
1407
+ },
1408
+ ]}
1409
+ mapSearchToCreateValues={(search) => ({
1410
+ displayName: search,
1411
+ weeklyCapacityHours: '40',
1412
+ })}
1413
+ onCreate={createManagerCollaborator}
1277
1414
  onChange={(value) =>
1278
1415
  setForm((current) => ({
1279
1416
  ...current,
1280
- managerCollaboratorId: value,
1417
+ managerCollaboratorId: value ? String(value) : 'none',
1281
1418
  }))
1282
1419
  }
1283
1420
  />
@@ -1372,12 +1509,11 @@ export function ProjectFormScreen({
1372
1509
  {!isCreateMode ? (
1373
1510
  <>
1374
1511
  <Tabs defaultValue="financials" className="space-y-3">
1375
- <TabsList className="grid w-full grid-cols-3">
1512
+ <TabsList className="grid w-full grid-cols-2">
1376
1513
  <TabsTrigger value="financials">
1377
1514
  {t('sections.financials')}
1378
1515
  </TabsTrigger>
1379
1516
  <TabsTrigger value="team">{t('sections.team')}</TabsTrigger>
1380
- <TabsTrigger value="files">{t('sections.files')}</TabsTrigger>
1381
1517
  </TabsList>
1382
1518
 
1383
1519
  <TabsContent value="financials" className="space-y-3">
@@ -1487,9 +1623,7 @@ export function ProjectFormScreen({
1487
1623
  }}
1488
1624
  initialValues={{
1489
1625
  code: form.code ? `PRJ-${form.code}` : '',
1490
- name: form.name
1491
- ? `${form.name} Service Agreement`
1492
- : '',
1626
+ name: form.name ? `${form.name} Service Agreement` : '',
1493
1627
  clientName: form.clientName,
1494
1628
  contractCategory: 'client',
1495
1629
  contractType: 'service_agreement',
@@ -1666,20 +1800,6 @@ export function ProjectFormScreen({
1666
1800
  </div>
1667
1801
  </div>
1668
1802
  </TabsContent>
1669
-
1670
- <TabsContent value="files" className="space-y-3">
1671
- <div className="space-y-0.5">
1672
- <h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
1673
- {t('sections.files')}
1674
- </h3>
1675
- <p className="text-[11px] text-muted-foreground/80">
1676
- {t('sections.filesDescription')}
1677
- </p>
1678
- </div>
1679
- {projectId ? (
1680
- <ProjectFileAttachments projectId={projectId} />
1681
- ) : null}
1682
- </TabsContent>
1683
1803
  </Tabs>
1684
1804
  </>
1685
1805
  ) : (