@hed-hog/operations 0.0.331 → 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 (58) hide show
  1. package/dist/controllers/operations-contracts.controller.d.ts +12 -12
  2. package/dist/operations.service.d.ts.map +1 -1
  3. package/dist/operations.service.js +8 -1
  4. package/dist/operations.service.js.map +1 -1
  5. package/dist/operations.service.spec.js +6 -0
  6. package/dist/operations.service.spec.js.map +1 -1
  7. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
  8. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
  9. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
  10. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
  11. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  12. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +84 -84
  13. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
  14. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
  15. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +4 -4
  16. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +803 -803
  17. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
  18. package/hedhog/frontend/app/_lib/api.ts.ejs +480 -480
  19. package/hedhog/frontend/app/_lib/types.ts.ejs +5 -5
  20. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +74 -74
  21. package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
  22. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  23. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
  24. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
  25. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
  26. package/hedhog/frontend/widgets/index.ts.ejs +25 -25
  27. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
  28. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
  29. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
  30. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
  31. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
  32. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
  33. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
  34. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
  35. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
  36. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
  37. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
  38. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
  39. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
  40. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
  41. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
  42. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
  43. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
  44. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
  45. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
  46. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
  47. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
  48. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
  49. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
  50. package/hedhog/table/operations_collaborator.yaml +8 -8
  51. package/hedhog/table/operations_task.yaml +76 -76
  52. package/hedhog/table/operations_task_activity.yaml +51 -51
  53. package/package.json +5 -5
  54. package/src/controllers/operations-tasks.controller.ts +156 -156
  55. package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
  56. package/src/dto/create-collaborator.dto.ts +4 -4
  57. package/src/operations.service.spec.ts +1006 -988
  58. package/src/operations.service.ts +8 -1
@@ -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
+ }
@@ -3,40 +3,40 @@
3
3
  import { EmptyState, Page } from '@/components/entity-list';
4
4
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
5
5
  import { Button } from '@/components/ui/button';
6
- import {
7
- Dialog,
8
- DialogContent,
9
- DialogDescription,
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
10
  DialogFooter,
11
11
  DialogHeader,
12
- DialogTitle,
13
- } from '@/components/ui/dialog';
14
- import { Input } from '@/components/ui/input';
15
- import { Label } from '@/components/ui/label';
16
- import { Progress } from '@/components/ui/progress';
17
- import {
18
- Select,
19
- SelectContent,
20
- SelectItem,
21
- SelectTrigger,
22
- SelectValue,
23
- } from '@/components/ui/select';
24
- import {
25
- Sheet,
26
- SheetContent,
27
- SheetHeader,
28
- SheetTitle,
29
- } from '@/components/ui/sheet';
30
- import {
31
- Table,
32
- TableBody,
33
- TableCell,
12
+ DialogTitle,
13
+ } from '@/components/ui/dialog';
14
+ import { Input } from '@/components/ui/input';
15
+ import { Label } from '@/components/ui/label';
16
+ import { Progress } from '@/components/ui/progress';
17
+ import {
18
+ Select,
19
+ SelectContent,
20
+ SelectItem,
21
+ SelectTrigger,
22
+ SelectValue,
23
+ } from '@/components/ui/select';
24
+ import {
25
+ Sheet,
26
+ SheetContent,
27
+ SheetHeader,
28
+ SheetTitle,
29
+ } from '@/components/ui/sheet';
30
+ import {
31
+ Table,
32
+ TableBody,
33
+ TableCell,
34
34
  TableHead,
35
35
  TableHeader,
36
- TableRow,
37
- } from '@/components/ui/table';
38
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
39
- import { RichTextEditor } from '@/components/rich-text-editor';
36
+ TableRow,
37
+ } from '@/components/ui/table';
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,
@@ -67,13 +67,13 @@ import {
67
67
  Timer,
68
68
  Trash2,
69
69
  } from 'lucide-react';
