@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.
- package/dist/controllers/operations-collaborators.controller.d.ts +73 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +100 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/controllers/operations-contracts.controller.d.ts +15 -15
- package/dist/controllers/operations-projects.controller.d.ts +3 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.js +55 -0
- package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.js +50 -0
- package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.js +8 -0
- package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.js +8 -0
- package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.js +9 -0
- package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.js +9 -0
- package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
- package/dist/operations.service.d.ts +98 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +226 -3
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +32 -11
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +38 -0
- package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
- package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +212 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +668 -11
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +182 -28
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +28 -7
- package/hedhog/frontend/app/_lib/api.ts.ejs +151 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
- package/hedhog/frontend/messages/en.json +96 -2
- package/hedhog/frontend/messages/pt.json +96 -2
- package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
- package/hedhog/table/operations_collaborator_payment.yaml +32 -0
- package/package.json +4 -4
- package/src/controllers/operations-collaborators.controller.ts +109 -0
- package/src/dto/create-collaborator-invoice.dto.ts +39 -0
- package/src/dto/create-collaborator-payment.dto.ts +35 -0
- package/src/dto/list-collaborator-invoice.dto.ts +3 -0
- package/src/dto/list-collaborator-payment.dto.ts +3 -0
- package/src/dto/update-collaborator-invoice.dto.ts +6 -0
- package/src/dto/update-collaborator-payment.dto.ts +6 -0
- 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
|
-
|
|
878
|
-
|
|
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
|
-
<
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
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 {
|
|
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({
|
|
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]">
|
|
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]"
|
|
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
|
-
<
|
|
663
|
+
<CommentContent content={task.description} />
|
|
652
664
|
</div>
|
|
653
665
|
) : null}
|
|
654
666
|
|
|
655
667
|
<Tabs
|
|
656
668
|
value={tab}
|
|
657
|
-
onValueChange={(v) =>
|
|
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(
|
|
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(/ /gi, ' ')
|
|
54
|
+
.replace(/&/gi, '&')
|
|
55
|
+
.replace(/</gi, '<')
|
|
56
|
+
.replace(/>/gi, '>')
|
|
57
|
+
.replace(/'/gi, "'")
|
|
58
|
+
.replace(/"/gi, '"')
|
|
59
|
+
.replace(/\s+/g, ' ')
|
|
60
|
+
.trim();
|
|
61
|
+
}
|