@hed-hog/operations 0.0.332 → 0.0.338

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 (109) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +0 -54
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +0 -100
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/controllers/operations-contracts.controller.d.ts +12 -12
  6. package/dist/operations.service.d.ts +0 -76
  7. package/dist/operations.service.d.ts.map +1 -1
  8. package/dist/operations.service.js +7 -230
  9. package/dist/operations.service.js.map +1 -1
  10. package/dist/operations.service.spec.js +6 -0
  11. package/dist/operations.service.spec.js.map +1 -1
  12. package/hedhog/data/menu.yaml +8 -27
  13. package/hedhog/data/route.yaml +0 -72
  14. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
  15. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +3 -39
  16. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
  17. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
  18. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
  19. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  20. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +5 -4
  21. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
  22. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +10 -218
  23. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
  24. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +24 -708
  25. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +38 -158
  26. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +1 -5
  27. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
  28. package/hedhog/frontend/app/_lib/api.ts.ejs +0 -151
  29. package/hedhog/frontend/app/_lib/types.ts.ejs +0 -1
  30. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +0 -18
  31. package/hedhog/frontend/app/my-projects/page.tsx.ejs +2 -16
  32. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +86 -24
  33. package/hedhog/frontend/app/projects/page.tsx.ejs +6 -42
  34. package/hedhog/frontend/messages/en.json +2 -96
  35. package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
  36. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  37. package/hedhog/frontend/messages/pt.json +2 -96
  38. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
  39. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
  40. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
  41. package/hedhog/frontend/widgets/index.ts.ejs +25 -25
  42. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
  43. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
  44. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
  45. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
  46. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
  47. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
  48. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
  49. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
  50. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
  51. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
  52. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
  53. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
  54. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
  55. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
  56. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
  57. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
  58. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
  59. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
  60. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
  61. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
  62. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
  63. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
  64. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
  65. package/hedhog/table/operations_collaborator.yaml +8 -8
  66. package/hedhog/table/operations_task.yaml +76 -76
  67. package/hedhog/table/operations_task_activity.yaml +51 -51
  68. package/package.json +6 -6
  69. package/src/controllers/operations-collaborators.controller.ts +8 -117
  70. package/src/controllers/operations-tasks.controller.ts +156 -156
  71. package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
  72. package/src/dto/create-collaborator.dto.ts +4 -4
  73. package/src/operations.service.spec.ts +1006 -988
  74. package/src/operations.service.ts +7 -323
  75. package/dist/dto/create-collaborator-invoice.dto.d.ts +0 -11
  76. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +0 -1
  77. package/dist/dto/create-collaborator-invoice.dto.js +0 -55
  78. package/dist/dto/create-collaborator-invoice.dto.js.map +0 -1
  79. package/dist/dto/create-collaborator-payment.dto.d.ts +0 -10
  80. package/dist/dto/create-collaborator-payment.dto.d.ts.map +0 -1
  81. package/dist/dto/create-collaborator-payment.dto.js +0 -50
  82. package/dist/dto/create-collaborator-payment.dto.js.map +0 -1
  83. package/dist/dto/list-collaborator-invoice.dto.d.ts +0 -4
  84. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +0 -1
  85. package/dist/dto/list-collaborator-invoice.dto.js +0 -8
  86. package/dist/dto/list-collaborator-invoice.dto.js.map +0 -1
  87. package/dist/dto/list-collaborator-payment.dto.d.ts +0 -4
  88. package/dist/dto/list-collaborator-payment.dto.d.ts.map +0 -1
  89. package/dist/dto/list-collaborator-payment.dto.js +0 -8
  90. package/dist/dto/list-collaborator-payment.dto.js.map +0 -1
  91. package/dist/dto/update-collaborator-invoice.dto.d.ts +0 -6
  92. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +0 -1
  93. package/dist/dto/update-collaborator-invoice.dto.js +0 -9
  94. package/dist/dto/update-collaborator-invoice.dto.js.map +0 -1
  95. package/dist/dto/update-collaborator-payment.dto.d.ts +0 -6
  96. package/dist/dto/update-collaborator-payment.dto.d.ts.map +0 -1
  97. package/dist/dto/update-collaborator-payment.dto.js +0 -9
  98. package/dist/dto/update-collaborator-payment.dto.js.map +0 -1
  99. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +0 -443
  100. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +0 -429
  101. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +0 -953
  102. package/hedhog/table/operations_collaborator_invoice.yaml +0 -35
  103. package/hedhog/table/operations_collaborator_payment.yaml +0 -32
  104. package/src/dto/create-collaborator-invoice.dto.ts +0 -39
  105. package/src/dto/create-collaborator-payment.dto.ts +0 -35
  106. package/src/dto/list-collaborator-invoice.dto.ts +0 -3
  107. package/src/dto/list-collaborator-payment.dto.ts +0 -3
  108. package/src/dto/update-collaborator-invoice.dto.ts +0 -6
  109. package/src/dto/update-collaborator-payment.dto.ts +0 -6
