@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
|
@@ -10,9 +10,17 @@ import {
|
|
|
10
10
|
SheetHeader,
|
|
11
11
|
SheetTitle,
|
|
12
12
|
} from '@/components/ui/sheet';
|
|
13
|
-
import {
|
|
13
|
+
import { Switch } from '@/components/ui/switch';
|
|
14
|
+
import {
|
|
15
|
+
Check,
|
|
16
|
+
Loader2,
|
|
17
|
+
Pencil,
|
|
18
|
+
Plus,
|
|
19
|
+
SlidersHorizontal,
|
|
20
|
+
X,
|
|
21
|
+
} from 'lucide-react';
|
|
14
22
|
import { useTranslations } from 'next-intl';
|
|
15
|
-
import { useState } from 'react';
|
|
23
|
+
import { useEffect, useState } from 'react';
|
|
16
24
|
import { fetchOperations, mutateOperations } from '../_lib/api';
|
|
17
25
|
import type {
|
|
18
26
|
OperationsCollaboratorDetails,
|
|
@@ -29,6 +37,7 @@ import { ProjectFormScreen } from './project-form-screen';
|
|
|
29
37
|
import { StatusBadge } from './status-badge';
|
|
30
38
|
|
|
31
39
|
const PAGE_SIZE = 10;
|
|
40
|
+
const LS_AUTO_REBALANCE_KEY = 'operations.projectAllocation.autoRebalance';
|
|
32
41
|
|
|
33
42
|
type AssignmentEditState = {
|
|
34
43
|
roleLabel: string;
|
|
@@ -53,14 +62,26 @@ export function ProjectAssignmentsTab({
|
|
|
53
62
|
}: ProjectAssignmentsTabProps) {
|
|
54
63
|
const commonT = useTranslations('operations.Common');
|
|
55
64
|
const detailsT = useTranslations('operations.CollaboratorDetailsPage');
|
|
65
|
+
const allocT = useTranslations(
|
|
66
|
+
'operations.CollaboratorFormPage.projectAllocation'
|
|
67
|
+
);
|
|
56
68
|
|
|
57
69
|
const [page, setPage] = useState(0);
|
|
58
70
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
59
71
|
const [editState, setEditState] = useState<AssignmentEditState | null>(null);
|
|
60
72
|
const [saving, setSaving] = useState(false);
|
|
73
|
+
const [distributing, setDistributing] = useState(false);
|
|
61
74
|
const [adding, setAdding] = useState(false);
|
|
62
75
|
const [createSheetOpen, setCreateSheetOpen] = useState(false);
|
|
63
76
|
const [pickerKey, setPickerKey] = useState(0);
|
|
77
|
+
const [autoRebalance, setAutoRebalance] = useState(() => {
|
|
78
|
+
if (typeof window === 'undefined') return false;
|
|
79
|
+
return localStorage.getItem(LS_AUTO_REBALANCE_KEY) === 'true';
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
localStorage.setItem(LS_AUTO_REBALANCE_KEY, String(autoRebalance));
|
|
84
|
+
}, [autoRebalance]);
|
|
64
85
|
|
|
65
86
|
const projects = collaborator?.assignedProjects ?? [];
|
|
66
87
|
const assignedIds = new Set(projects.map((p) => p.id));
|
|
@@ -70,6 +91,28 @@ export function ProjectAssignmentsTab({
|
|
|
70
91
|
(page + 1) * PAGE_SIZE
|
|
71
92
|
);
|
|
72
93
|
|
|
94
|
+
const savedTotal = projects.reduce(
|
|
95
|
+
(sum, p) => sum + (p.allocationPercent ?? 0),
|
|
96
|
+
0
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const editingProject =
|
|
100
|
+
editingId != null ? projects.find((p) => p.id === editingId) : null;
|
|
101
|
+
const projectedTotal =
|
|
102
|
+
editState && editingProject
|
|
103
|
+
? projects.reduce((sum, p) => {
|
|
104
|
+
if (p.id === editingId) {
|
|
105
|
+
return sum + (parseNumberInput(editState.allocationPercent) ?? 0);
|
|
106
|
+
}
|
|
107
|
+
return sum + (p.allocationPercent ?? 0);
|
|
108
|
+
}, 0)
|
|
109
|
+
: savedTotal;
|
|
110
|
+
|
|
111
|
+
const displayTotal = editState ? projectedTotal : savedTotal;
|
|
112
|
+
|
|
113
|
+
const allocationWarning =
|
|
114
|
+
displayTotal < 100 ? 'idle' : displayTotal > 100 ? 'overloaded' : null;
|
|
115
|
+
|
|
73
116
|
const startEditing = (
|
|
74
117
|
project: OperationsCollaboratorDetails['assignedProjects'][number]
|
|
75
118
|
) => {
|
|
@@ -98,6 +141,8 @@ export function ProjectAssignmentsTab({
|
|
|
98
141
|
if (!collaborator || !editState) return;
|
|
99
142
|
setSaving(true);
|
|
100
143
|
try {
|
|
144
|
+
const newPercent = parseNumberInput(editState.allocationPercent);
|
|
145
|
+
|
|
101
146
|
await mutateOperations(
|
|
102
147
|
request,
|
|
103
148
|
`/operations/collaborators/${collaborator.id}/projects/${projectId}`,
|
|
@@ -105,11 +150,64 @@ export function ProjectAssignmentsTab({
|
|
|
105
150
|
{
|
|
106
151
|
roleLabel: trimToNull(editState.roleLabel),
|
|
107
152
|
weeklyHours: parseNumberInput(editState.weeklyHours),
|
|
108
|
-
allocationPercent:
|
|
153
|
+
allocationPercent: newPercent,
|
|
109
154
|
startDate: trimToNull(editState.startDate),
|
|
110
155
|
endDate: trimToNull(editState.endDate),
|
|
111
156
|
}
|
|
112
157
|
);
|
|
158
|
+
|
|
159
|
+
if (autoRebalance && newPercent != null) {
|
|
160
|
+
const others = projects.filter((p) => p.id !== projectId);
|
|
161
|
+
if (others.length > 0) {
|
|
162
|
+
const remaining = 100 - newPercent;
|
|
163
|
+
const othersTotal = others.reduce(
|
|
164
|
+
(sum, p) => sum + (p.allocationPercent ?? 0),
|
|
165
|
+
0
|
|
166
|
+
);
|
|
167
|
+
await Promise.all(
|
|
168
|
+
others.map((p, idx) => {
|
|
169
|
+
let newValue: number;
|
|
170
|
+
if (othersTotal > 0) {
|
|
171
|
+
newValue =
|
|
172
|
+
Math.round(
|
|
173
|
+
((p.allocationPercent ?? 0) / othersTotal) * remaining * 100
|
|
174
|
+
) / 100;
|
|
175
|
+
} else {
|
|
176
|
+
newValue = Math.round((remaining / others.length) * 100) / 100;
|
|
177
|
+
}
|
|
178
|
+
if (idx === others.length - 1) {
|
|
179
|
+
const alreadyAssigned = others
|
|
180
|
+
.slice(0, idx)
|
|
181
|
+
.reduce((sum, op) => {
|
|
182
|
+
if (othersTotal > 0) {
|
|
183
|
+
return (
|
|
184
|
+
sum +
|
|
185
|
+
Math.round(
|
|
186
|
+
((op.allocationPercent ?? 0) / othersTotal) *
|
|
187
|
+
remaining *
|
|
188
|
+
100
|
|
189
|
+
) /
|
|
190
|
+
100
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return (
|
|
194
|
+
sum + Math.round((remaining / others.length) * 100) / 100
|
|
195
|
+
);
|
|
196
|
+
}, 0);
|
|
197
|
+
newValue =
|
|
198
|
+
Math.round((remaining - alreadyAssigned) * 100) / 100;
|
|
199
|
+
}
|
|
200
|
+
return mutateOperations(
|
|
201
|
+
request,
|
|
202
|
+
`/operations/collaborators/${collaborator.id}/projects/${p.id}`,
|
|
203
|
+
'PATCH',
|
|
204
|
+
{ allocationPercent: newValue }
|
|
205
|
+
);
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
113
211
|
setEditingId(null);
|
|
114
212
|
setEditState(null);
|
|
115
213
|
onUpdated();
|
|
@@ -120,6 +218,37 @@ export function ProjectAssignmentsTab({
|
|
|
120
218
|
}
|
|
121
219
|
};
|
|
122
220
|
|
|
221
|
+
const distributeEqually = async () => {
|
|
222
|
+
if (!collaborator) return;
|
|
223
|
+
const activeProjects = projects.filter((p) => p.status === 'active');
|
|
224
|
+
const targetProjects =
|
|
225
|
+
activeProjects.length > 0 ? activeProjects : projects;
|
|
226
|
+
if (targetProjects.length === 0) return;
|
|
227
|
+
setDistributing(true);
|
|
228
|
+
try {
|
|
229
|
+
const equalShare = Math.floor((100 / targetProjects.length) * 100) / 100;
|
|
230
|
+
const remainder =
|
|
231
|
+
Math.round((100 - equalShare * (targetProjects.length - 1)) * 100) /
|
|
232
|
+
100;
|
|
233
|
+
await Promise.all(
|
|
234
|
+
targetProjects.map((p, idx) =>
|
|
235
|
+
mutateOperations(
|
|
236
|
+
request,
|
|
237
|
+
`/operations/collaborators/${collaborator.id}/projects/${p.id}`,
|
|
238
|
+
'PATCH',
|
|
239
|
+
{
|
|
240
|
+
allocationPercent:
|
|
241
|
+
idx === targetProjects.length - 1 ? remainder : equalShare,
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
onUpdated();
|
|
247
|
+
} finally {
|
|
248
|
+
setDistributing(false);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
123
252
|
const assignProject = async (projectId: number) => {
|
|
124
253
|
if (!collaborator) return;
|
|
125
254
|
setAdding(true);
|
|
@@ -139,6 +268,69 @@ export function ProjectAssignmentsTab({
|
|
|
139
268
|
|
|
140
269
|
return (
|
|
141
270
|
<div className="space-y-3">
|
|
271
|
+
{projects.length > 0 && (
|
|
272
|
+
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border px-3 py-2">
|
|
273
|
+
<div className="flex items-center gap-3">
|
|
274
|
+
<span className="text-xs text-muted-foreground">
|
|
275
|
+
{allocT('total')}:
|
|
276
|
+
</span>
|
|
277
|
+
<span
|
|
278
|
+
className={`text-sm font-semibold ${
|
|
279
|
+
allocationWarning === 'overloaded'
|
|
280
|
+
? 'text-destructive'
|
|
281
|
+
: allocationWarning === 'idle'
|
|
282
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
283
|
+
: 'text-green-600 dark:text-green-400'
|
|
284
|
+
}`}
|
|
285
|
+
>
|
|
286
|
+
{displayTotal}%
|
|
287
|
+
</span>
|
|
288
|
+
{allocationWarning === 'overloaded' && (
|
|
289
|
+
<span className="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">
|
|
290
|
+
{allocT('overloaded')} +
|
|
291
|
+
{Math.round((displayTotal - 100) * 100) / 100}%
|
|
292
|
+
</span>
|
|
293
|
+
)}
|
|
294
|
+
{allocationWarning === 'idle' && (
|
|
295
|
+
<span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
|
|
296
|
+
{allocT('idle')} -{Math.round((100 - displayTotal) * 100) / 100}
|
|
297
|
+
%
|
|
298
|
+
</span>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
{!disabled && (
|
|
302
|
+
<div className="flex items-center gap-3">
|
|
303
|
+
<div className="flex items-center gap-1.5">
|
|
304
|
+
<SlidersHorizontal className="size-3 text-muted-foreground" />
|
|
305
|
+
<span className="text-[11px] text-muted-foreground">
|
|
306
|
+
{allocT('autoRebalance')}
|
|
307
|
+
</span>
|
|
308
|
+
<Switch
|
|
309
|
+
checked={autoRebalance}
|
|
310
|
+
onCheckedChange={setAutoRebalance}
|
|
311
|
+
className="scale-75"
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
<Button
|
|
315
|
+
type="button"
|
|
316
|
+
variant="outline"
|
|
317
|
+
size="sm"
|
|
318
|
+
className="h-6 px-2 text-[11px]"
|
|
319
|
+
disabled={distributing || projects.length === 0}
|
|
320
|
+
onClick={() => void distributeEqually()}
|
|
321
|
+
>
|
|
322
|
+
{distributing ? (
|
|
323
|
+
<Loader2 className="mr-1 size-3 animate-spin" />
|
|
324
|
+
) : null}
|
|
325
|
+
{distributing
|
|
326
|
+
? allocT('distributing')
|
|
327
|
+
: allocT('distributeEqually')}
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
142
334
|
{!disabled ? (
|
|
143
335
|
<div className="flex items-center gap-2">
|
|
144
336
|
<div className="min-w-0 flex-1">
|
|
@@ -161,8 +353,7 @@ export function ProjectAssignmentsTab({
|
|
|
161
353
|
return {
|
|
162
354
|
items: res.data.filter((proj) => !assignedIds.has(proj.id)),
|
|
163
355
|
hasMore:
|
|
164
|
-
(res.page ?? p) * (res.pageSize ?? ps) <
|
|
165
|
-
(res.total ?? 0),
|
|
356
|
+
(res.page ?? p) * (res.pageSize ?? ps) < (res.total ?? 0),
|
|
166
357
|
};
|
|
167
358
|
}}
|
|
168
359
|
getOptionValue={(opt) => opt.id}
|
|
@@ -194,7 +385,9 @@ export function ProjectAssignmentsTab({
|
|
|
194
385
|
) : null}
|
|
195
386
|
|
|
196
387
|
{projects.length === 0 ? (
|
|
197
|
-
<p className="text-sm text-muted-foreground">
|
|
388
|
+
<p className="text-sm text-muted-foreground">
|
|
389
|
+
{detailsT('noProjects')}
|
|
390
|
+
</p>
|
|
198
391
|
) : (
|
|
199
392
|
<>
|
|
200
393
|
<div className="space-y-2">
|
|
@@ -243,8 +436,13 @@ export function ProjectAssignmentsTab({
|
|
|
243
436
|
/>
|
|
244
437
|
</div>
|
|
245
438
|
<div className="space-y-1">
|
|
246
|
-
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
439
|
+
<div className="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
247
440
|
{commonT('labels.allocationPercent')}
|
|
441
|
+
{autoRebalance && (
|
|
442
|
+
<span className="rounded bg-primary/10 px-1 text-[9px] text-primary">
|
|
443
|
+
auto
|
|
444
|
+
</span>
|
|
445
|
+
)}
|
|
248
446
|
</div>
|
|
249
447
|
<div className="relative">
|
|
250
448
|
<Input
|
|
@@ -370,7 +568,9 @@ export function ProjectAssignmentsTab({
|
|
|
370
568
|
onClick={() => startEditing(project)}
|
|
371
569
|
>
|
|
372
570
|
<Pencil className="size-3" />
|
|
373
|
-
<span className="sr-only">
|
|
571
|
+
<span className="sr-only">
|
|
572
|
+
{commonT('actions.edit')}
|
|
573
|
+
</span>
|
|
374
574
|
</Button>
|
|
375
575
|
) : null}
|
|
376
576
|
</div>
|
|
@@ -417,7 +617,9 @@ export function ProjectAssignmentsTab({
|
|
|
417
617
|
<SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
|
|
418
618
|
<SheetHeader>
|
|
419
619
|
<SheetTitle>{detailsT('createProject')}</SheetTitle>
|
|
420
|
-
<SheetDescription>
|
|
620
|
+
<SheetDescription>
|
|
621
|
+
{detailsT('createProjectDescription')}
|
|
622
|
+
</SheetDescription>
|
|
421
623
|
</SheetHeader>
|
|
422
624
|
<ProjectFormScreen
|
|
423
625
|
onCancel={() => setCreateSheetOpen(false)}
|
|
@@ -432,4 +634,4 @@ export function ProjectAssignmentsTab({
|
|
|
432
634
|
</Sheet>
|
|
433
635
|
</div>
|
|
434
636
|
);
|
|
435
|
-
}
|
|
637
|
+
}
|