@hed-hog/finance 0.0.235 → 0.0.237

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 (75) hide show
  1. package/dist/dto/create-cost-center.dto.d.ts +4 -0
  2. package/dist/dto/create-cost-center.dto.d.ts.map +1 -0
  3. package/dist/dto/create-cost-center.dto.js +24 -0
  4. package/dist/dto/create-cost-center.dto.js.map +1 -0
  5. package/dist/dto/create-finance-category.dto.d.ts +6 -0
  6. package/dist/dto/create-finance-category.dto.d.ts.map +1 -0
  7. package/dist/dto/create-finance-category.dto.js +37 -0
  8. package/dist/dto/create-finance-category.dto.js.map +1 -0
  9. package/dist/dto/create-period-close.dto.d.ts +7 -0
  10. package/dist/dto/create-period-close.dto.d.ts.map +1 -0
  11. package/dist/dto/create-period-close.dto.js +44 -0
  12. package/dist/dto/create-period-close.dto.js.map +1 -0
  13. package/dist/dto/move-finance-category.dto.d.ts +5 -0
  14. package/dist/dto/move-finance-category.dto.d.ts.map +1 -0
  15. package/dist/dto/move-finance-category.dto.js +32 -0
  16. package/dist/dto/move-finance-category.dto.js.map +1 -0
  17. package/dist/dto/update-cost-center.dto.d.ts +5 -0
  18. package/dist/dto/update-cost-center.dto.d.ts.map +1 -0
  19. package/dist/dto/update-cost-center.dto.js +32 -0
  20. package/dist/dto/update-cost-center.dto.js.map +1 -0
  21. package/dist/dto/update-finance-category.dto.d.ts +7 -0
  22. package/dist/dto/update-finance-category.dto.d.ts.map +1 -0
  23. package/dist/dto/update-finance-category.dto.js +46 -0
  24. package/dist/dto/update-finance-category.dto.js.map +1 -0
  25. package/dist/finance-audit-logs.controller.d.ts +13 -0
  26. package/dist/finance-audit-logs.controller.d.ts.map +1 -0
  27. package/dist/finance-audit-logs.controller.js +54 -0
  28. package/dist/finance-audit-logs.controller.js.map +1 -0
  29. package/dist/finance-categories.controller.d.ts +42 -0
  30. package/dist/finance-categories.controller.d.ts.map +1 -0
  31. package/dist/finance-categories.controller.js +84 -0
  32. package/dist/finance-categories.controller.js.map +1 -0
  33. package/dist/finance-cost-centers.controller.d.ts +32 -0
  34. package/dist/finance-cost-centers.controller.d.ts.map +1 -0
  35. package/dist/finance-cost-centers.controller.js +72 -0
  36. package/dist/finance-cost-centers.controller.js.map +1 -0
  37. package/dist/finance-period-close.controller.d.ts +27 -0
  38. package/dist/finance-period-close.controller.d.ts.map +1 -0
  39. package/dist/finance-period-close.controller.js +64 -0
  40. package/dist/finance-period-close.controller.js.map +1 -0
  41. package/dist/finance.module.d.ts.map +1 -1
  42. package/dist/finance.module.js +8 -0
  43. package/dist/finance.module.js.map +1 -1
  44. package/dist/finance.service.d.ts +111 -0
  45. package/dist/finance.service.d.ts.map +1 -1
  46. package/dist/finance.service.js +446 -17
  47. package/dist/finance.service.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -0
  51. package/dist/index.js.map +1 -1
  52. package/hedhog/data/route.yaml +108 -0
  53. package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +627 -0
  54. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +865 -883
  55. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +838 -861
  56. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +309 -0
  57. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +725 -0
  58. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +378 -0
  59. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +502 -0
  60. package/hedhog/frontend/messages/en.json +225 -0
  61. package/hedhog/frontend/messages/pt.json +225 -0
  62. package/package.json +5 -5
  63. package/src/dto/create-cost-center.dto.ts +9 -0
  64. package/src/dto/create-finance-category.dto.ts +21 -0
  65. package/src/dto/create-period-close.dto.ts +34 -0
  66. package/src/dto/move-finance-category.dto.ts +18 -0
  67. package/src/dto/update-cost-center.dto.ts +17 -0
  68. package/src/dto/update-finance-category.dto.ts +30 -0
  69. package/src/finance-audit-logs.controller.ts +30 -0
  70. package/src/finance-categories.controller.ts +52 -0
  71. package/src/finance-cost-centers.controller.ts +43 -0
  72. package/src/finance-period-close.controller.ts +34 -0
  73. package/src/finance.module.ts +8 -0
  74. package/src/finance.service.ts +578 -9
  75. package/src/index.ts +4 -0
