@hed-hog/finance 0.0.237 → 0.0.238
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/dto/create-finance-tag.dto.d.ts +5 -0
- package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
- package/dist/dto/create-finance-tag.dto.js +29 -0
- package/dist/dto/create-finance-tag.dto.js.map +1 -0
- package/dist/dto/update-installment-tags.dto.d.ts +4 -0
- package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
- package/dist/dto/update-installment-tags.dto.js +27 -0
- package/dist/dto/update-installment-tags.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +4 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +81 -0
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +36 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +86 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +185 -2
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +249 -78
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +41 -3
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +202 -20
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +40 -1
- package/hedhog/frontend/messages/en.json +38 -0
- package/hedhog/frontend/messages/pt.json +38 -0
- package/package.json +5 -5
- package/src/dto/create-finance-tag.dto.ts +15 -0
- package/src/dto/update-installment-tags.dto.ts +12 -0
- package/src/finance-installments.controller.ts +43 -9
- package/src/finance.service.ts +255 -12
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
4
|
import { AuditTimeline } from '@/components/ui/audit-timeline';
|
|
5
|
-
import { Badge } from '@/components/ui/badge';
|
|
6
5
|
import { Button } from '@/components/ui/button';
|
|
7
6
|
import {
|
|
8
7
|
Card,
|
|
@@ -29,8 +28,9 @@ import {
|
|
|
29
28
|
TableRow,
|
|
30
29
|
} from '@/components/ui/table';
|
|
31
30
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
31
|
+
import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
|
|
32
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
32
33
|
import {
|
|
33
|
-
ArrowLeft,
|
|
34
34
|
CheckCircle,
|
|
35
35
|
Download,
|
|
36
36
|
Edit,
|
|
@@ -42,11 +42,13 @@ import {
|
|
|
42
42
|
import { useTranslations } from 'next-intl';
|
|
43
43
|
import Link from 'next/link';
|
|
44
44
|
import { useParams } from 'next/navigation';
|
|
45
|
+
import { useEffect, useState } from 'react';
|
|
45
46
|
import { formatarData } from '../../../_lib/formatters';
|
|
46
47
|
import { useFinanceData } from '../../../_lib/use-finance-data';
|
|
47
48
|
|
|
48
49
|
export default function TituloDetalhePage() {
|
|
49
50
|
const t = useTranslations('finance.PayableInstallmentDetailPage');
|
|
51
|
+
const { request, showToastHandler } = useApp();
|
|
50
52
|
const params = useParams<{ id: string }>();
|
|
51
53
|
const id = params?.id;
|
|
52
54
|
const { data } = useFinanceData();
|
|
@@ -62,6 +64,22 @@ export default function TituloDetalhePage() {
|
|
|
62
64
|
|
|
63
65
|
const titulo = titulosPagar.find((t) => t.id === id);
|
|
64
66
|
|
|
67
|
+
const [availableTags, setAvailableTags] = useState<any[]>([]);
|
|
68
|
+
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
|
69
|
+
const [isCreatingTag, setIsCreatingTag] = useState(false);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setAvailableTags(tags || []);
|
|
73
|
+
}, [tags]);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!titulo) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setSelectedTagIds(Array.isArray(titulo.tags) ? titulo.tags : []);
|
|
81
|
+
}, [titulo]);
|
|
82
|
+
|
|
65
83
|
if (!titulo) {
|
|
66
84
|
return (
|
|
67
85
|
<div className="space-y-6">
|
|
@@ -95,9 +113,20 @@ export default function TituloDetalhePage() {
|
|
|
95
113
|
const fornecedor = getPessoaById(titulo.fornecedorId);
|
|
96
114
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
97
115
|
const centroCusto = getCentroCustoById(titulo.centroCustoId);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
116
|
+
|
|
117
|
+
const tagOptions = availableTags.map((tag) => ({
|
|
118
|
+
id: String(tag.id),
|
|
119
|
+
name: String(tag.nome || ''),
|
|
120
|
+
color: tag.cor,
|
|
121
|
+
usageCount:
|
|
122
|
+
typeof tag.usageCount === 'number'
|
|
123
|
+
? tag.usageCount
|
|
124
|
+
: typeof tag.usoCount === 'number'
|
|
125
|
+
? tag.usoCount
|
|
126
|
+
: typeof tag.count === 'number'
|
|
127
|
+
? tag.count
|
|
128
|
+
: undefined,
|
|
129
|
+
}));
|
|
101
130
|
|
|
102
131
|
const auditEvents = logsAuditoria
|
|
103
132
|
.filter(
|
|
@@ -113,64 +142,181 @@ export default function TituloDetalhePage() {
|
|
|
113
142
|
depois: log.depois,
|
|
114
143
|
}));
|
|
115
144
|
|
|
145
|
+
const attachmentDetails = Array.isArray(titulo.anexosDetalhes)
|
|
146
|
+
? titulo.anexosDetalhes
|
|
147
|
+
: (titulo.anexos || []).map((nome: string) => ({ nome }));
|
|
148
|
+
|
|
149
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
150
|
+
if (!fileId) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const response = await request<{ url?: string }>({
|
|
155
|
+
url: `/file/open/${fileId}`,
|
|
156
|
+
method: 'PUT',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const url = response?.data?.url;
|
|
160
|
+
if (!url) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const tTagSelector = (key: string, fallback: string) => {
|
|
168
|
+
const fullKey = `tagSelector.${key}`;
|
|
169
|
+
return t.has(fullKey) ? t(fullKey) : fallback;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const toTagSlug = (value: string) => {
|
|
173
|
+
return value
|
|
174
|
+
.normalize('NFD')
|
|
175
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
176
|
+
.toLowerCase()
|
|
177
|
+
.trim()
|
|
178
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
179
|
+
.replace(/(^-|-$)+/g, '');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const handleCreateTag = async (name: string) => {
|
|
183
|
+
const slug = toTagSlug(name);
|
|
184
|
+
|
|
185
|
+
if (!slug) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setIsCreatingTag(true);
|
|
190
|
+
try {
|
|
191
|
+
const response = await request<{
|
|
192
|
+
id?: string | number;
|
|
193
|
+
nome?: string;
|
|
194
|
+
cor?: string;
|
|
195
|
+
}>({
|
|
196
|
+
url: '/finance/tags',
|
|
197
|
+
method: 'POST',
|
|
198
|
+
data: {
|
|
199
|
+
name: slug,
|
|
200
|
+
color: '#000000',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const created = response?.data;
|
|
205
|
+
const newTag = {
|
|
206
|
+
id: String(created?.id || `temp-${Date.now()}`),
|
|
207
|
+
nome: created?.nome || slug,
|
|
208
|
+
cor: created?.cor || '#000000',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
setAvailableTags((current) => {
|
|
212
|
+
if (current.some((tag) => String(tag.id) === newTag.id)) {
|
|
213
|
+
return current;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return [...current, newTag];
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
showToastHandler?.(
|
|
220
|
+
'success',
|
|
221
|
+
tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
id: newTag.id,
|
|
226
|
+
name: newTag.nome,
|
|
227
|
+
color: newTag.cor,
|
|
228
|
+
};
|
|
229
|
+
} catch {
|
|
230
|
+
showToastHandler?.(
|
|
231
|
+
'error',
|
|
232
|
+
tTagSelector('messages.createError', 'Não foi possível criar a tag')
|
|
233
|
+
);
|
|
234
|
+
return null;
|
|
235
|
+
} finally {
|
|
236
|
+
setIsCreatingTag(false);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const handleChangeTags = async (nextTagIds: string[]) => {
|
|
241
|
+
if (!titulo?.id) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const response = await request<{ tags?: string[] }>({
|
|
247
|
+
url: `/finance/accounts-payable/installments/${titulo.id}/tags`,
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
data: {
|
|
250
|
+
tag_ids: nextTagIds.map((tagId) => Number(tagId)),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (Array.isArray(response?.data?.tags)) {
|
|
255
|
+
setSelectedTagIds(response.data.tags);
|
|
256
|
+
} else {
|
|
257
|
+
setSelectedTagIds(nextTagIds);
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
showToastHandler?.(
|
|
261
|
+
'error',
|
|
262
|
+
tTagSelector(
|
|
263
|
+
'messages.updateError',
|
|
264
|
+
'Não foi possível atualizar as tags'
|
|
265
|
+
)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
116
270
|
return (
|
|
117
271
|
<Page>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{t('actions.cancel')}
|
|
167
|
-
</DropdownMenuItem>
|
|
168
|
-
</DropdownMenuContent>
|
|
169
|
-
</DropdownMenu>
|
|
170
|
-
</div>
|
|
171
|
-
}
|
|
172
|
-
/>
|
|
173
|
-
</div>
|
|
272
|
+
<PageHeader
|
|
273
|
+
title={titulo.documento}
|
|
274
|
+
description={titulo.descricao}
|
|
275
|
+
breadcrumbs={[
|
|
276
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
277
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
278
|
+
{
|
|
279
|
+
label: t('breadcrumbs.payables'),
|
|
280
|
+
href: '/finance/accounts-payable/installments',
|
|
281
|
+
},
|
|
282
|
+
{ label: titulo.documento },
|
|
283
|
+
]}
|
|
284
|
+
actions={
|
|
285
|
+
<div className="flex items-center gap-2">
|
|
286
|
+
<DropdownMenu>
|
|
287
|
+
<DropdownMenuTrigger asChild>
|
|
288
|
+
<Button variant="outline">
|
|
289
|
+
<MoreHorizontal className="mr-2 h-4 w-4" />
|
|
290
|
+
{t('actions.title')}
|
|
291
|
+
</Button>
|
|
292
|
+
</DropdownMenuTrigger>
|
|
293
|
+
<DropdownMenuContent align="end">
|
|
294
|
+
<DropdownMenuItem>
|
|
295
|
+
<Edit className="mr-2 h-4 w-4" />
|
|
296
|
+
{t('actions.edit')}
|
|
297
|
+
</DropdownMenuItem>
|
|
298
|
+
<DropdownMenuItem>
|
|
299
|
+
<CheckCircle className="mr-2 h-4 w-4" />
|
|
300
|
+
{t('actions.approve')}
|
|
301
|
+
</DropdownMenuItem>
|
|
302
|
+
<DropdownMenuItem>
|
|
303
|
+
<Download className="mr-2 h-4 w-4" />
|
|
304
|
+
{t('actions.settle')}
|
|
305
|
+
</DropdownMenuItem>
|
|
306
|
+
<DropdownMenuItem>
|
|
307
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
308
|
+
{t('actions.reverse')}
|
|
309
|
+
</DropdownMenuItem>
|
|
310
|
+
<DropdownMenuSeparator />
|
|
311
|
+
<DropdownMenuItem className="text-destructive">
|
|
312
|
+
<XCircle className="mr-2 h-4 w-4" />
|
|
313
|
+
{t('actions.cancel')}
|
|
314
|
+
</DropdownMenuItem>
|
|
315
|
+
</DropdownMenuContent>
|
|
316
|
+
</DropdownMenu>
|
|
317
|
+
</div>
|
|
318
|
+
}
|
|
319
|
+
/>
|
|
174
320
|
|
|
175
321
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
176
322
|
<Card className="lg:col-span-2">
|
|
@@ -234,20 +380,44 @@ export default function TituloDetalhePage() {
|
|
|
234
380
|
<dt className="text-sm font-medium text-muted-foreground">
|
|
235
381
|
{t('documentData.tags')}
|
|
236
382
|
</dt>
|
|
237
|
-
<dd className="mt-1
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
383
|
+
<dd className="mt-1">
|
|
384
|
+
<TagSelectorSheet
|
|
385
|
+
selectedTagIds={selectedTagIds}
|
|
386
|
+
tags={tagOptions}
|
|
387
|
+
onChange={handleChangeTags}
|
|
388
|
+
onCreateTag={handleCreateTag}
|
|
389
|
+
disabled={isCreatingTag}
|
|
390
|
+
emptyText={tTagSelector('noTags', 'Sem tags')}
|
|
391
|
+
labels={{
|
|
392
|
+
addTag: tTagSelector('addTag', 'Adicionar tag'),
|
|
393
|
+
sheetTitle: tTagSelector('sheetTitle', 'Gerenciar tags'),
|
|
394
|
+
sheetDescription: tTagSelector(
|
|
395
|
+
'sheetDescription',
|
|
396
|
+
'Selecione tags existentes ou crie uma nova.'
|
|
397
|
+
),
|
|
398
|
+
createLabel: tTagSelector('createLabel', 'Nova tag'),
|
|
399
|
+
createPlaceholder: tTagSelector(
|
|
400
|
+
'createPlaceholder',
|
|
401
|
+
'Digite o nome da tag'
|
|
402
|
+
),
|
|
403
|
+
createAction: tTagSelector('createAction', 'Criar tag'),
|
|
404
|
+
popularTitle: tTagSelector(
|
|
405
|
+
'popularTitle',
|
|
406
|
+
'Tags mais usadas'
|
|
407
|
+
),
|
|
408
|
+
selectedTitle: tTagSelector(
|
|
409
|
+
'selectedTitle',
|
|
410
|
+
'Tags selecionadas'
|
|
411
|
+
),
|
|
412
|
+
noTags: tTagSelector('noTags', 'Sem tags'),
|
|
413
|
+
cancel: tTagSelector('cancel', 'Cancelar'),
|
|
414
|
+
apply: tTagSelector('apply', 'Aplicar'),
|
|
415
|
+
removeTagAria: (tagName: string) =>
|
|
416
|
+
t.has('tagSelector.removeTagAria')
|
|
417
|
+
? t('tagSelector.removeTagAria', { tag: tagName })
|
|
418
|
+
: `Remover tag ${tagName}`,
|
|
419
|
+
}}
|
|
420
|
+
/>
|
|
251
421
|
</dd>
|
|
252
422
|
</div>
|
|
253
423
|
</dl>
|
|
@@ -260,16 +430,17 @@ export default function TituloDetalhePage() {
|
|
|
260
430
|
<CardDescription>{t('attachments.description')}</CardDescription>
|
|
261
431
|
</CardHeader>
|
|
262
432
|
<CardContent>
|
|
263
|
-
{
|
|
433
|
+
{attachmentDetails.length > 0 ? (
|
|
264
434
|
<ul className="space-y-2">
|
|
265
|
-
{
|
|
435
|
+
{attachmentDetails.map((anexo: any, i: number) => (
|
|
266
436
|
<li key={i}>
|
|
267
437
|
<Button
|
|
268
438
|
variant="ghost"
|
|
269
439
|
className="h-auto w-full justify-start p-2"
|
|
440
|
+
onClick={() => void handleOpenAttachment(anexo?.id)}
|
|
270
441
|
>
|
|
271
442
|
<FileText className="mr-2 h-4 w-4" />
|
|
272
|
-
{anexo}
|
|
443
|
+
{anexo?.nome}
|
|
273
444
|
</Button>
|
|
274
445
|
</li>
|
|
275
446
|
))}
|
|
@@ -645,12 +645,12 @@ function NovoTituloSheet({
|
|
|
645
645
|
|
|
646
646
|
export default function TitulosPagarPage() {
|
|
647
647
|
const t = useTranslations('finance.PayableInstallmentsPage');
|
|
648
|
-
const { request } = useApp();
|
|
648
|
+
const { request, currentLocaleCode, showToastHandler } = useApp();
|
|
649
649
|
const { data, refetch } = useFinanceData();
|
|
650
650
|
const { titulosPagar, pessoas } = data;
|
|
651
651
|
|
|
652
652
|
const { data: categoriasData } = useQuery<any[]>({
|
|
653
|
-
queryKey: ['finance-categories-options'],
|
|
653
|
+
queryKey: ['finance-categories-options', currentLocaleCode],
|
|
654
654
|
queryFn: async () => {
|
|
655
655
|
const response = await request({
|
|
656
656
|
url: '/finance/categories',
|
|
@@ -696,6 +696,29 @@ export default function TitulosPagarPage() {
|
|
|
696
696
|
return matchesSearch && matchesStatus;
|
|
697
697
|
});
|
|
698
698
|
|
|
699
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
700
|
+
if (!fileId) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const response = await request<{ url?: string }>({
|
|
706
|
+
url: `/file/open/${fileId}`,
|
|
707
|
+
method: 'PUT',
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
const url = response?.data?.url;
|
|
711
|
+
if (!url) {
|
|
712
|
+
showToastHandler?.('error', 'Não foi possível abrir o anexo');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
717
|
+
} catch {
|
|
718
|
+
showToastHandler?.('error', 'Não foi possível abrir o anexo');
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
699
722
|
return (
|
|
700
723
|
<Page>
|
|
701
724
|
<PageHeader
|
|
@@ -776,7 +799,22 @@ export default function TitulosPagarPage() {
|
|
|
776
799
|
{titulo.documento}
|
|
777
800
|
</Link>
|
|
778
801
|
{titulo.anexos.length > 0 && (
|
|
779
|
-
<
|
|
802
|
+
<Button
|
|
803
|
+
type="button"
|
|
804
|
+
variant="ghost"
|
|
805
|
+
size="icon"
|
|
806
|
+
className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
|
|
807
|
+
onClick={(event) => {
|
|
808
|
+
event.preventDefault();
|
|
809
|
+
event.stopPropagation();
|
|
810
|
+
const firstAttachmentId =
|
|
811
|
+
titulo.anexosDetalhes?.[0]?.id;
|
|
812
|
+
void handleOpenAttachment(firstAttachmentId);
|
|
813
|
+
}}
|
|
814
|
+
aria-label="Abrir anexo"
|
|
815
|
+
>
|
|
816
|
+
<Paperclip className="h-3 w-3" />
|
|
817
|
+
</Button>
|
|
780
818
|
)}
|
|
781
819
|
</TableCell>
|
|
782
820
|
<TableCell>{fornecedor?.nome}</TableCell>
|