@hed-hog/operations 0.0.338 → 0.0.349

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 (61) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +73 -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/controllers/operations-contracts.controller.d.ts +15 -15
  6. package/dist/controllers/operations-projects.controller.d.ts +3 -0
  7. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  8. package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
  9. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
  10. package/dist/dto/create-collaborator-invoice.dto.js +55 -0
  11. package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
  12. package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
  13. package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
  14. package/dist/dto/create-collaborator-payment.dto.js +50 -0
  15. package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
  16. package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
  17. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
  18. package/dist/dto/list-collaborator-invoice.dto.js +8 -0
  19. package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
  20. package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
  21. package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
  22. package/dist/dto/list-collaborator-payment.dto.js +8 -0
  23. package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
  24. package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
  25. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
  26. package/dist/dto/update-collaborator-invoice.dto.js +9 -0
  27. package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
  28. package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
  29. package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
  30. package/dist/dto/update-collaborator-payment.dto.js +9 -0
  31. package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
  32. package/dist/operations.service.d.ts +98 -0
  33. package/dist/operations.service.d.ts.map +1 -1
  34. package/dist/operations.service.js +226 -3
  35. package/dist/operations.service.js.map +1 -1
  36. package/hedhog/data/menu.yaml +32 -11
  37. package/hedhog/data/route.yaml +72 -0
  38. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +38 -0
  39. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
  40. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
  41. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +212 -10
  42. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +668 -11
  43. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +182 -28
  44. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +28 -7
  45. package/hedhog/frontend/app/_lib/api.ts.ejs +151 -0
  46. package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
  47. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
  48. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
  49. package/hedhog/frontend/messages/en.json +96 -2
  50. package/hedhog/frontend/messages/pt.json +96 -2
  51. package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
  52. package/hedhog/table/operations_collaborator_payment.yaml +32 -0
  53. package/package.json +4 -4
  54. package/src/controllers/operations-collaborators.controller.ts +109 -0
  55. package/src/dto/create-collaborator-invoice.dto.ts +39 -0
  56. package/src/dto/create-collaborator-payment.dto.ts +35 -0
  57. package/src/dto/list-collaborator-invoice.dto.ts +3 -0
  58. package/src/dto/list-collaborator-payment.dto.ts +3 -0
  59. package/src/dto/update-collaborator-invoice.dto.ts +6 -0
  60. package/src/dto/update-collaborator-payment.dto.ts +6 -0
  61. package/src/operations.service.ts +318 -4
@@ -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,37 @@ 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
100
  import { ProjectFileAttachments } from './project-file-attachments';
106
101
 
107
102
  const OPTION_PAGE_SIZE = 12;
108
103
 