@@ -82,12 +82,12 @@ export function CollaboratorTimesheetsTab({
82
82
  if (!timesheet.approvalId) return;
83
83
  setApprovingId(timesheet.id);
84
84
  try {
85
- await mutateOperations(
86
- request,
87
- `/operations/approvals/${timesheet.approvalId}/approve`,
88
- 'POST',
89
- {}
90
- );
85
+ await mutateOperations(
86
+ request,
87
+ `/operations/approvals/${timesheet.approvalId}/approve`,
88
+ 'POST',
89
+ {}
90
+ );
91
91
  void refetch();
92
92
  } finally {
93
93
  setApprovingId(null);
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { RichTextEditor } from '@/components/rich-text-editor';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
6
+ import { Textarea } from '@/components/ui/textarea';
7
+ import { useApp } from '@hed-hog/next-app-provider';
8
+ import { LoaderCircle, Sparkles } from 'lucide-react';
9
+ import { useTranslations } from 'next-intl';
10
+ import { useMemo, useState } from 'react';
11
+ import { mutateOperations } from '../_lib/api';
12
+ import { SectionCard } from './section-card';
13
+
14
+ type ContractContentEditorProps = {
15
+ value: string;
16
+ onChange: (value: string) => void;
17
+ editorTitle: string;
18
+ editorDescription: string;
19
+ previewTitle: string;
20
+ previewDescription: string;
21
+ previewFallbackHtml?: string;
22
+ promptContext?: Record<string, string | number | boolean | null | undefined>;
23
+ compact?: boolean;
24
+ descriptionMode?: 'inline' | 'tooltip';
25
+ layout?: 'split' | 'tabs';
26
+ showPreview?: boolean;
27
+ chrome?: 'card' | 'plain';
28
+ };
29
+
30
+ export function ContractContentEditor({
31
+ value,
32
+ onChange,
33
+ editorTitle,
34
+ editorDescription,
35
+ previewTitle,
36
+ previewDescription,
37
+ previewFallbackHtml,
38
+ promptContext,
39
+ compact = false,
40
+ descriptionMode = 'inline',
41
+ layout = 'split',
42
+ showPreview = true,
43
+ chrome = 'card',
44
+ }: ContractContentEditorProps) {
45
+ const t = useTranslations('operations.ContractContentEditor');
46
+ const { request, showToastHandler, getSettingValue } = useApp();
47
+ const [prompt, setPrompt] = useState('');
48
+ const [isGenerating, setIsGenerating] = useState(false);
49
+
50
+ const hasOpenAi = Boolean(getSettingValue('ai-openai-api-key-enabled'));
51
+ const hasGemini = Boolean(getSettingValue('ai-gemini-api-key-enabled'));
52
+ const provider = hasOpenAi ? 'openai' : hasGemini ? 'gemini' : null;
53
+ const providerLabel = provider === 'openai' ? 'OpenAI' : 'Gemini';
54
+
55
+ const promptContextSummary = useMemo(
56
+ () =>
57
+ Object.entries(promptContext ?? {})
58
+ .filter(
59
+ ([, rawValue]) =>
60
+ rawValue !== null &&
61
+ rawValue !== undefined &&
62
+ String(rawValue).trim()
63
+ )
64
+ .map(([key, rawValue]) => `${key}: ${String(rawValue)}`)
65
+ .join('\n'),
66
+ [promptContext]
67
+ );
68
+
69
+ const fillSuggestedPrompt = () => {
70
+ setPrompt(
71
+ t('suggestedPrompt', {
72
+ subject: String(
73
+ promptContext?.name ||
74
+ promptContext?.project_name ||
75
+ promptContext?.client_name ||
76
+ 'contrato'
77
+ ),
78
+ })
79
+ );
80
+ };
81
+
82
+ const generateWithAi = async () => {
83
+ if (!provider) {
84
+ showToastHandler?.('error', t('messages.missingConfiguration'));
85
+ return;
86
+ }
87
+
88
+ const trimmedPrompt = prompt.trim();
89
+ if (!trimmedPrompt) {
90
+ showToastHandler?.('error', t('messages.missingPrompt'));
91
+ return;
92
+ }
93
+
94
+ setIsGenerating(true);
95
+
96
+ try {
97
+ const response = await mutateOperations<{ content?: string }>(
98
+ request,
99
+ '/ai/chat',
100
+ 'POST',
101
+ {
102
+ provider,
103
+ message: trimmedPrompt,
104
+ systemPrompt: [
105
+ 'You are drafting professional business contract content for a back-office operations system.',
106
+ 'Return ONLY clean HTML suitable for a rich text editor preview.',
107
+ 'Do not include Markdown fences, explanations, or commentary.',
108
+ 'When useful, keep placeholders like {{client_name}}, {{project_name}}, {{project_code}}, {{start_date}}, {{end_date}}, {{budget_amount}}, and {{monthly_hour_cap}} in the generated content.',
109
+ 'Prefer short sections, headings, bullet points, and concise legal-style language.',
110
+ value?.trim()
111
+ ? `Current HTML content to improve or rewrite:\n${value}`
112
+ : 'There is no current contract content yet.',
113
+ promptContextSummary
114
+ ? `Context:\n${promptContextSummary}`
115
+ : 'No extra context was provided.',
116
+ ].join('\n\n'),
117
+ }
118
+ );
119
+
120
+ const generatedHtml = String(response?.content ?? '').trim();
121
+ if (!generatedHtml) {
122
+ throw new Error('Empty AI response');
123
+ }
124
+
125
+ onChange(generatedHtml);
126
+ showToastHandler?.(
127
+ 'success',
128
+ t('messages.generateSuccess', { provider: providerLabel })
129
+ );
130
+ } catch {
131
+ showToastHandler?.('error', t('messages.generateError'));
132
+ } finally {
133
+ setIsGenerating(false);
134
+ }
135
+ };
136
+
137
+ const renderSection = (
138
+ title: string,
139
+ description: string,
140
+ content: React.ReactNode
141
+ ) => {
142
+ if (chrome === 'plain') {
143
+ return (
144
+ <div className="space-y-3">
145
+ <div className="space-y-1">
146
+ <h3 className="text-lg font-semibold">{title}</h3>
147
+ <p className="text-sm text-muted-foreground">{description}</p>
148
+ </div>
149
+ {content}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ return (
155
+ <SectionCard
156
+ title={title}
157
+ description={description}
158
+ compact={compact}
159
+ descriptionMode={descriptionMode}
160
+ >
161
+ {content}
162
+ </SectionCard>
163
+ );
164
+ };
165
+
166
+ const editorSection = renderSection(
167
+ editorTitle,
168
+ editorDescription,
169
+ <RichTextEditor value={value} onChange={onChange} />
170
+ );
171
+
172
+ const previewSection = renderSection(
173
+ previewTitle,
174
+ previewDescription,
175
+ <div
176
+ className="prose prose-sm max-w-none rounded-lg border p-4"
177
+ dangerouslySetInnerHTML={{
178
+ __html: value || previewFallbackHtml || `<p>${t('emptyPreview')}</p>`,
179
+ }}
180
+ />
181
+ );
182
+
183
+ return (
184
+ <div className="space-y-4">
185
+ {provider ? (
186
+ <div className="rounded-lg border border-dashed px-3 py-3">
187
+ <div className="flex flex-wrap items-start justify-between gap-3">
188
+ <div className="space-y-1">
189
+ <div className="flex items-center gap-2 text-sm font-medium">
190
+ <Sparkles className="size-4 text-primary" />
191
+ <span>{t('assistantTitle')}</span>
192
+ </div>
193
+ <p className="text-xs text-muted-foreground">
194
+ {t('assistantDescription', { provider: providerLabel })}
195
+ </p>
196
+ </div>
197
+ <Button
198
+ type="button"
199
+ variant="outline"
200
+ size="sm"
201
+ className="cursor-pointer"
202
+ onClick={fillSuggestedPrompt}
203
+ >
204
+ {t('actions.useSuggestion')}
205
+ </Button>
206
+ </div>
207
+
208
+ <div className="mt-3 space-y-2">
209
+ <Textarea
210
+ rows={compact ? 2 : 3}
211
+ value={prompt}
212
+ placeholder={t('promptPlaceholder')}
213
+ onChange={(event) => setPrompt(event.target.value)}
214
+ />
215
+ <div className="flex flex-wrap gap-2">
216
+ <Button
217
+ type="button"
218
+ size="sm"
219
+ className="cursor-pointer"
220
+ disabled={isGenerating}
221
+ onClick={() => void generateWithAi()}
222
+ >
223
+ {isGenerating ? (
224
+ <LoaderCircle className="size-4 animate-spin" />
225
+ ) : (
226
+ <Sparkles className="size-4" />
227
+ )}
228
+ {t('actions.generate')}
229
+ </Button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ ) : null}
234
+
235
+ {!showPreview ? (
236
+ editorSection
237
+ ) : layout === 'tabs' ? (
238
+ <Tabs defaultValue="editor" className="w-full">
239
+ <TabsList className="grid w-full grid-cols-2">
240
+ <TabsTrigger value="editor">{editorTitle}</TabsTrigger>
241
+ <TabsTrigger value="preview">{previewTitle}</TabsTrigger>
242
+ </TabsList>
243
+ <TabsContent value="editor" className="mt-4">
244
+ {editorSection}
245
+ </TabsContent>
246
+ <TabsContent value="preview" className="mt-4">
247
+ {previewSection}
248
+ </TabsContent>
249
+ </Tabs>
250
+ ) : (
251
+ <div className="grid gap-4 xl:grid-cols-2">
252
+ {editorSection}
253
+ {previewSection}
254
+ </div>
255
+ )}
256
+ </div>
257
+ );
258
+ }
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
- import { RichTextEditor } from '@/components/rich-text-editor';
5
4
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
6
5
  import { Button } from '@/components/ui/button';
7
6
  import {
@@ -37,6 +36,7 @@ import {
37
36
  TableRow,
38
37
  } from '@/components/ui/table';
39
38
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
39
+ import { RichTextEditor } from '@/components/rich-text-editor';
40
40
  import {
41
41
  closestCenter,
42
42
  DndContext,
@@ -85,8 +85,8 @@ import { OperationsHeader } from './operations-header';
85
85
  import { SectionCard } from './section-card';
86
86
  import { StatusBadge } from './status-badge';
87
87
  import {
88
- TaskCommentsSection,
89
88
  TaskDetailSheet,
89
+ TaskCommentsSection,
90
90
  type TaskDetailSheetData,
91
91
  } from './task-detail-sheet';
92
92
  import { TaskFileAttachments } from './task-file-attachments';
@@ -834,7 +834,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
834
834
  {(isOver) => (
835
835
  <div
836
836
  className={[
837
- 'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
837
+ 'flex min-h-128 flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
838
838
  getColumnClassName(column.id),
839
839
  isOver
840
840
  ? 'border-primary shadow-lg ring-2 ring-primary/15'
@@ -870,7 +870,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
870
870
  </div>
871
871
  </div>
872
872
 
873
- <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
873
+ <div className="flex flex-1 flex-col gap-2">
874
874
  <AnimatePresence initial={false}>
875
875
  {taskColumns[column.id].map((task) => {
876
876
  const tags = getTaskTags(task);
@@ -1309,6 +1309,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
1309
1309
  }}
1310
1310
  />
1311
1311
 
1312
+
1312
1313
  <Sheet
1313
1314
  open={taskFormOpen}
1314
1315
  onOpenChange={(open) => {
@@ -0,0 +1 @@
1
+ export { PersonPicker as PersonSelectWithCreate } from '../../contact/_components/person-picker';
@@ -10,17 +10,9 @@ import {
10
10
  SheetHeader,
11
11
  SheetTitle,
12
12
  } from '@/components/ui/sheet';
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';
13
+ import { Check, Loader2, Pencil, Plus, X } from 'lucide-react';
22
14
  import { useTranslations } from 'next-intl';
23
- import { useEffect, useState } from 'react';
15
+ import { useState } from 'react';
24
16
  import { fetchOperations, mutateOperations } from '../_lib/api';
25
17
  import type {
26
18
  OperationsCollaboratorDetails,
@@ -37,7 +29,6 @@ import { ProjectFormScreen } from './project-form-screen';
37
29
  import { StatusBadge } from './status-badge';
38
30
 
39
31
  const PAGE_SIZE = 10;
40
- const LS_AUTO_REBALANCE_KEY = 'operations.projectAllocation.autoRebalance';
41
32
 
42
33
  type AssignmentEditState = {
43
34
  roleLabel: string;
@@ -62,26 +53,14 @@ export function ProjectAssignmentsTab({
62
53
  }: ProjectAssignmentsTabProps) {
63
54
  const commonT = useTranslations('operations.Common');
64
55
  const detailsT = useTranslations('operations.CollaboratorDetailsPage');
65
- const allocT = useTranslations(
66
- 'operations.CollaboratorFormPage.projectAllocation'
67
- );
68
56
 
69
57
  const [page, setPage] = useState(0);
70
58
  const [editingId, setEditingId] = useState<number | null>(null);
71
59
  const [editState, setEditState] = useState<AssignmentEditState | null>(null);
72
60
  const [saving, setSaving] = useState(false);
73
- const [distributing, setDistributing] = useState(false);
74
61
  const [adding, setAdding] = useState(false);
75
62
  const [createSheetOpen, setCreateSheetOpen] = useState(false);
76
63
  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]);
85
64
 
86
65
  const projects = collaborator?.assignedProjects ?? [];
87
66
  const assignedIds = new Set(projects.map((p) => p.id));
@@ -91,29 +70,6 @@ export function ProjectAssignmentsTab({
91
70
  (page + 1) * PAGE_SIZE
92
71
  );
93
72
 
94
- const savedTotal = projects.reduce(
95
- (sum, p) => sum + (p.allocationPercent ?? 0),
96
- 0
97
- );
98
-
99
- // When editing, show projected total
100
- const editingProject =
101
- editingId != null ? projects.find((p) => p.id === editingId) : null;
102
- const projectedTotal =
103
- editState && editingProject
104
- ? projects.reduce((sum, p) => {
105
- if (p.id === editingId) {
106
- return sum + (parseNumberInput(editState.allocationPercent) ?? 0);
107
- }
108
- return sum + (p.allocationPercent ?? 0);
109
- }, 0)
110
- : savedTotal;
111
-
112
- const displayTotal = editState ? projectedTotal : savedTotal;
113
-
114
- const allocationWarning =
115
- displayTotal < 100 ? 'idle' : displayTotal > 100 ? 'overloaded' : null;
116
-
117
73
  const startEditing = (
118
74
  project: OperationsCollaboratorDetails['assignedProjects'][number]
119
75
  ) => {
@@ -142,8 +98,6 @@ export function ProjectAssignmentsTab({
142
98
  if (!collaborator || !editState) return;
143
99
  setSaving(true);
144
100
  try {
145
- const newPercent = parseNumberInput(editState.allocationPercent);
146
-
147
101
  await mutateOperations(
148
102
  request,
149
103
  `/operations/collaborators/${collaborator.id}/projects/${projectId}`,
@@ -151,66 +105,11 @@ export function ProjectAssignmentsTab({
151
105
  {
152
106
  roleLabel: trimToNull(editState.roleLabel),
153
107
  weeklyHours: parseNumberInput(editState.weeklyHours),
154
- allocationPercent: newPercent,
108
+ allocationPercent: parseNumberInput(editState.allocationPercent),
155
109
  startDate: trimToNull(editState.startDate),
156
110
  endDate: trimToNull(editState.endDate),
157
111
  }
158
112
  );
159
-
160
- // Auto-rebalance: distribute remaining % among other projects
161
- if (autoRebalance && newPercent != null) {
162
- const others = projects.filter((p) => p.id !== projectId);
163
- if (others.length > 0) {
164
- const remaining = 100 - newPercent;
165
- const othersTotal = others.reduce(
166
- (sum, p) => sum + (p.allocationPercent ?? 0),
167
- 0
168
- );
169
- await Promise.all(
170
- others.map((p, idx) => {
171
- let newValue: number;
172
- if (othersTotal > 0) {
173
- newValue =
174
- Math.round(
175
- ((p.allocationPercent ?? 0) / othersTotal) * remaining * 100
176
- ) / 100;
177
- } else {
178
- newValue = Math.round((remaining / others.length) * 100) / 100;
179
- }
180
- // Ensure the last item corrects rounding drift
181
- if (idx === others.length - 1) {
182
- const alreadyAssigned = others
183
- .slice(0, idx)
184
- .reduce((sum, op) => {
185
- if (othersTotal > 0) {
186
- return (
187
- sum +
188
- Math.round(
189
- ((op.allocationPercent ?? 0) / othersTotal) *
190
- remaining *
191
- 100
192
- ) /
193
- 100
194
- );
195
- }
196
- return (
197
- sum + Math.round((remaining / others.length) * 100) / 100
198
- );
199
- }, 0);
200
- newValue =
201
- Math.round((remaining - alreadyAssigned) * 100) / 100;
202
- }
203
- return mutateOperations(
204
- request,
205
- `/operations/collaborators/${collaborator.id}/projects/${p.id}`,
206
- 'PATCH',
207
- { allocationPercent: newValue }
208
- );
209
- })
210
- );
211
- }
212
- }
213
-
214
113
  setEditingId(null);
215
114
  setEditState(null);
216
115
  onUpdated();
@@ -221,37 +120,6 @@ export function ProjectAssignmentsTab({
221
120
  }
222
121
  };
223
122
 
224
- const distributeEqually = async () => {
225
- if (!collaborator) return;
226
- const activeProjects = projects.filter((p) => p.status === 'active');
227
- const targetProjects =
228
- activeProjects.length > 0 ? activeProjects : projects;
229
- if (targetProjects.length === 0) return;
230
- setDistributing(true);
231
- try {
232
- const equalShare = Math.floor((100 / targetProjects.length) * 100) / 100;
233
- const remainder =
234
- Math.round((100 - equalShare * (targetProjects.length - 1)) * 100) /
235
- 100;
236
- await Promise.all(
237
- targetProjects.map((p, idx) =>
238
- mutateOperations(
239
- request,
240
- `/operations/collaborators/${collaborator.id}/projects/${p.id}`,
241
- 'PATCH',
242
- {
243
- allocationPercent:
244
- idx === targetProjects.length - 1 ? remainder : equalShare,
245
- }
246
- )
247
- )
248
- );
249
- onUpdated();
250
- } finally {
251
- setDistributing(false);
252
- }
253
- };
254
-
255
123
  const assignProject = async (projectId: number) => {
256
124
  if (!collaborator) return;
257
125
  setAdding(true);
@@ -271,72 +139,6 @@ export function ProjectAssignmentsTab({
271
139
 
272
140
  return (
273
141
  <div className="space-y-3">
274
- {/* Allocation summary bar */}
275
- {projects.length > 0 && (
276
- <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border px-3 py-2">
277
- <div className="flex items-center gap-3">
278
- <span className="text-xs text-muted-foreground">
279
- {allocT('total')}:
280
- </span>
281
- <span
282
- className={`text-sm font-semibold ${
283
- allocationWarning === 'overloaded'
284
- ? 'text-destructive'
285
- : allocationWarning === 'idle'
286
- ? 'text-amber-600 dark:text-amber-400'
287
- : 'text-green-600 dark:text-green-400'
288
- }`}
289
- >
290
- {displayTotal}%
291
- </span>
292
- {allocationWarning === 'overloaded' && (
293
- <span className="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">
294
- {allocT('overloaded')} +
295
- {Math.round((displayTotal - 100) * 100) / 100}%
296
- </span>
297
- )}
298
- {allocationWarning === 'idle' && (
299
- <span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-[10px] font-medium text-amber-600 dark:text-amber-400">
300
- {allocT('idle')} -{Math.round((100 - displayTotal) * 100) / 100}
301
- %
302
- </span>
303
- )}
304
- </div>
305
- {!disabled && (
306
- <div className="flex items-center gap-3">
307
- {/* Auto-rebalance toggle */}
308
- <div className="flex items-center gap-1.5">
309
- <SlidersHorizontal className="size-3 text-muted-foreground" />
310
- <span className="text-[11px] text-muted-foreground">
311
- {allocT('autoRebalance')}
312
- </span>
313
- <Switch
314
- checked={autoRebalance}
315
- onCheckedChange={setAutoRebalance}
316
- className="scale-75"
317
- />
318
- </div>
319
- {/* Distribute equally */}
320
- <Button
321
- type="button"
322
- variant="outline"
323
- size="sm"
324
- className="h-6 px-2 text-[11px]"
325
- disabled={distributing || projects.length === 0}
326
- onClick={() => void distributeEqually()}
327
- >
328
- {distributing ? (
329
- <Loader2 className="mr-1 size-3 animate-spin" />
330
- ) : null}
331
- {distributing
332
- ? allocT('distributing')
333
- : allocT('distributeEqually')}
334
- </Button>
335
- </div>
336
- )}
337
- </div>
338
- )}
339
-
340
142
  {!disabled ? (
341
143
  <div className="flex items-center gap-2">
342
144
  <div className="min-w-0 flex-1">
@@ -359,7 +161,8 @@ export function ProjectAssignmentsTab({
359
161
  return {
360
162
  items: res.data.filter((proj) => !assignedIds.has(proj.id)),
361
163
  hasMore:
362
- (res.page ?? p) * (res.pageSize ?? ps) < (res.total ?? 0),
164
+ (res.page ?? p) * (res.pageSize ?? ps) <
165
+ (res.total ?? 0),
363
166
  };
364
167
  }}
365
168
  getOptionValue={(opt) => opt.id}
@@ -391,9 +194,7 @@ export function ProjectAssignmentsTab({
391
194
  ) : null}
392
195
 
393
196
  {projects.length === 0 ? (
394
- <p className="text-sm text-muted-foreground">
395
- {detailsT('noProjects')}
396
- </p>
197
+ <p className="text-sm text-muted-foreground">{detailsT('noProjects')}</p>
397
198
  ) : (
398
199
  <>
399
200
  <div className="space-y-2">
@@ -442,13 +243,8 @@ export function ProjectAssignmentsTab({
442
243
  />
443
244
  </div>
444
245
  <div className="space-y-1">
445
- <div className="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground">
246
+ <div className="text-[10px] uppercase tracking-wide text-muted-foreground">
446
247
  {commonT('labels.allocationPercent')}
447
- {autoRebalance && (
448
- <span className="rounded bg-primary/10 px-1 text-[9px] text-primary">
449
- auto
450
- </span>
451
- )}
452
248
  </div>
453
249
  <div className="relative">
454
250
  <Input
@@ -574,9 +370,7 @@ export function ProjectAssignmentsTab({
574
370
  onClick={() => startEditing(project)}
575
371
  >
576
372
  <Pencil className="size-3" />
577
- <span className="sr-only">
578
- {commonT('actions.edit')}
579
- </span>
373
+ <span className="sr-only">{commonT('actions.edit')}</span>
580
374
  </Button>
581
375
  ) : null}
582
376
  </div>
@@ -623,9 +417,7 @@ export function ProjectAssignmentsTab({
623
417
  <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
624
418
  <SheetHeader>
625
419
  <SheetTitle>{detailsT('createProject')}</SheetTitle>
626
- <SheetDescription>
627
- {detailsT('createProjectDescription')}
628
- </SheetDescription>
420
+ <SheetDescription>{detailsT('createProjectDescription')}</SheetDescription>
629
421
  </SheetHeader>
630
422
  <ProjectFormScreen
631
423
  onCancel={() => setCreateSheetOpen(false)}
@@ -640,4 +432,4 @@ export function ProjectAssignmentsTab({
640
432
  </Sheet>
641
433
  </div>
642
434
  );
643
- }
435
+ }