@hed-hog/operations 0.0.331 → 0.0.332
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 +54 -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/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 +76 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +235 -5
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +27 -8
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
- 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/my-project-summary-screen.tsx.ejs +86 -87
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
- package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
- package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
- package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
- 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 +5 -5
- package/src/controllers/operations-collaborators.controller.ts +117 -8
- 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 +328 -5
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Input } from '@/components/ui/input';
|
|
5
|
+
import { Label } from '@/components/ui/label';
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue,
|
|
12
|
+
} from '@/components/ui/select';
|
|
13
|
+
import {
|
|
14
|
+
Table,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
TableHead,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableRow,
|
|
20
|
+
} from '@/components/ui/table';
|
|
21
|
+
import { Textarea } from '@/components/ui/textarea';
|
|
22
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
+
import { BanknoteIcon, Pencil, Plus, Trash2, X } from 'lucide-react';
|
|
24
|
+
import { useTranslations } from 'next-intl';
|
|
25
|
+
import { useState } from 'react';
|
|
26
|
+
import {
|
|
27
|
+
type CollaboratorPayment,
|
|
28
|
+
createCollaboratorPayment,
|
|
29
|
+
deleteCollaboratorPayment,
|
|
30
|
+
fetchCollaboratorPayments,
|
|
31
|
+
updateCollaboratorPayment,
|
|
32
|
+
} from '../_lib/api';
|
|
33
|
+
import { formatCurrency, formatDate } from '../_lib/utils/format';
|
|
34
|
+
|
|
35
|
+
const PAYMENT_METHODS = ['bank_transfer', 'pix', 'check', 'other'] as const;
|
|
36
|
+
type PaymentMethod = (typeof PAYMENT_METHODS)[number];
|
|
37
|
+
|
|
38
|
+
type FormState = {
|
|
39
|
+
amount: string;
|
|
40
|
+
paymentDate: string;
|
|
41
|
+
referenceMonth: string;
|
|
42
|
+
paymentMethod: PaymentMethod;
|
|
43
|
+
notes: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function emptyForm(): FormState {
|
|
47
|
+
return {
|
|
48
|
+
amount: '',
|
|
49
|
+
paymentDate: '',
|
|
50
|
+
referenceMonth: '',
|
|
51
|
+
paymentMethod: 'pix',
|
|
52
|
+
notes: '',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function paymentToForm(p: CollaboratorPayment): FormState {
|
|
57
|
+
return {
|
|
58
|
+
amount: p.amount ? Number(p.amount).toFixed(2).replace('.', ',') : '',
|
|
59
|
+
paymentDate: p.paymentDate ? p.paymentDate.slice(0, 10) : '',
|
|
60
|
+
referenceMonth: p.referenceMonth
|
|
61
|
+
? p.referenceMonth.split('-').reverse().join('/')
|
|
62
|
+
: '',
|
|
63
|
+
paymentMethod: (p.paymentMethod as PaymentMethod) ?? 'pix',
|
|
64
|
+
notes: p.notes ?? '',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type Props = {
|
|
69
|
+
collaboratorId: number;
|
|
70
|
+
disabled?: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function CollaboratorPaymentHistoryTab({
|
|
74
|
+
collaboratorId,
|
|
75
|
+
disabled = false,
|
|
76
|
+
}: Props) {
|
|
77
|
+
const t = useTranslations('operations.CollaboratorFormPage');
|
|
78
|
+
const commonT = useTranslations('operations.Common');
|
|
79
|
+
const { request, showToastHandler, getSettingValue, currentLocaleCode } =
|
|
80
|
+
useApp();
|
|
81
|
+
|
|
82
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
83
|
+
const [editingId, setEditingId] = useState<number | null>(null);
|
|
84
|
+
const [confirmDeleteId, setConfirmDeleteId] = useState<number | null>(null);
|
|
85
|
+
const [form, setForm] = useState<FormState>(emptyForm());
|
|
86
|
+
const [saving, setSaving] = useState(false);
|
|
87
|
+
|
|
88
|
+
const {
|
|
89
|
+
data: payments = [],
|
|
90
|
+
isLoading,
|
|
91
|
+
refetch,
|
|
92
|
+
} = useQuery<CollaboratorPayment[]>({
|
|
93
|
+
queryKey: [
|
|
94
|
+
'operations-collaborator-payment-history',
|
|
95
|
+
currentLocaleCode,
|
|
96
|
+
collaboratorId,
|
|
97
|
+
],
|
|
98
|
+
staleTime: 0,
|
|
99
|
+
refetchOnMount: 'always',
|
|
100
|
+
queryFn: () => fetchCollaboratorPayments(request, collaboratorId),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
function openAdd() {
|
|
104
|
+
setEditingId(null);
|
|
105
|
+
setForm(emptyForm());
|
|
106
|
+
setIsAdding(true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function openEdit(p: CollaboratorPayment) {
|
|
110
|
+
setIsAdding(false);
|
|
111
|
+
setForm(paymentToForm(p));
|
|
112
|
+
setEditingId(p.id);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cancelForm() {
|
|
116
|
+
setIsAdding(false);
|
|
117
|
+
setEditingId(null);
|
|
118
|
+
setForm(emptyForm());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function handleSave() {
|
|
122
|
+
const amount = parseFloat(form.amount.replace(',', '.'));
|
|
123
|
+
if (!form.paymentDate || isNaN(amount) || amount <= 0) return;
|
|
124
|
+
|
|
125
|
+
setSaving(true);
|
|
126
|
+
try {
|
|
127
|
+
const rawMonth = form.referenceMonth;
|
|
128
|
+
const referenceMonth =
|
|
129
|
+
rawMonth && rawMonth.length === 7
|
|
130
|
+
? rawMonth.split('/').reverse().join('-')
|
|
131
|
+
: rawMonth || null;
|
|
132
|
+
|
|
133
|
+
const data = {
|
|
134
|
+
amount,
|
|
135
|
+
paymentDate: form.paymentDate,
|
|
136
|
+
referenceMonth,
|
|
137
|
+
paymentMethod: form.paymentMethod,
|
|
138
|
+
notes: form.notes || null,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (editingId !== null) {
|
|
142
|
+
await updateCollaboratorPayment(
|
|
143
|
+
request,
|
|
144
|
+
collaboratorId,
|
|
145
|
+
editingId,
|
|
146
|
+
data
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
await createCollaboratorPayment(request, collaboratorId, data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
showToastHandler?.('success', t('messages.saveSuccess'));
|
|
153
|
+
cancelForm();
|
|
154
|
+
await refetch();
|
|
155
|
+
} catch {
|
|
156
|
+
showToastHandler?.('error', t('messages.saveError'));
|
|
157
|
+
} finally {
|
|
158
|
+
setSaving(false);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function handleDelete(paymentId: number) {
|
|
163
|
+
setSaving(true);
|
|
164
|
+
try {
|
|
165
|
+
await deleteCollaboratorPayment(request, collaboratorId, paymentId);
|
|
166
|
+
showToastHandler?.('success', t('messages.deleteSuccess'));
|
|
167
|
+
setConfirmDeleteId(null);
|
|
168
|
+
await refetch();
|
|
169
|
+
} catch {
|
|
170
|
+
showToastHandler?.('error', t('messages.deleteError'));
|
|
171
|
+
} finally {
|
|
172
|
+
setSaving(false);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const methodLabel = (m: string) => {
|
|
177
|
+
const key =
|
|
178
|
+
`paymentHistory.method${m.charAt(0).toUpperCase() + m.slice(1).replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())}` as Parameters<
|
|
179
|
+
typeof t
|
|
180
|
+
>[0];
|
|
181
|
+
try {
|
|
182
|
+
return t(key);
|
|
183
|
+
} catch {
|
|
184
|
+
return m;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const showForm = isAdding || editingId !== null;
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<div className="space-y-4">
|
|
192
|
+
{!disabled && (
|
|
193
|
+
<div className="flex justify-end">
|
|
194
|
+
<Button
|
|
195
|
+
size="sm"
|
|
196
|
+
variant="outline"
|
|
197
|
+
onClick={openAdd}
|
|
198
|
+
disabled={showForm}
|
|
199
|
+
>
|
|
200
|
+
<Plus className="mr-1 size-4" />
|
|
201
|
+
{t('paymentHistory.add')}
|
|
202
|
+
</Button>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
{showForm && (
|
|
207
|
+
<div className="rounded-md border bg-muted/30 p-4">
|
|
208
|
+
<p className="mb-3 text-sm font-medium">
|
|
209
|
+
{editingId !== null
|
|
210
|
+
? t('paymentHistory.editTitle')
|
|
211
|
+
: t('paymentHistory.add')}
|
|
212
|
+
</p>
|
|
213
|
+
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
214
|
+
<div className="space-y-1">
|
|
215
|
+
<Label className="text-xs">{t('paymentHistory.amount')} *</Label>
|
|
216
|
+
<Input
|
|
217
|
+
type="text"
|
|
218
|
+
inputMode="numeric"
|
|
219
|
+
placeholder="0,00"
|
|
220
|
+
value={form.amount}
|
|
221
|
+
onChange={(e) => {
|
|
222
|
+
const digits = e.target.value.replace(/\D/g, '');
|
|
223
|
+
if (!digits) {
|
|
224
|
+
setForm((f) => ({ ...f, amount: '' }));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const cents = parseInt(digits, 10);
|
|
228
|
+
const formatted = (cents / 100).toFixed(2).replace('.', ',');
|
|
229
|
+
setForm((f) => ({ ...f, amount: formatted }));
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
<div className="space-y-1">
|
|
234
|
+
<Label className="text-xs">
|
|
235
|
+
{t('paymentHistory.paymentDate')} *
|
|
236
|
+
</Label>
|
|
237
|
+
<Input
|
|
238
|
+
type="date"
|
|
239
|
+
value={form.paymentDate}
|
|
240
|
+
onChange={(e) =>
|
|
241
|
+
setForm((f) => ({ ...f, paymentDate: e.target.value }))
|
|
242
|
+
}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="space-y-1">
|
|
246
|
+
<Label className="text-xs">
|
|
247
|
+
{t('paymentHistory.referenceMonth')}
|
|
248
|
+
</Label>
|
|
249
|
+
<Input
|
|
250
|
+
type="text"
|
|
251
|
+
inputMode="numeric"
|
|
252
|
+
placeholder="MM/AAAA"
|
|
253
|
+
maxLength={7}
|
|
254
|
+
value={form.referenceMonth}
|
|
255
|
+
onChange={(e) => {
|
|
256
|
+
const digits = e.target.value.replace(/\D/g, '');
|
|
257
|
+
const masked =
|
|
258
|
+
digits.length > 2
|
|
259
|
+
? digits.slice(0, 2) + '/' + digits.slice(2, 6)
|
|
260
|
+
: digits;
|
|
261
|
+
setForm((f) => ({ ...f, referenceMonth: masked }));
|
|
262
|
+
}}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
<div className="space-y-1">
|
|
266
|
+
<Label className="text-xs">
|
|
267
|
+
{t('paymentHistory.paymentMethod')}
|
|
268
|
+
</Label>
|
|
269
|
+
<Select
|
|
270
|
+
value={form.paymentMethod}
|
|
271
|
+
onValueChange={(v) =>
|
|
272
|
+
setForm((f) => ({
|
|
273
|
+
...f,
|
|
274
|
+
paymentMethod: v as PaymentMethod,
|
|
275
|
+
}))
|
|
276
|
+
}
|
|
277
|
+
>
|
|
278
|
+
<SelectTrigger className="w-full cursor-pointer">
|
|
279
|
+
<SelectValue />
|
|
280
|
+
</SelectTrigger>
|
|
281
|
+
<SelectContent>
|
|
282
|
+
{PAYMENT_METHODS.map((m) => (
|
|
283
|
+
<SelectItem key={m} value={m} className="cursor-pointer">
|
|
284
|
+
{methodLabel(m)}
|
|
285
|
+
</SelectItem>
|
|
286
|
+
))}
|
|
287
|
+
</SelectContent>
|
|
288
|
+
</Select>
|
|
289
|
+
</div>
|
|
290
|
+
<div className="space-y-1 sm:col-span-2">
|
|
291
|
+
<Label className="text-xs">{t('paymentHistory.notes')}</Label>
|
|
292
|
+
<Textarea
|
|
293
|
+
rows={2}
|
|
294
|
+
value={form.notes}
|
|
295
|
+
onChange={(e) =>
|
|
296
|
+
setForm((f) => ({ ...f, notes: e.target.value }))
|
|
297
|
+
}
|
|
298
|
+
/>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
302
|
+
<Button
|
|
303
|
+
size="sm"
|
|
304
|
+
variant="ghost"
|
|
305
|
+
onClick={cancelForm}
|
|
306
|
+
disabled={saving}
|
|
307
|
+
>
|
|
308
|
+
<X className="mr-1 size-4" />
|
|
309
|
+
{commonT('actions.cancel')}
|
|
310
|
+
</Button>
|
|
311
|
+
<Button size="sm" onClick={handleSave} disabled={saving}>
|
|
312
|
+
{commonT('actions.save')}
|
|
313
|
+
</Button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{isLoading ? (
|
|
319
|
+
<p className="text-sm text-muted-foreground">{t('loading')}</p>
|
|
320
|
+
) : payments.length === 0 && !showForm ? (
|
|
321
|
+
<div className="flex flex-col items-center gap-3 py-10 text-center">
|
|
322
|
+
<BanknoteIcon className="size-10 text-muted-foreground/40" />
|
|
323
|
+
<p className="text-sm text-muted-foreground">
|
|
324
|
+
{t('tabs.paymentHistoryEmpty')}
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
) : payments.length > 0 ? (
|
|
328
|
+
<div className="overflow-x-auto rounded-md border">
|
|
329
|
+
<Table>
|
|
330
|
+
<TableHeader>
|
|
331
|
+
<TableRow>
|
|
332
|
+
<TableHead>{t('paymentHistory.amount')}</TableHead>
|
|
333
|
+
<TableHead>{t('paymentHistory.paymentDate')}</TableHead>
|
|
334
|
+
<TableHead>{t('paymentHistory.referenceMonth')}</TableHead>
|
|
335
|
+
<TableHead>{t('paymentHistory.paymentMethod')}</TableHead>
|
|
336
|
+
<TableHead>{t('paymentHistory.notes')}</TableHead>
|
|
337
|
+
{!disabled && (
|
|
338
|
+
<TableHead className="w-20 text-right"></TableHead>
|
|
339
|
+
)}
|
|
340
|
+
</TableRow>
|
|
341
|
+
</TableHeader>
|
|
342
|
+
<TableBody>
|
|
343
|
+
{payments.map((entry) => (
|
|
344
|
+
<TableRow key={entry.id}>
|
|
345
|
+
<TableCell className="font-medium">
|
|
346
|
+
{formatCurrency(
|
|
347
|
+
Number(entry.amount),
|
|
348
|
+
getSettingValue,
|
|
349
|
+
currentLocaleCode
|
|
350
|
+
)}
|
|
351
|
+
</TableCell>
|
|
352
|
+
<TableCell>
|
|
353
|
+
{entry.paymentDate
|
|
354
|
+
? formatDate(
|
|
355
|
+
entry.paymentDate,
|
|
356
|
+
getSettingValue,
|
|
357
|
+
currentLocaleCode
|
|
358
|
+
)
|
|
359
|
+
: '—'}
|
|
360
|
+
</TableCell>
|
|
361
|
+
<TableCell>
|
|
362
|
+
{entry.referenceMonth
|
|
363
|
+
? entry.referenceMonth.split('-').reverse().join('/')
|
|
364
|
+
: '—'}
|
|
365
|
+
</TableCell>
|
|
366
|
+
<TableCell>
|
|
367
|
+
{entry.paymentMethod
|
|
368
|
+
? methodLabel(entry.paymentMethod)
|
|
369
|
+
: '—'}
|
|
370
|
+
</TableCell>
|
|
371
|
+
<TableCell className="text-muted-foreground">
|
|
372
|
+
{entry.notes ?? '—'}
|
|
373
|
+
</TableCell>
|
|
374
|
+
{!disabled && (
|
|
375
|
+
<TableCell className="text-right">
|
|
376
|
+
{confirmDeleteId === entry.id ? (
|
|
377
|
+
<div className="flex items-center justify-end gap-1">
|
|
378
|
+
<span className="text-xs text-muted-foreground">
|
|
379
|
+
{t('paymentHistory.deleteConfirm')}
|
|
380
|
+
</span>
|
|
381
|
+
<Button
|
|
382
|
+
size="sm"
|
|
383
|
+
variant="destructive"
|
|
384
|
+
className="h-7 px-2 text-xs cursor-pointer"
|
|
385
|
+
disabled={saving}
|
|
386
|
+
onClick={() => handleDelete(entry.id)}
|
|
387
|
+
>
|
|
388
|
+
{commonT('actions.delete')}
|
|
389
|
+
</Button>
|
|
390
|
+
<Button
|
|
391
|
+
size="sm"
|
|
392
|
+
variant="ghost"
|
|
393
|
+
className="h-7 px-2 text-xs cursor-pointer"
|
|
394
|
+
onClick={() => setConfirmDeleteId(null)}
|
|
395
|
+
>
|
|
396
|
+
<X className="size-3" />
|
|
397
|
+
</Button>
|
|
398
|
+
</div>
|
|
399
|
+
) : (
|
|
400
|
+
<div className="flex items-center justify-end gap-1">
|
|
401
|
+
<Button
|
|
402
|
+
size="icon"
|
|
403
|
+
variant="ghost"
|
|
404
|
+
className="size-7 cursor-pointer"
|
|
405
|
+
onClick={() => openEdit(entry)}
|
|
406
|
+
>
|
|
407
|
+
<Pencil className="size-3.5" />
|
|
408
|
+
</Button>
|
|
409
|
+
<Button
|
|
410
|
+
size="icon"
|
|
411
|
+
variant="ghost"
|
|
412
|
+
className="size-7 cursor-pointer text-destructive hover:text-destructive"
|
|
413
|
+
onClick={() => setConfirmDeleteId(entry.id)}
|
|
414
|
+
>
|
|
415
|
+
<Trash2 className="size-3.5" />
|
|
416
|
+
</Button>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</TableCell>
|
|
420
|
+
)}
|
|
421
|
+
</TableRow>
|
|
422
|
+
))}
|
|
423
|
+
</TableBody>
|
|
424
|
+
</Table>
|
|
425
|
+
</div>
|
|
426
|
+
) : null}
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { EmptyState, Page } from '@/components/entity-list';
|
|
4
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
4
5
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
|
-
import {
|
|
7
|
-
Dialog,
|
|
8
|
-
DialogContent,
|
|
9
|
-
DialogDescription,
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
10
11
|
DialogFooter,
|
|
11
12
|
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,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/components/ui/dialog';
|
|
15
|
+
import { Input } from '@/components/ui/input';
|
|
16
|
+
import { Label } from '@/components/ui/label';
|
|
17
|
+
import { Progress } from '@/components/ui/progress';
|
|
18
|
+
import {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from '@/components/ui/select';
|
|
25
|
+
import {
|
|
26
|
+
Sheet,
|
|
27
|
+
SheetContent,
|
|
28
|
+
SheetHeader,
|
|
29
|
+
SheetTitle,
|
|
30
|
+
} from '@/components/ui/sheet';
|
|
31
|
+
import {
|
|
32
|
+
Table,
|
|
33
|
+
TableBody,
|
|
34
|
+
TableCell,
|
|
34
35
|
TableHead,
|
|
35
36
|
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';
|
|
37
|
+
TableRow,
|
|
38
|
+
} from '@/components/ui/table';
|
|
39
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
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
|
-
|
|
89
|
-
|
|
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
|
+
TaskCommentsSection,
|
|
89
|
+
TaskDetailSheet,
|
|
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:
|
|
@@ -834,7 +834,7 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
834
834
|
{(isOver) => (
|
|
835
835
|
<div
|
|
836
836
|
className={[
|
|
837
|
-
'flex min-h-
|
|
837
|
+
'flex min-h-48 max-h-160 flex-col 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 flex-1 flex-col gap-2">
|
|
873
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
|
|
874
874
|
<AnimatePresence initial={false}>
|
|
875
875
|
{taskColumns[column.id].map((task) => {
|
|
876
876
|
const tags = getTaskTags(task);
|
|
@@ -1309,11 +1309,10 @@ export function MyProjectSummaryScreen({ projectId }: { projectId: number }) {
|
|
|
1309
1309
|
}}
|
|
1310
1310
|
/>
|
|
1311
1311
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (!open) {
|
|
1312
|
+
<Sheet
|
|
1313
|
+
open={taskFormOpen}
|
|
1314
|
+
onOpenChange={(open) => {
|
|
1315
|
+
if (!open) {
|
|
1317
1316
|
setTaskFormOpen(false);
|
|
1318
1317
|
setEditingTaskId(null);
|
|
1319
1318
|
}
|