104
+ function getPersonAvatarUrl(avatarId?: number | null) {
105
+ return typeof avatarId === 'number' && avatarId > 0
106
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
107
+ : undefined;
108
+ }
109
+
110
+ function getUserPhotoUrl(photoId?: number | null) {
111
+ return typeof photoId === 'number' && photoId > 0
112
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
113
+ : undefined;
114
+ }
115
+
116
+ function getInitials(value?: string | null) {
117
+ const parts = String(value ?? '')
118
+ .trim()
119
+ .split(/\s+/)
120
+ .filter(Boolean)
121
+ .slice(0, 2);
122
+
123
+ if (!parts.length) {
124
+ return '??';
125
+ }
126
+
127
+ return parts.map((part) => part[0]?.toUpperCase() ?? '').join('');
128
+ }
129
+
109
130
  type TeamAssignmentState = {
110
131
  collaboratorId: number;
111
132
  selected: boolean;
@@ -140,6 +161,27 @@ type ProjectFormState = {
140
161
 
141
162
  type ProjectFormValues = ProjectFormState;
142
163
 
164
+ type ManagerOption = {
165
+ id: number;
166
+ title: string;
167
+ description: string;
168
+ avatarUrl: string | null;
169
+ };
170
+
171
+ function toManagerOption(collaborator: OperationsCollaborator): ManagerOption {
172
+ return {
173
+ id: collaborator.id,
174
+ title: collaborator.displayName,
175
+ description: [collaborator.department, collaborator.title]
176
+ .filter(Boolean)
177
+ .join(' • '),
178
+ avatarUrl:
179
+ getUserPhotoUrl(collaborator.userPhotoId) ??
180
+ getPersonAvatarUrl(collaborator.personAvatarId) ??
181
+ null,
182
+ };
183
+ }
184
+
143
185
  function generateProjectCode(name: string): string {
144
186
  const words = name.trim().split(/\s+/).filter(Boolean);
145
187
  if (words.length === 0) return '';
@@ -645,6 +687,7 @@ export function ProjectFormScreen({
645
687
  const t = useTranslations('operations.ProjectFormPage');
646
688
  const commonT = useTranslations('operations.Common');
647
689
  const contractT = useTranslations('operations.ContractFormPage');
690
+ const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
648
691
  const { request, showToastHandler, currentLocaleCode } = useApp();
649
692
  const access = useOperationsAccess();
650
693
  const router = useRouter();
@@ -655,6 +698,8 @@ export function ProjectFormScreen({
655
698
  const isSheetMode = Boolean(onCancel);
656
699
  const isCreateMode = !projectId;
657
700
  const [codeAutoMode, setCodeAutoMode] = useState(isCreateMode);
701
+ const [createdManagerCollaborators, setCreatedManagerCollaborators] =
702
+ useState<OperationsCollaborator[]>([]);
658
703
 
659
704
  const projectFormSchema = useMemo(
660
705
  () =>
@@ -746,6 +791,39 @@ export function ProjectFormScreen({
746
791
  [rawCollaborators]
747
792
  );
748
793
 
794
+ const createManagerCollaborator = async (
795
+ values: Record<string, string>
796
+ ): Promise<ManagerOption | null> => {
797
+ const displayName = values.displayName?.trim() ?? '';
798
+ const weeklyCapacityHours = parseNumberInput(
799
+ values.weeklyCapacityHours ?? ''
800
+ );
801
+
802
+ if (!displayName) {
803
+ return null;
804
+ }
805
+
806
+ const created = await mutateOperations<OperationsCollaborator>(
807
+ request,
808
+ '/operations/collaborators',
809
+ 'POST',
810
+ {
811
+ displayName,
812
+ weeklyCapacityHours,
813
+ status: 'active',
814
+ autoGenerateContractDraft: false,
815
+ }
816
+ );
817
+
818
+ setCreatedManagerCollaborators((current) => {
819
+ const next = current.filter((item) => item.id !== created.id);
820
+ next.unshift(created);
821
+ return next;
822
+ });
823
+
824
+ return toManagerOption(created);
825
+ };
826
+
749
827
  const { data: contracts = [], refetch: refetchContracts } = useQuery<
750
828
  OperationsContract[]
751
829
  >({
@@ -819,6 +897,10 @@ export function ProjectFormScreen({
819
897
  byId.set(collaborator.id, collaborator);
820
898
  }
821
899
 
900
+ for (const collaborator of createdManagerCollaborators) {
901
+ byId.set(collaborator.id, collaborator);
902
+ }
903
+
822
904
  if (
823
905
  project?.managerCollaboratorId &&
824
906
  !byId.has(project.managerCollaboratorId)
@@ -847,7 +929,7 @@ export function ProjectFormScreen({
847
929
  }
848
930
 
849
931
  return Array.from(byId.values());
850
- }, [collaborators, project]);
932
+ }, [collaborators, createdManagerCollaborators, project]);
851
933
 
852
934
  const availableContracts = useMemo(() => {
853
935
  if (!project?.relatedContract) {
@@ -873,18 +955,9 @@ export function ProjectFormScreen({
873
955
 
874
956
  const managerOptions = useMemo(
875
957
  () =>
876
- availableCollaborators.map((collaborator) => ({
877
- id: collaborator.id,
878
- title: collaborator.displayName,
879
- description: [collaborator.department, collaborator.title]
880
- .filter(Boolean)
881
- .join(' • '),
882
- avatarUrl:
883
- typeof collaborator.personAvatarId === 'number' &&
884
- collaborator.personAvatarId > 0
885
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${collaborator.personAvatarId}`
886
- : null,
887
- })),
958
+ availableCollaborators.map((collaborator) =>
959
+ toManagerOption(collaborator)
960
+ ),
888
961
  [availableCollaborators]
889
962
  );
890
963
 
@@ -1176,6 +1249,9 @@ export function ProjectFormScreen({
1176
1249
  : null
1177
1250
  }
1178
1251
  initialSelectedLabel={field.value}
1252
+ initialSelectedAvatarId={
1253
+ project?.clientAvatarId ?? null
1254
+ }
1179
1255
  selectPlaceholder={t('placeholders.clientName')}
1180
1256
  onChange={(personId, personName) => {
1181
1257
  field.onChange(personName ?? '');
@@ -1267,17 +1343,97 @@ export function ProjectFormScreen({
1267
1343
  <div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-5">
1268
1344
  <div className="min-w-0 space-y-2">
1269
1345
  <FieldLabel label={commonT('labels.manager')} />
1270
- <SearchableSelect
1271
- label=""
1272
- value={form.managerCollaboratorId}
1346
+ <EntityPicker
1347
+ value={
1348
+ form.managerCollaboratorId === 'none'
1349
+ ? null
1350
+ : Number(form.managerCollaboratorId)
1351
+ }
1273
1352
  options={managerOptions}
1353
+ getOptionValue={(option) => option.id}
1354
+ getOptionLabel={(option) => option.title}
1355
+ getOptionDescription={(option) =>
1356
+ option.description || undefined
1357
+ }
1358
+ renderOption={({ option }) => (
1359
+ <div className="flex min-w-0 items-center gap-2.5">
1360
+ <Avatar className="size-6 shrink-0">
1361
+ <AvatarImage
1362
+ src={option.avatarUrl ?? undefined}
1363
+ alt={option.title}
1364
+ />
1365
+ <AvatarFallback className="text-[10px]">
1366
+ {getInitials(option.title)}
1367
+ </AvatarFallback>
1368
+ </Avatar>
1369
+ <div className="min-w-0">
1370
+ <div className="truncate text-sm">{option.title}</div>
1371
+ {option.description ? (
1372
+ <div className="truncate text-xs text-muted-foreground">
1373
+ {option.description}
1374
+ </div>
1375
+ ) : null}
1376
+ </div>
1377
+ </div>
1378
+ )}
1379
+ renderSelectedValue={({ option, label }) =>
1380
+ option ? (
1381
+ <div className="flex min-w-0 items-center gap-2">
1382
+ <Avatar className="size-5 shrink-0">
1383
+ <AvatarImage
1384
+ src={option.avatarUrl ?? undefined}
1385
+ alt={option.title}
1386
+ />
1387
+ <AvatarFallback className="text-[10px]">
1388
+ {getInitials(option.title)}
1389
+ </AvatarFallback>
1390
+ </Avatar>
1391
+ <span className="truncate">{option.title}</span>
1392
+ </div>
1393
+ ) : (
1394
+ <span className="text-muted-foreground">{label}</span>
1395
+ )
1396
+ }
1274
1397
  placeholder={commonT('labels.notAssigned')}
1275
1398
  searchPlaceholder={t('placeholders.managerSearch')}
1276
- emptyLabel={commonT('labels.notAssigned')}
1399
+ emptySelectionLabel={commonT('labels.notAssigned')}
1400
+ valueType="number"
1401
+ clearable
1402
+ allowEmptySelection
1403
+ showCreateButton
1404
+ entityLabel={commonT('labels.manager').toLowerCase()}
1405
+ createActionLabel={`${commonT('actions.create')} ${commonT(
1406
+ 'labels.manager'
1407
+ ).toLowerCase()}`}
1408
+ createTitle={`${commonT('actions.create')} ${commonT(
1409
+ 'labels.manager'
1410
+ ).toLowerCase()}`}
1411
+ createDescription={collaboratorFormT(
1412
+ 'sections.employmentInfoCreateDescription'
1413
+ )}
1414
+ createFields={[
1415
+ {
1416
+ name: 'displayName',
1417
+ label: collaboratorFormT('fields.displayName'),
1418
+ placeholder: collaboratorFormT('fields.displayName'),
1419
+ required: true,
1420
+ },
1421
+ {
1422
+ name: 'weeklyCapacityHours',
1423
+ label: collaboratorFormT('fields.weeklyCapacityHours'),
1424
+ placeholder: '40',
1425
+ type: 'number',
1426
+ },
1427
+ ]}
1428
+ mapSearchToCreateValues={(search) => ({
1429
+ displayName: search,
1430
+ weeklyCapacityHours: '40',
1431
+ })}
1432
+ onCreate={createManagerCollaborator}
1277
1433
  onChange={(value) =>
1278
1434
  setForm((current) => ({
1279
1435
  ...current,
1280
- managerCollaboratorId: value,
1436
+ managerCollaboratorId: value ? String(value) : 'none',
1281
1437
  }))
1282
1438
  }
1283
1439
  />
@@ -1487,9 +1643,7 @@ export function ProjectFormScreen({
1487
1643
  }}
1488
1644
  initialValues={{
1489
1645
  code: form.code ? `PRJ-${form.code}` : '',
1490
- name: form.name
1491
- ? `${form.name} Service Agreement`
1492
- : '',
1646
+ name: form.name ? `${form.name} Service Agreement` : '',
1493
1647
  clientName: form.clientName,
1494
1648
  contractCategory: 'client',
1495
1649
  contractType: 'service_agreement',
@@ -41,7 +41,11 @@ import type {
41
41
  OperationsTaskActivity,
42
42
  OperationsTaskComment,
43
43
  } from '../_lib/types';
44
- import { formatDate, formatDateTime, getStatusBadgeClass } from '../_lib/utils/format';
44
+ import {
45
+ formatDate,
46
+ formatDateTime,
47
+ getStatusBadgeClass,
48
+ } from '../_lib/utils/format';
45
49
  import {
46
50
  formatDurationMinutes,
47
51
  getElapsedDoingMinutes,
@@ -138,7 +142,10 @@ export type TaskCommentsSectionProps = {
138
142
  onChanged?: () => void;
139
143
  };
140
144
 
141
- export function TaskCommentsSection({ taskId, onChanged }: TaskCommentsSectionProps) {
145
+ export function TaskCommentsSection({
146
+ taskId,
147
+ onChanged,
148
+ }: TaskCommentsSectionProps) {
142
149
  const { request, showToastHandler, getSettingValue } = useApp();
143
150
  const ct = useTranslations('operations.ProjectDetailsPage.commentsSection');
144
151
  const editWindowMinutes = Number(
@@ -386,9 +393,13 @@ export function TaskCommentsSection({ taskId, onChanged }: TaskCommentsSectionPr
386
393
  />
387
394
  <div className="flex items-center justify-end gap-2">
388
395
  <span className="text-[10px] text-muted-foreground select-none">
389
- <kbd className="rounded border bg-muted px-1 py-0.5 font-mono text-[10px]">Ctrl</kbd>
396
+ <kbd className="rounded border bg-muted px-1 py-0.5 font-mono text-[10px]">
397
+ Ctrl
398
+ </kbd>
390
399
  {' + '}
391
- <kbd className="rounded border bg-muted px-1 py-0.5 font-mono text-[10px]">↵</kbd>
400
+ <kbd className="rounded border bg-muted px-1 py-0.5 font-mono text-[10px]">
401
+
402
+ </kbd>
392
403
  </span>
393
404
  <Button
394
405
  size="sm"
@@ -581,6 +592,7 @@ export function TaskDetailSheet({
581
592
  open,
582
593
  onOpenChange,
583
594
  statusLabel,
595
+ footer,
584
596
  defaultTab = 'comments',
585
597
  }: Props) {
586
598
  const detailT = useTranslations('operations.ProjectDetailsPage');
@@ -648,13 +660,15 @@ export function TaskDetailSheet({
648
660
  <p className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
649
661
  {detailT('taskForm.descriptionLabel')}
650
662
  </p>
651
- <p className="text-sm leading-relaxed">{task.description}</p>
663
+ <CommentContent content={task.description} />
652
664
  </div>
653
665
  ) : null}
654
666
 
655
667
  <Tabs
656
668
  value={tab}
657
- onValueChange={(v) => setTab(v as 'comments' | 'activities')}
669
+ onValueChange={(v) =>
670
+ setTab(v as 'comments' | 'activities')
671
+ }
658
672
  className="flex flex-1 min-h-0 flex-col"
659
673
  >
660
674
  <div className="shrink-0 border-b px-5 py-2">
@@ -744,7 +758,11 @@ export function TaskDetailSheet({
744
758
  <div className="flex items-center gap-1.5 text-sm">
745
759
  <Calendar className="size-3.5 shrink-0 text-muted-foreground" />
746
760
  <span>
747
- {formatDate(task.dueDate, getSettingValue, currentLocaleCode)}
761
+ {formatDate(
762
+ task.dueDate,
763
+ getSettingValue,
764
+ currentLocaleCode
765
+ )}
748
766
  </span>
749
767
  </div>
750
768
  </div>
@@ -788,6 +806,9 @@ export function TaskDetailSheet({
788
806
  </div>
789
807
  </aside>
790
808
  </div>
809
+ {footer ? (
810
+ <div className="shrink-0 border-t px-5 py-4">{footer}</div>
811
+ ) : null}
791
812
  </>
792
813
  ) : null}
793
814
  </SheetContent>
@@ -478,3 +478,154 @@ export function deleteTaskComment(
478
478
  'DELETE'
479
479
  );
480
480
  }
481
+
482
+ // Collaborator Payments
483
+
484
+ export type CollaboratorPayment = {
485
+ id: number;
486
+ collaboratorId: number;
487
+ amount: string;
488
+ paymentDate: string;
489
+ referenceMonth: string | null;
490
+ paymentMethod: string;
491
+ notes: string | null;
492
+ createdAt: string;
493
+ };
494
+
495
+ export function fetchCollaboratorPayments(
496
+ request: RequestFn,
497
+ collaboratorId: number
498
+ ) {
499
+ return fetchOperations<CollaboratorPayment[]>(
500
+ request,
501
+ `/operations/collaborators/${collaboratorId}/payment-history`
502
+ );
503
+ }
504
+
505
+ export function createCollaboratorPayment(
506
+ request: RequestFn,
507
+ collaboratorId: number,
508
+ data: {
509
+ amount: number;
510
+ paymentDate: string;
511
+ referenceMonth?: string | null;
512
+ paymentMethod?: string;
513
+ notes?: string | null;
514
+ }
515
+ ) {
516
+ return mutateOperations<CollaboratorPayment>(
517
+ request,
518
+ `/operations/collaborators/${collaboratorId}/payment-history`,
519
+ 'POST',
520
+ data
521
+ );
522
+ }
523
+
524
+ export function updateCollaboratorPayment(
525
+ request: RequestFn,
526
+ collaboratorId: number,
527
+ paymentId: number,
528
+ data: Partial<{
529
+ amount: number;
530
+ paymentDate: string;
531
+ referenceMonth: string | null;
532
+ paymentMethod: string;
533
+ notes: string | null;
534
+ }>
535
+ ) {
536
+ return mutateOperations<CollaboratorPayment>(
537
+ request,
538
+ `/operations/collaborators/${collaboratorId}/payment-history/${paymentId}`,
539
+ 'PATCH',
540
+ data
541
+ );
542
+ }
543
+
544
+ export function deleteCollaboratorPayment(
545
+ request: RequestFn,
546
+ collaboratorId: number,
547
+ paymentId: number
548
+ ) {
549
+ return mutateOperations<{ success: boolean }>(
550
+ request,
551
+ `/operations/collaborators/${collaboratorId}/payment-history/${paymentId}`,
552
+ 'DELETE'
553
+ );
554
+ }
555
+
556
+ // Collaborator Invoices
557
+
558
+ export type CollaboratorInvoice = {
559
+ id: number;
560
+ collaboratorId: number;
561
+ invoiceNumber: string | null;
562
+ amount: string;
563
+ issueDate: string;
564
+ dueDate: string | null;
565
+ status: string;
566
+ description: string | null;
567
+ createdAt: string;
568
+ };
569
+
570
+ export function fetchCollaboratorInvoices(
571
+ request: RequestFn,
572
+ collaboratorId: number
573
+ ) {
574
+ return fetchOperations<CollaboratorInvoice[]>(
575
+ request,
576
+ `/operations/collaborators/${collaboratorId}/invoices`
577
+ );
578
+ }
579
+
580
+ export function createCollaboratorInvoice(
581
+ request: RequestFn,
582
+ collaboratorId: number,
583
+ data: {
584
+ invoiceNumber?: string | null;
585
+ amount: number;
586
+ issueDate: string;
587
+ dueDate?: string | null;
588
+ status?: string;
589
+ description?: string | null;
590
+ }
591
+ ) {
592
+ return mutateOperations<CollaboratorInvoice>(
593
+ request,
594
+ `/operations/collaborators/${collaboratorId}/invoices`,
595
+ 'POST',
596
+ data
597
+ );
598
+ }
599
+
600
+ export function updateCollaboratorInvoice(
601
+ request: RequestFn,
602
+ collaboratorId: number,
603
+ invoiceId: number,
604
+ data: Partial<{
605
+ invoiceNumber: string | null;
606
+ amount: number;
607
+ issueDate: string;
608
+ dueDate: string | null;
609
+ status: string;
610
+ description: string | null;
611
+ }>
612
+ ) {
613
+ return mutateOperations<CollaboratorInvoice>(
614
+ request,
615
+ `/operations/collaborators/${collaboratorId}/invoices/${invoiceId}`,
616
+ 'PATCH',
617
+ data
618
+ );
619
+ }
620
+
621
+ export function deleteCollaboratorInvoice(
622
+ request: RequestFn,
623
+ collaboratorId: number,
624
+ invoiceId: number
625
+ ) {
626
+ return mutateOperations<{ success: boolean }>(
627
+ request,
628
+ `/operations/collaborators/${collaboratorId}/invoices/${invoiceId}`,
629
+ 'DELETE'
630
+ );
631
+ }
@@ -435,6 +435,7 @@ export type OperationsProject = {
435
435
  managerCollaboratorId?: number | null;
436
436
  clientPersonId?: number | null;
437
437
  clientAvatarId?: number | null;
438
+ clientUserPhotoId?: number | null;
438
439
  code: string;
439
440
  name: string;
440
441
  clientName?: string | null;
@@ -41,3 +41,21 @@ export function formatDurationMinutes(minutes?: number | null) {
41
41
  if (remainingMinutes <= 0) return `${hours}h`;
42
42
  return `${hours}h ${remainingMinutes}min`;
43
43
  }
44
+
45
+ export function getTaskDescriptionPreview(value?: string | null) {
46
+ if (!value) return '';
47
+
48
+ return String(value)
49
+ .replace(/<br\s*\/?>(?=)/gi, ' ')
50
+ .replace(/<\/(p|div|li|ul|ol|h1|h2|h3|h4|h5|h6)>/gi, ' ')
51
+ .replace(/<li[^>]*>/gi, ' ')
52
+ .replace(/<[^>]*>/g, '')
53
+ .replace(/&nbsp;/gi, ' ')
54
+ .replace(/&amp;/gi, '&')
55
+ .replace(/&lt;/gi, '<')
56
+ .replace(/&gt;/gi, '>')
57
+ .replace(/&#39;/gi, "'")
58
+ .replace(/&quot;/gi, '"')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+ }