@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.
Files changed (31) hide show
  1. package/dist/dto/create-finance-tag.dto.d.ts +5 -0
  2. package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
  3. package/dist/dto/create-finance-tag.dto.js +29 -0
  4. package/dist/dto/create-finance-tag.dto.js.map +1 -0
  5. package/dist/dto/update-installment-tags.dto.d.ts +4 -0
  6. package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
  7. package/dist/dto/update-installment-tags.dto.js +27 -0
  8. package/dist/dto/update-installment-tags.dto.js.map +1 -0
  9. package/dist/finance-data.controller.d.ts +4 -0
  10. package/dist/finance-data.controller.d.ts.map +1 -1
  11. package/dist/finance-installments.controller.d.ts +81 -0
  12. package/dist/finance-installments.controller.d.ts.map +1 -1
  13. package/dist/finance-installments.controller.js +36 -0
  14. package/dist/finance-installments.controller.js.map +1 -1
  15. package/dist/finance.service.d.ts +86 -0
  16. package/dist/finance.service.d.ts.map +1 -1
  17. package/dist/finance.service.js +185 -2
  18. package/dist/finance.service.js.map +1 -1
  19. package/hedhog/data/route.yaml +27 -0
  20. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
  21. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +249 -78
  22. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +41 -3
  23. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +202 -20
  24. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +40 -1
  25. package/hedhog/frontend/messages/en.json +38 -0
  26. package/hedhog/frontend/messages/pt.json +38 -0
  27. package/package.json +5 -5
  28. package/src/dto/create-finance-tag.dto.ts +15 -0
  29. package/src/dto/update-installment-tags.dto.ts +12 -0
  30. package/src/finance-installments.controller.ts +43 -9
  31. 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
- const tituloTags = titulo.tags
99
- .map((tagId: any) => tags.find((t: any) => t.id === tagId))
100
- .filter(Boolean);
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
- <div className="flex items-center gap-4">
119
- <Button variant="ghost" size="icon" asChild>
120
- <Link href="/finance/accounts-payable/installments">
121
- <ArrowLeft className="h-4 w-4" />
122
- </Link>
123
- </Button>
124
- <PageHeader
125
- title={titulo.documento}
126
- description={titulo.descricao}
127
- breadcrumbs={[
128
- { label: t('breadcrumbs.home'), href: '/' },
129
- { label: t('breadcrumbs.finance'), href: '/finance' },
130
- {
131
- label: t('breadcrumbs.payables'),
132
- href: '/finance/accounts-payable/installments',
133
- },
134
- { label: titulo.documento },
135
- ]}
136
- actions={
137
- <div className="flex items-center gap-2">
138
- <StatusBadge status={titulo.status} />
139
- <DropdownMenu>
140
- <DropdownMenuTrigger asChild>
141
- <Button variant="outline">
142
- <MoreHorizontal className="mr-2 h-4 w-4" />
143
- {t('actions.title')}
144
- </Button>
145
- </DropdownMenuTrigger>
146
- <DropdownMenuContent align="end">
147
- <DropdownMenuItem>
148
- <Edit className="mr-2 h-4 w-4" />
149
- {t('actions.edit')}
150
- </DropdownMenuItem>
151
- <DropdownMenuItem>
152
- <CheckCircle className="mr-2 h-4 w-4" />
153
- {t('actions.approve')}
154
- </DropdownMenuItem>
155
- <DropdownMenuItem>
156
- <Download className="mr-2 h-4 w-4" />
157
- {t('actions.settle')}
158
- </DropdownMenuItem>
159
- <DropdownMenuItem>
160
- <Undo className="mr-2 h-4 w-4" />
161
- {t('actions.reverse')}
162
- </DropdownMenuItem>
163
- <DropdownMenuSeparator />
164
- <DropdownMenuItem className="text-destructive">
165
- <XCircle className="mr-2 h-4 w-4" />
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 flex gap-1">
238
- {tituloTags.length > 0 ? (
239
- tituloTags.map((tag: any) => (
240
- <Badge
241
- key={tag?.id}
242
- variant="outline"
243
- style={{ borderColor: tag?.cor, color: tag?.cor }}
244
- >
245
- {tag?.nome}
246
- </Badge>
247
- ))
248
- ) : (
249
- <span className="text-muted-foreground">-</span>
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
- {titulo.anexos.length > 0 ? (
433
+ {attachmentDetails.length > 0 ? (
264
434
  <ul className="space-y-2">
265
- {titulo.anexos.map((anexo: any, i: number) => (
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
- <Paperclip className="ml-1 inline h-3 w-3 text-muted-foreground" />
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>