@hed-hog/finance 0.0.236 → 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 (34) 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/_components/person-field-with-create.tsx.ejs +627 -0
  21. package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
  22. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +249 -78
  23. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +903 -883
  24. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +202 -20
  25. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +877 -861
  26. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +108 -25
  27. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +15 -8
  28. package/hedhog/frontend/messages/en.json +38 -0
  29. package/hedhog/frontend/messages/pt.json +38 -0
  30. package/package.json +5 -5
  31. package/src/dto/create-finance-tag.dto.ts +15 -0
  32. package/src/dto/update-installment-tags.dto.ts +12 -0
  33. package/src/finance-installments.controller.ts +43 -9
  34. package/src/finance.service.ts +255 -12
@@ -28,6 +28,8 @@ import {
28
28
  TableRow,
29
29
  } from '@/components/ui/table';
30
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';
31
33
  import {
32
34
  ArrowLeft,
33
35
  Download,
@@ -39,11 +41,13 @@ import {
39
41
  import { useTranslations } from 'next-intl';
40
42
  import Link from 'next/link';
41
43
  import { useParams } from 'next/navigation';
44
+ import { useEffect, useState } from 'react';
42
45
  import { formatarData } from '../../../_lib/formatters';
43
46
  import { useFinanceData } from '../../../_lib/use-finance-data';
44
47
 
45
48
  export default function TituloReceberDetalhePage() {
46
49
  const t = useTranslations('finance.ReceivableInstallmentDetailPage');
50
+ const { request, showToastHandler } = useApp();
47
51
  const params = useParams<{ id: string }>();
48
52
  const id = params?.id;
49
53
  const { data } = useFinanceData();
@@ -59,6 +63,22 @@ export default function TituloReceberDetalhePage() {
59
63
 
60
64
  const titulo = titulosReceber.find((t) => t.id === id);
61
65
 
66
+ const [availableTags, setAvailableTags] = useState<any[]>([]);
67
+ const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
68
+ const [isCreatingTag, setIsCreatingTag] = useState(false);
69
+
70
+ useEffect(() => {
71
+ setAvailableTags(tags || []);
72
+ }, [tags]);
73
+
74
+ useEffect(() => {
75
+ if (!titulo) {
76
+ return;
77
+ }
78
+
79
+ setSelectedTagIds(Array.isArray(titulo.tags) ? titulo.tags : []);
80
+ }, [titulo]);
81
+
62
82
  const canalBadge = {
63
83
  boleto: {
64
84
  label: t('channels.boleto'),
@@ -108,9 +128,21 @@ export default function TituloReceberDetalhePage() {
108
128
  const cliente = getPessoaById(titulo.clienteId);
109
129
  const categoria = getCategoriaById(titulo.categoriaId);
110
130
  const centroCusto = getCentroCustoById(titulo.centroCustoId);
111
- const tituloTags = titulo.tags
112
- .map((tagId: any) => tags.find((t: any) => t.id === tagId))
113
- .filter(Boolean);
131
+
132
+ const tagOptions = availableTags.map((tag) => ({
133
+ id: String(tag.id),
134
+ name: String(tag.nome || ''),
135
+ color: tag.cor,
136
+ usageCount:
137
+ typeof tag.usageCount === 'number'
138
+ ? tag.usageCount
139
+ : typeof tag.usoCount === 'number'
140
+ ? tag.usoCount
141
+ : typeof tag.count === 'number'
142
+ ? tag.count
143
+ : undefined,
144
+ }));
145
+
114
146
  const canal =
115
147
  canalBadge[titulo.canal as keyof typeof canalBadge] ||
116
148
  canalBadge.transferencia;
@@ -129,6 +161,131 @@ export default function TituloReceberDetalhePage() {
129
161
  depois: log.depois,
130
162
  }));
131
163
 
164
+ const attachmentDetails = Array.isArray(titulo.anexosDetalhes)
165
+ ? titulo.anexosDetalhes
166
+ : (titulo.anexos || []).map((nome: string) => ({ nome }));
167
+
168
+ const handleOpenAttachment = async (fileId?: string) => {
169
+ if (!fileId) {
170
+ return;
171
+ }
172
+
173
+ const response = await request<{ url?: string }>({
174
+ url: `/file/open/${fileId}`,
175
+ method: 'PUT',
176
+ });
177
+
178
+ const url = response?.data?.url;
179
+ if (!url) {
180
+ return;
181
+ }
182
+
183
+ window.open(url, '_blank', 'noopener,noreferrer');
184
+ };
185
+
186
+ const tTagSelector = (key: string, fallback: string) => {
187
+ const fullKey = `tagSelector.${key}`;
188
+ return t.has(fullKey) ? t(fullKey) : fallback;
189
+ };
190
+
191
+ const toTagSlug = (value: string) => {
192
+ return value
193
+ .normalize('NFD')
194
+ .replace(/[\u0300-\u036f]/g, '')
195
+ .toLowerCase()
196
+ .trim()
197
+ .replace(/[^a-z0-9]+/g, '-')
198
+ .replace(/(^-|-$)+/g, '');
199
+ };
200
+
201
+ const handleCreateTag = async (name: string) => {
202
+ const slug = toTagSlug(name);
203
+
204
+ if (!slug) {
205
+ return null;
206
+ }
207
+
208
+ setIsCreatingTag(true);
209
+ try {
210
+ const response = await request<{
211
+ id?: string | number;
212
+ nome?: string;
213
+ cor?: string;
214
+ }>({
215
+ url: '/finance/tags',
216
+ method: 'POST',
217
+ data: {
218
+ name: slug,
219
+ color: '#000000',
220
+ },
221
+ });
222
+
223
+ const created = response?.data;
224
+ const newTag = {
225
+ id: String(created?.id || `temp-${Date.now()}`),
226
+ nome: created?.nome || slug,
227
+ cor: created?.cor || '#000000',
228
+ };
229
+
230
+ setAvailableTags((current) => {
231
+ if (current.some((tag) => String(tag.id) === newTag.id)) {
232
+ return current;
233
+ }
234
+
235
+ return [...current, newTag];
236
+ });
237
+
238
+ showToastHandler?.(
239
+ 'success',
240
+ tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
241
+ );
242
+
243
+ return {
244
+ id: newTag.id,
245
+ name: newTag.nome,
246
+ color: newTag.cor,
247
+ };
248
+ } catch {
249
+ showToastHandler?.(
250
+ 'error',
251
+ tTagSelector('messages.createError', 'Não foi possível criar a tag')
252
+ );
253
+ return null;
254
+ } finally {
255
+ setIsCreatingTag(false);
256
+ }
257
+ };
258
+
259
+ const handleChangeTags = async (nextTagIds: string[]) => {
260
+ if (!titulo?.id) {
261
+ return;
262
+ }
263
+
264
+ try {
265
+ const response = await request<{ tags?: string[] }>({
266
+ url: `/finance/accounts-receivable/installments/${titulo.id}/tags`,
267
+ method: 'PATCH',
268
+ data: {
269
+ tag_ids: nextTagIds.map((tagId) => Number(tagId)),
270
+ },
271
+ });
272
+
273
+ if (Array.isArray(response?.data?.tags)) {
274
+ setSelectedTagIds(response.data.tags);
275
+ } else {
276
+ setSelectedTagIds(nextTagIds);
277
+ }
278
+ } catch {
279
+ showToastHandler?.(
280
+ 'error',
281
+ tTagSelector(
282
+ 'messages.updateError',
283
+ 'Não foi possível atualizar as tags'
284
+ )
285
+ );
286
+ }
287
+ };
288
+
132
289
  return (
133
290
  <Page>
134
291
  <div className="flex items-center gap-4">
@@ -245,20 +402,44 @@ export default function TituloReceberDetalhePage() {
245
402
  <dt className="text-sm font-medium text-muted-foreground">
246
403
  {t('documentData.tags')}
247
404
  </dt>
248
- <dd className="mt-1 flex gap-1">
249
- {tituloTags.length > 0 ? (
250
- tituloTags.map((tag: any) => (
251
- <Badge
252
- key={tag?.id}
253
- variant="outline"
254
- style={{ borderColor: tag?.cor, color: tag?.cor }}
255
- >
256
- {tag?.nome}
257
- </Badge>
258
- ))
259
- ) : (
260
- <span className="text-muted-foreground">-</span>
261
- )}
405
+ <dd className="mt-1">
406
+ <TagSelectorSheet
407
+ selectedTagIds={selectedTagIds}
408
+ tags={tagOptions}
409
+ onChange={handleChangeTags}
410
+ onCreateTag={handleCreateTag}
411
+ disabled={isCreatingTag}
412
+ emptyText={tTagSelector('noTags', 'Sem tags')}
413
+ labels={{
414
+ addTag: tTagSelector('addTag', 'Adicionar tag'),
415
+ sheetTitle: tTagSelector('sheetTitle', 'Gerenciar tags'),
416
+ sheetDescription: tTagSelector(
417
+ 'sheetDescription',
418
+ 'Selecione tags existentes ou crie uma nova.'
419
+ ),
420
+ createLabel: tTagSelector('createLabel', 'Nova tag'),
421
+ createPlaceholder: tTagSelector(
422
+ 'createPlaceholder',
423
+ 'Digite o nome da tag'
424
+ ),
425
+ createAction: tTagSelector('createAction', 'Criar tag'),
426
+ popularTitle: tTagSelector(
427
+ 'popularTitle',
428
+ 'Tags mais usadas'
429
+ ),
430
+ selectedTitle: tTagSelector(
431
+ 'selectedTitle',
432
+ 'Tags selecionadas'
433
+ ),
434
+ noTags: tTagSelector('noTags', 'Sem tags'),
435
+ cancel: tTagSelector('cancel', 'Cancelar'),
436
+ apply: tTagSelector('apply', 'Aplicar'),
437
+ removeTagAria: (tagName: string) =>
438
+ t.has('tagSelector.removeTagAria')
439
+ ? t('tagSelector.removeTagAria', { tag: tagName })
440
+ : `Remover tag ${tagName}`,
441
+ }}
442
+ />
262
443
  </dd>
263
444
  </div>
264
445
  </dl>
@@ -271,16 +452,17 @@ export default function TituloReceberDetalhePage() {
271
452
  <CardDescription>{t('attachments.description')}</CardDescription>
272
453
  </CardHeader>
273
454
  <CardContent>
274
- {titulo.anexos.length > 0 ? (
455
+ {attachmentDetails.length > 0 ? (
275
456
  <ul className="space-y-2">
276
- {titulo.anexos.map((anexo: any, i: number) => (
457
+ {attachmentDetails.map((anexo: any, i: number) => (
277
458
  <li key={i}>
278
459
  <Button
279
460
  variant="ghost"
280
461
  className="h-auto w-full justify-start p-2"
462
+ onClick={() => void handleOpenAttachment(anexo?.id)}
281
463
  >
282
464
  <FileText className="mr-2 h-4 w-4" />
283
- {anexo}
465
+ {anexo?.nome}
284
466
  </Button>
285
467
  </li>
286
468
  ))}