@@ -0,0 +1,502 @@
1
+ 'use client';
2
+
3
+ import { Page, PageHeader, PaginationFooter } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from '@/components/ui/card';
13
+ import {
14
+ Form,
15
+ FormControl,
16
+ FormField,
17
+ FormItem,
18
+ FormLabel,
19
+ FormMessage,
20
+ } from '@/components/ui/form';
21
+ import { Input } from '@/components/ui/input';
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectItem,
26
+ SelectTrigger,
27
+ SelectValue,
28
+ } from '@/components/ui/select';
29
+ import {
30
+ Sheet,
31
+ SheetContent,
32
+ SheetDescription,
33
+ SheetHeader,
34
+ SheetTitle,
35
+ } from '@/components/ui/sheet';
36
+ import {
37
+ Table,
38
+ TableBody,
39
+ TableCell,
40
+ TableHead,
41
+ TableHeader,
42
+ TableRow,
43
+ } from '@/components/ui/table';
44
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
45
+ import { zodResolver } from '@hookform/resolvers/zod';
46
+ import { CalendarCheck2, Plus, Search, X } from 'lucide-react';
47
+ import { useTranslations } from 'next-intl';
48
+ import { useState } from 'react';
49
+ import { useForm } from 'react-hook-form';
50
+ import { z } from 'zod';
51
+
52
+ type PeriodClose = {
53
+ id: string;
54
+ periodStart: string | null;
55
+ periodEnd: string | null;
56
+ status: 'open' | 'closed';
57
+ closedAt: string | null;
58
+ closedByUserId: string | null;
59
+ closedByName: string;
60
+ closedByEmail: string;
61
+ notes: string;
62
+ createdAt: string | null;
63
+ };
64
+
65
+ type PaginatedPeriodClose = {
66
+ total: number;
67
+ lastPage: number;
68
+ page: number;
69
+ pageSize: number;
70
+ data: PeriodClose[];
71
+ };
72
+
73
+ type ClosePeriodFormValues = {
74
+ periodStart: string;
75
+ periodEnd: string;
76
+ notes?: string;
77
+ status: 'closed' | 'open';
78
+ };
79
+
80
+ function ClosePeriodSheet({
81
+ open,
82
+ onOpenChange,
83
+ onCreated,
84
+ t,
85
+ }: {
86
+ open: boolean;
87
+ onOpenChange: (open: boolean) => void;
88
+ onCreated: () => Promise<any> | void;
89
+ t: ReturnType<typeof useTranslations>;
90
+ }) {
91
+ const { request, showToastHandler } = useApp();
92
+
93
+ const closePeriodSchema = z.object({
94
+ periodStart: z.string().min(1, t('sheet.validation.startRequired')),
95
+ periodEnd: z.string().min(1, t('sheet.validation.endRequired')),
96
+ notes: z.string().optional(),
97
+ status: z.enum(['closed', 'open']).default('closed'),
98
+ });
99
+
100
+ const form = useForm<ClosePeriodFormValues>({
101
+ resolver: zodResolver(closePeriodSchema),
102
+ defaultValues: {
103
+ periodStart: '',
104
+ periodEnd: '',
105
+ notes: '',
106
+ status: 'closed',
107
+ },
108
+ });
109
+
110
+ const onSubmit = async (values: ClosePeriodFormValues) => {
111
+ try {
112
+ await request({
113
+ url: '/finance/period-close',
114
+ method: 'POST',
115
+ data: {
116
+ period_start: values.periodStart,
117
+ period_end: values.periodEnd,
118
+ notes: values.notes?.trim() || undefined,
119
+ status: values.status,
120
+ },
121
+ });
122
+
123
+ await onCreated();
124
+ form.reset();
125
+ onOpenChange(false);
126
+ showToastHandler?.('success', t('messages.createSuccess'));
127
+ } catch {
128
+ showToastHandler?.('error', t('messages.createError'));
129
+ }
130
+ };
131
+
132
+ return (
133
+ <Sheet
134
+ open={open}
135
+ onOpenChange={(nextOpen) => {
136
+ onOpenChange(nextOpen);
137
+ if (!nextOpen) {
138
+ form.reset();
139
+ }
140
+ }}
141
+ >
142
+ <SheetContent className="w-full sm:max-w-lg">
143
+ <SheetHeader>
144
+ <SheetTitle>{t('sheet.title')}</SheetTitle>
145
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
146
+ </SheetHeader>
147
+
148
+ <Form {...form}>
149
+ <form
150
+ className="space-y-4 p-4"
151
+ onSubmit={form.handleSubmit(onSubmit)}
152
+ >
153
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
154
+ <FormField
155
+ control={form.control}
156
+ name="periodStart"
157
+ render={({ field }) => (
158
+ <FormItem>
159
+ <FormLabel>{t('sheet.fields.startDate')}</FormLabel>
160
+ <FormControl>
161
+ <Input type="date" {...field} />
162
+ </FormControl>
163
+ <FormMessage />
164
+ </FormItem>
165
+ )}
166
+ />
167
+
168
+ <FormField
169
+ control={form.control}
170
+ name="periodEnd"
171
+ render={({ field }) => (
172
+ <FormItem>
173
+ <FormLabel>{t('sheet.fields.endDate')}</FormLabel>
174
+ <FormControl>
175
+ <Input type="date" {...field} />
176
+ </FormControl>
177
+ <FormMessage />
178
+ </FormItem>
179
+ )}
180
+ />
181
+ </div>
182
+
183
+ <FormField
184
+ control={form.control}
185
+ name="status"
186
+ render={({ field }) => (
187
+ <FormItem>
188
+ <FormLabel>{t('sheet.fields.status')}</FormLabel>
189
+ <Select value={field.value} onValueChange={field.onChange}>
190
+ <FormControl>
191
+ <SelectTrigger>
192
+ <SelectValue placeholder={t('common.select')} />
193
+ </SelectTrigger>
194
+ </FormControl>
195
+ <SelectContent>
196
+ <SelectItem value="closed">
197
+ {t('status.closed')}
198
+ </SelectItem>
199
+ <SelectItem value="open">{t('status.open')}</SelectItem>
200
+ </SelectContent>
201
+ </Select>
202
+ <FormMessage />
203
+ </FormItem>
204
+ )}
205
+ />
206
+
207
+ <FormField
208
+ control={form.control}
209
+ name="notes"
210
+ render={({ field }) => (
211
+ <FormItem>
212
+ <FormLabel>{t('sheet.fields.notes')}</FormLabel>
213
+ <FormControl>
214
+ <Input
215
+ placeholder={t('sheet.fields.notesPlaceholder')}
216
+ {...field}
217
+ value={field.value || ''}
218
+ />
219
+ </FormControl>
220
+ <FormMessage />
221
+ </FormItem>
222
+ )}
223
+ />
224
+
225
+ <div className="flex justify-end gap-2">
226
+ <Button
227
+ type="button"
228
+ variant="outline"
229
+ onClick={() => onOpenChange(false)}
230
+ >
231
+ {t('common.cancel')}
232
+ </Button>
233
+ <Button type="submit" disabled={form.formState.isSubmitting}>
234
+ {t('common.save')}
235
+ </Button>
236
+ </div>
237
+ </form>
238
+ </Form>
239
+ </SheetContent>
240
+ </Sheet>
241
+ );
242
+ }
243
+
244
+ export default function PeriodClosePage() {
245
+ const t = useTranslations('finance.AdminPeriodClosePage');
246
+ const { request } = useApp();
247
+
248
+ const [sheetOpen, setSheetOpen] = useState(false);
249
+ const [page, setPage] = useState(1);
250
+ const [pageSize, setPageSize] = useState(10);
251
+ const [search, setSearch] = useState('');
252
+ const [status, setStatus] = useState('all');
253
+ const [user, setUser] = useState('');
254
+ const [from, setFrom] = useState('');
255
+ const [to, setTo] = useState('');
256
+
257
+ const { data, isLoading, refetch } = useQuery<PaginatedPeriodClose>({
258
+ queryKey: [
259
+ 'finance-period-close',
260
+ page,
261
+ pageSize,
262
+ search,
263
+ status,
264
+ user,
265
+ from,
266
+ to,
267
+ ],
268
+ queryFn: async () => {
269
+ const params = new URLSearchParams();
270
+ params.set('page', String(page));
271
+ params.set('pageSize', String(pageSize));
272
+ params.set('sortField', 'period_start');
273
+ params.set('sortOrder', 'desc');
274
+
275
+ if (search.trim()) params.set('search', search.trim());
276
+ if (status !== 'all') params.set('status', status);
277
+ if (user.trim()) params.set('user', user.trim());
278
+ if (from) params.set('from', from);
279
+ if (to) params.set('to', to);
280
+
281
+ const response = await request({
282
+ url: `/finance/period-close?${params.toString()}`,
283
+ method: 'GET',
284
+ });
285
+
286
+ return (response.data || {
287
+ total: 0,
288
+ lastPage: 1,
289
+ page: 1,
290
+ pageSize,
291
+ data: [],
292
+ }) as PaginatedPeriodClose;
293
+ },
294
+ placeholderData: (old) => old,
295
+ });
296
+
297
+ const rows = data?.data || [];
298
+
299
+ return (
300
+ <Page>
301
+ <PageHeader
302
+ title={t('header.title')}
303
+ description={t('header.description')}
304
+ breadcrumbs={[
305
+ { label: t('breadcrumbs.finance'), href: '/finance' },
306
+ {
307
+ label: t('breadcrumbs.administration'),
308
+ href: '/finance/administration',
309
+ },
310
+ { label: t('breadcrumbs.current') },
311
+ ]}
312
+ />
313
+
314
+ <Card>
315
+ <CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
316
+ <div>
317
+ <CardTitle className="flex items-center gap-2">
318
+ <CalendarCheck2 className="h-5 w-5" />
319
+ {t('card.title')}
320
+ </CardTitle>
321
+ <CardDescription>{t('card.description')}</CardDescription>
322
+ </div>
323
+ <Button className="gap-2" onClick={() => setSheetOpen(true)}>
324
+ <Plus className="h-4 w-4" />
325
+ {t('actions.new')}
326
+ </Button>
327
+ </CardHeader>
328
+
329
+ <CardContent className="space-y-4">
330
+ <div className="grid gap-3 md:grid-cols-2 lg:grid-cols-4">
331
+ <div className="relative lg:col-span-2">
332
+ <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
333
+ <Input
334
+ placeholder={t('filters.searchPlaceholder')}
335
+ value={search}
336
+ onChange={(event) => {
337
+ setSearch(event.target.value);
338
+ setPage(1);
339
+ }}
340
+ className="pl-8"
341
+ />
342
+ </div>
343
+
344
+ <Select
345
+ value={status}
346
+ onValueChange={(value) => {
347
+ setStatus(value);
348
+ setPage(1);
349
+ }}
350
+ >
351
+ <SelectTrigger>
352
+ <SelectValue placeholder={t('filters.status')} />
353
+ </SelectTrigger>
354
+ <SelectContent>
355
+ <SelectItem value="all">{t('filters.allStatus')}</SelectItem>
356
+ <SelectItem value="closed">{t('status.closed')}</SelectItem>
357
+ <SelectItem value="open">{t('status.open')}</SelectItem>
358
+ </SelectContent>
359
+ </Select>
360
+
361
+ <Input
362
+ placeholder={t('filters.userPlaceholder')}
363
+ value={user}
364
+ onChange={(event) => {
365
+ setUser(event.target.value);
366
+ setPage(1);
367
+ }}
368
+ />
369
+
370
+ <Input
371
+ type="date"
372
+ value={from}
373
+ onChange={(event) => {
374
+ setFrom(event.target.value);
375
+ setPage(1);
376
+ }}
377
+ />
378
+
379
+ <Input
380
+ type="date"
381
+ value={to}
382
+ onChange={(event) => {
383
+ setTo(event.target.value);
384
+ setPage(1);
385
+ }}
386
+ />
387
+
388
+ <Button
389
+ variant="outline"
390
+ className="gap-2"
391
+ onClick={() => {
392
+ setSearch('');
393
+ setStatus('all');
394
+ setUser('');
395
+ setFrom('');
396
+ setTo('');
397
+ setPage(1);
398
+ refetch();
399
+ }}
400
+ >
401
+ <X className="h-4 w-4" />
402
+ {t('filters.clear')}
403
+ </Button>
404
+ </div>
405
+
406
+ <div className="rounded-md border">
407
+ <Table>
408
+ <TableHeader>
409
+ <TableRow>
410
+ <TableHead>{t('table.headers.start')}</TableHead>
411
+ <TableHead>{t('table.headers.end')}</TableHead>
412
+ <TableHead>{t('table.headers.status')}</TableHead>
413
+ <TableHead>{t('table.headers.closedBy')}</TableHead>
414
+ <TableHead>{t('table.headers.closedAt')}</TableHead>
415
+ <TableHead>{t('table.headers.notes')}</TableHead>
416
+ </TableRow>
417
+ </TableHeader>
418
+ <TableBody>
419
+ {isLoading ? (
420
+ <TableRow>
421
+ <TableCell colSpan={6} className="h-24 text-center">
422
+ {t('table.loading')}
423
+ </TableCell>
424
+ </TableRow>
425
+ ) : rows.length === 0 ? (
426
+ <TableRow>
427
+ <TableCell colSpan={6} className="h-24 text-center">
428
+ {t('table.empty')}
429
+ </TableCell>
430
+ </TableRow>
431
+ ) : (
432
+ rows.map((row) => (
433
+ <TableRow key={row.id}>
434
+ <TableCell>
435
+ {row.periodStart
436
+ ? new Date(row.periodStart).toLocaleDateString(
437
+ 'pt-BR'
438
+ )
439
+ : '-'}
440
+ </TableCell>
441
+ <TableCell>
442
+ {row.periodEnd
443
+ ? new Date(row.periodEnd).toLocaleDateString('pt-BR')
444
+ : '-'}
445
+ </TableCell>
446
+ <TableCell>
447
+ <Badge
448
+ variant={
449
+ row.status === 'closed' ? 'default' : 'secondary'
450
+ }
451
+ >
452
+ {row.status === 'closed'
453
+ ? t('status.closed')
454
+ : t('status.open')}
455
+ </Badge>
456
+ </TableCell>
457
+ <TableCell>
458
+ <div className="flex flex-col">
459
+ <span>{row.closedByName || '-'}</span>
460
+ <span className="text-xs text-muted-foreground">
461
+ {row.closedByEmail || row.closedByUserId || '-'}
462
+ </span>
463
+ </div>
464
+ </TableCell>
465
+ <TableCell>
466
+ {row.closedAt
467
+ ? new Date(row.closedAt).toLocaleString('pt-BR')
468
+ : '-'}
469
+ </TableCell>
470
+ <TableCell className="max-w-[320px] truncate">
471
+ {row.notes || '-'}
472
+ </TableCell>
473
+ </TableRow>
474
+ ))
475
+ )}
476
+ </TableBody>
477
+ </Table>
478
+ </div>
479
+
480
+ <PaginationFooter
481
+ currentPage={data?.page || page}
482
+ pageSize={data?.pageSize || pageSize}
483
+ totalItems={data?.total || 0}
484
+ onPageChange={(newPage) => setPage(newPage)}
485
+ onPageSizeChange={(newPageSize) => {
486
+ setPageSize(newPageSize);
487
+ setPage(1);
488
+ }}
489
+ pageSizeOptions={[10, 20, 30, 40, 50]}
490
+ />
491
+ </CardContent>
492
+ </Card>
493
+
494
+ <ClosePeriodSheet
495
+ open={sheetOpen}
496
+ onOpenChange={setSheetOpen}
497
+ onCreated={refetch}
498
+ t={t}
499
+ />
500
+ </Page>
501
+ );
502
+ }