70
- import { useTranslations } from 'next-intl';
71
- import { useCallback, useMemo, useState } from 'react';
72
- import { fetchOperations, mutateOperations } from '../_lib/api';
73
- import { useMentionItems } from '../_lib/hooks/use-mention-items';
74
- import type {
75
- OperationsMyProjectSummary,
76
- OperationsTaskOption,
70
+ import { useTranslations } from 'next-intl';
71
+ import { useCallback, useMemo, useState } from 'react';
72
+ import { fetchOperations, mutateOperations } from '../_lib/api';
73
+ import { useMentionItems } from '../_lib/hooks/use-mention-items';
74
+ import type {
75
+ OperationsMyProjectSummary,
76
+ OperationsTaskOption,
77
77
  PaginatedResponse,
78
78
  } from '../_lib/types';
79
79
  import {
@@ -84,13 +84,13 @@ import {
84
84
  import { OperationsHeader } from './operations-header';
85
85
  import { SectionCard } from './section-card';
86
86
  import { StatusBadge } from './status-badge';
87
- import {
88
- TaskDetailSheet,
89
- TaskCommentsSection,
90
- type TaskDetailSheetData,
91
- } from './task-detail-sheet';
92
- import { TaskFileAttachments } from './task-file-attachments';
93
- import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
87
+ import {
88
+ TaskDetailSheet,
89
+ TaskCommentsSection,
90
+ type TaskDetailSheetData,
91
+ } from './task-detail-sheet';
92
+ import { TaskFileAttachments } from './task-file-attachments';
93
+ import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
94
94
 
95
95
  type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
96
96
 
@@ -106,12 +106,12 @@ type BoardTask = {
106
106
  assigneeCollaboratorId: number | null;
107
107
  assigneeName: string | null;
108
108
  assigneeUserPhotoId: number | null;
109
- assigneePersonAvatarId: number | null;
110
- commentCount: number;
111
- fileCount: number;
112
- doingStartedAt: string | null;
113
- totalDoingMinutes: number;
114
- };
109
+ assigneePersonAvatarId: number | null;
110
+ commentCount: number;
111
+ fileCount: number;
112
+ doingStartedAt: string | null;
113
+ totalDoingMinutes: number;
114
+ };
115
115
 
116
116
  type TaskFormState = {
117
117
  name: string;
@@ -171,26 +171,26 @@ function parseTaskId(value: UniqueIdentifier | null | undefined) {
171
171
  return match ? Number(match[1]) : null;
172
172
  }
173
173
 
174
- function parseColumnId(
175
- value: UniqueIdentifier | null | undefined
176
- ): BoardColumnId | null {
177
- if (!value) return null;
178
- const match = String(value).match(/^col-(.+)$/);
179
- const id = match?.[1];
180
- return KANBAN_COLUMNS.some((c) => c.id === id) ? (id as BoardColumnId) : null;
181
- }
182
-
183
- function normalizeDateInputValue(value?: string | null) {
184
- if (!value) return '';
185
- const match = String(value)
186
- .trim()
187
- .match(/^\d{4}-\d{2}-\d{2}/);
188
- return match?.[0] ?? '';
189
- }
190
-
191
- function apiTaskToBoardTask(
192
- row: OperationsMyProjectSummary['tasks'][number] | OperationsTaskOption
193
- ): BoardTask {
174
+ function parseColumnId(
175
+ value: UniqueIdentifier | null | undefined
176
+ ): BoardColumnId | null {
177
+ if (!value) return null;
178
+ const match = String(value).match(/^col-(.+)$/);
179
+ const id = match?.[1];
180
+ return KANBAN_COLUMNS.some((c) => c.id === id) ? (id as BoardColumnId) : null;
181
+ }
182
+
183
+ function normalizeDateInputValue(value?: string | null) {
184
+ if (!value) return '';
185
+ const match = String(value)
186
+ .trim()
187
+ .match(/^\d{4}-\d{2}-\d{2}/);
188
+ return match?.[0] ?? '';
189
+ }
190
+
191
+ function apiTaskToBoardTask(
192
+ row: OperationsMyProjectSummary['tasks'][number] | OperationsTaskOption
193
+ ): BoardTask {
194
194
  const status = KANBAN_COLUMNS.some((c) => c.id === row.status)
195
195
  ? (row.status as BoardColumnId)
196
196
  : 'todo';
@@ -377,12 +377,12 @@ function DroppableColumn({
377
377
 
378
378
  export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
379
379
  const t = useTranslations('operations.ProjectDetailsPage');
380
- const commonT = useTranslations('operations.Common');
381
- const formT = useTranslations('operations.ProjectFormPage');
382
- const { request, currentLocaleCode, getSettingValue } = useApp();
383
- const mentionItems = useMentionItems(request);
384
-
385
- const getProjectStatusLabel = (value?: string | null) => {
380
+ const commonT = useTranslations('operations.Common');
381
+ const formT = useTranslations('operations.ProjectFormPage');
382
+ const { request, currentLocaleCode, getSettingValue } = useApp();
383
+ const mentionItems = useMentionItems(request);
384
+
385
+ const getProjectStatusLabel = (value?: string | null) => {
386
386
  if (!value) return commonT('labels.notAvailable');
387
387
  try {
388
388
  return formT(`options.statuses.${value}`);
@@ -569,10 +569,10 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
569
569
  setActiveDragTask(task ?? null);
570
570
  };
571
571
 
572
- const openCreateTaskForm = useCallback(
573
- (defaultStatus: BoardColumnId = 'todo') => {
574
- setEditingTaskId(null);
575
- setTaskFormData({
572
+ const openCreateTaskForm = useCallback(
573
+ (defaultStatus: BoardColumnId = 'todo') => {
574
+ setEditingTaskId(null);
575
+ setTaskFormData({
576
576
  ...EMPTY_TASK_FORM,
577
577
  status: defaultStatus,
578
578
  assigneeCollaboratorId:
@@ -1310,10 +1310,10 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
1310
1310
  />
1311
1311
 
1312
1312
 
1313
- <Sheet
1314
- open={taskFormOpen}
1315
- onOpenChange={(open) => {
1316
- if (!open) {
1313
+ <Sheet
1314
+ open={taskFormOpen}
1315
+ onOpenChange={(open) => {
1316
+ if (!open) {
1317
1317
  setTaskFormOpen(false);
1318
1318
  setEditingTaskId(null);
1319
1319
  }
@@ -0,0 +1 @@
1
+ export { PersonPicker as PersonSelectWithCreate } from '../../contact/_components/person-picker';
@@ -112,22 +112,22 @@ export function ProjectCostReportScreen({ projectId }: Props) {
112
112
  isReimbursable !== 'all' ? isReimbursable === 'true' : undefined,
113
113
  };
114
114
 
115
- const { data: report, isLoading } = useQuery<ProjectCostReport>({
116
- queryKey: ['project-cost-report', projectId, filters],
117
- queryFn: () =>
118
- getProjectCostReport(request, projectId, filters) as Promise<ProjectCostReport>,
119
- });
120
-
121
- const { data: categories = [] } = useQuery<ProjectCostCategory[]>({
122
- queryKey: ['project-cost-categories-list'],
123
- queryFn: () =>
124
- listProjectCostCategories(request, {}) as Promise<ProjectCostCategory[]>,
125
- });
126
-
127
- const { data: costTypes = [] } = useQuery<ProjectCostType[]>({
128
- queryKey: ['project-cost-types-list'],
129
- queryFn: () => listProjectCostTypes(request, {}) as Promise<ProjectCostType[]>,
130
- });
115
+ const { data: report, isLoading } = useQuery<ProjectCostReport>({
116
+ queryKey: ['project-cost-report', projectId, filters],
117
+ queryFn: () =>
118
+ getProjectCostReport(request, projectId, filters) as Promise<ProjectCostReport>,
119
+ });
120
+
121
+ const { data: categories = [] } = useQuery<ProjectCostCategory[]>({
122
+ queryKey: ['project-cost-categories-list'],
123
+ queryFn: () =>
124
+ listProjectCostCategories(request, {}) as Promise<ProjectCostCategory[]>,
125
+ });
126
+
127
+ const { data: costTypes = [] } = useQuery<ProjectCostType[]>({
128
+ queryKey: ['project-cost-types-list'],
129
+ queryFn: () => listProjectCostTypes(request, {}) as Promise<ProjectCostType[]>,
130
+ });
131
131
 
132
132
  const fmt = (v: number) =>
133
133
  formatCurrency(v, getSettingValue, currentLocaleCode);
@@ -423,13 +423,13 @@ export function ProjectCostReportScreen({ projectId }: Props) {
423
423
  ))}
424
424
  </Pie>
425
425
  <ChartTooltip
426
- content={({ active, payload }) => {
427
- if (!active || !payload?.length) return null;
428
- const d = payload[0]?.payload;
429
- if (!d) return null;
430
-
431
- return (
432
- <div className="rounded-md bg-background px-3 py-2 text-sm shadow-md border">
426
+ content={({ active, payload }) => {
427
+ if (!active || !payload?.length) return null;
428
+ const d = payload[0]?.payload;
429
+ if (!d) return null;
430
+
431
+ return (
432
+ <div className="rounded-md bg-background px-3 py-2 text-sm shadow-md border">
433
433
  <p className="font-medium">
434
434
  {d.category_name ?? '—'}
435
435
  </p>
@@ -154,10 +154,10 @@ import {
154
154
  TaskDetailSheet,
155
155
  type TaskDetailSheetData,
156
156
  } from './task-detail-sheet';
157
- import { ProjectFileAttachments } from './project-file-attachments';
158
- import { TaskFileAttachments } from './task-file-attachments';
159
- import { TaskFormSheet } from './task-form-sheet';
160
- import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
157
+ import { ProjectFileAttachments } from './project-file-attachments';
158
+ import { TaskFileAttachments } from './task-file-attachments';
159
+ import { TaskFormSheet } from './task-form-sheet';
160
+ import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
161
161
 
162
162
  type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
163
163