@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.
- package/dist/controllers/operations-contracts.controller.d.ts +12 -12
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +8 -1
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +6 -0
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
- package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
- package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
- package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +84 -84
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
- package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +4 -4
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +803 -803
- package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
- package/hedhog/frontend/app/_lib/api.ts.ejs +480 -480
- package/hedhog/frontend/app/_lib/types.ts.ejs +5 -5
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +74 -74
- package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
- package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
- package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/index.ts.ejs +25 -25
- package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
- package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
- package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
- package/hedhog/table/operations_collaborator.yaml +8 -8
- package/hedhog/table/operations_task.yaml +76 -76
- package/hedhog/table/operations_task_activity.yaml +51 -51
- package/package.json +5 -5
- package/src/controllers/operations-tasks.controller.ts +156 -156
- package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
- package/src/dto/create-collaborator.dto.ts +4 -4
- package/src/operations.service.spec.ts +1006 -988
- 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
|
|