@hed-hog/finance 0.0.299 → 0.0.300
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-bank-account.dto.d.ts +1 -0
- package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
- package/dist/dto/create-bank-account.dto.js +7 -0
- package/dist/dto/create-bank-account.dto.js.map +1 -1
- package/dist/dto/update-bank-account.dto.d.ts +1 -0
- package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
- package/dist/dto/update-bank-account.dto.js +7 -0
- package/dist/dto/update-bank-account.dto.js.map +1 -1
- package/dist/finance-bank-accounts.controller.d.ts +3 -0
- package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
- package/dist/finance-data.controller.d.ts +1 -0
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance.service.d.ts +4 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +5 -0
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/dashboard.yaml +6 -0
- package/hedhog/data/dashboard_component.yaml +72 -17
- package/hedhog/data/dashboard_component_role.yaml +30 -0
- package/hedhog/data/dashboard_item.yaml +155 -0
- package/hedhog/data/dashboard_role.yaml +6 -0
- package/hedhog/data/role_menu.yaml +6 -0
- package/hedhog/data/role_route.yaml +127 -0
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +328 -12
- package/hedhog/frontend/messages/en.json +48 -2
- package/hedhog/frontend/messages/pt.json +48 -2
- package/hedhog/frontend/widgets/alerts.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/bank-reconciliation-status.tsx.ejs +142 -0
- package/hedhog/frontend/widgets/cash-balance-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/cash-flow-chart.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/default-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/payable-30d-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/pending-approvals-kpi.tsx.ejs +78 -0
- package/hedhog/frontend/widgets/pending-approvals-list.tsx.ejs +147 -0
- package/hedhog/frontend/widgets/pending-reconciliation-kpi.tsx.ejs +84 -0
- package/hedhog/frontend/widgets/receivable-30d-kpi.tsx.ejs +9 -9
- package/hedhog/frontend/widgets/receivable-aging-analysis.tsx.ejs +163 -0
- package/hedhog/frontend/widgets/upcoming-payable.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/upcoming-receivable.tsx.ejs +1 -1
- package/hedhog/table/bank_account.yaml +8 -0
- package/package.json +5 -5
- package/src/dto/create-bank-account.dto.ts +7 -1
- package/src/dto/update-bank-account.dto.ts +7 -1
- package/src/finance.service.ts +4 -0
|
@@ -59,10 +59,11 @@ import {
|
|
|
59
59
|
TrendingUp,
|
|
60
60
|
Upload,
|
|
61
61
|
Wallet,
|
|
62
|
+
type LucideIcon,
|
|
62
63
|
} from 'lucide-react';
|
|
63
64
|
import { useTranslations } from 'next-intl';
|
|
64
65
|
import Link from 'next/link';
|
|
65
|
-
import { useEffect, useState } from 'react';
|
|
66
|
+
import { useEffect, useRef, useState, type ChangeEvent } from 'react';
|
|
66
67
|
import { useForm } from 'react-hook-form';
|
|
67
68
|
import { z } from 'zod';
|
|
68
69
|
|
|
@@ -72,6 +73,7 @@ const bankAccountFormSchema = z.object({
|
|
|
72
73
|
conta: z.string().optional(),
|
|
73
74
|
tipo: z.string().min(1, 'Tipo é obrigatório'),
|
|
74
75
|
descricao: z.string().optional(),
|
|
76
|
+
logoFileId: z.number().int().nullable().optional(),
|
|
75
77
|
saldoInicial: z.number().min(0, 'Saldo inicial inválido'),
|
|
76
78
|
});
|
|
77
79
|
|
|
@@ -85,11 +87,48 @@ type BankAccount = {
|
|
|
85
87
|
agencia: string;
|
|
86
88
|
conta: string;
|
|
87
89
|
tipo: 'corrente' | 'poupanca' | 'investimento' | 'caixa';
|
|
90
|
+
logoFileId: number | null;
|
|
88
91
|
saldoAtual: number;
|
|
89
92
|
saldoConciliado: number;
|
|
90
93
|
ativo: boolean;
|
|
91
94
|
};
|
|
92
95
|
|
|
96
|
+
type UploadedFilePayload = {
|
|
97
|
+
id?: number | string | null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function BankAccountLogo({
|
|
101
|
+
account,
|
|
102
|
+
icon: Icon,
|
|
103
|
+
}: {
|
|
104
|
+
account: BankAccount;
|
|
105
|
+
icon: LucideIcon;
|
|
106
|
+
}) {
|
|
107
|
+
const [hasImageError, setHasImageError] = useState(false);
|
|
108
|
+
const apiBaseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '');
|
|
109
|
+
const logoUrl =
|
|
110
|
+
account.logoFileId && account.logoFileId > 0
|
|
111
|
+
? `${apiBaseUrl}/file/open/${account.logoFileId}?v=${account.logoFileId}`
|
|
112
|
+
: null;
|
|
113
|
+
|
|
114
|
+
if (!logoUrl || hasImageError) {
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
|
|
117
|
+
<Icon className="h-5 w-5" />
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<img
|
|
124
|
+
src={logoUrl}
|
|
125
|
+
alt={account.banco}
|
|
126
|
+
className="h-10 w-10 rounded-lg border bg-muted object-cover"
|
|
127
|
+
onError={() => setHasImageError(true)}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
function NovaContaSheet({
|
|
94
133
|
t,
|
|
95
134
|
onCreated,
|
|
@@ -119,6 +158,21 @@ function NovaContaSheet({
|
|
|
119
158
|
const updateErrorMessage = t.has('messages.updateError')
|
|
120
159
|
? t('messages.updateError')
|
|
121
160
|
: 'Erro ao atualizar conta bancária';
|
|
161
|
+
const logoUploadSuccessMessage = t.has('messages.logoUploadSuccess')
|
|
162
|
+
? t('messages.logoUploadSuccess')
|
|
163
|
+
: 'Logo enviado com sucesso';
|
|
164
|
+
const logoUploadErrorMessage = t.has('messages.logoUploadError')
|
|
165
|
+
? t('messages.logoUploadError')
|
|
166
|
+
: 'Erro ao enviar logo';
|
|
167
|
+
const logoRemoveSuccessMessage = t.has('messages.logoRemoveSuccess')
|
|
168
|
+
? t('messages.logoRemoveSuccess')
|
|
169
|
+
: 'Logo removido com sucesso';
|
|
170
|
+
const logoInvalidTypeMessage = t.has('messages.logoInvalidType')
|
|
171
|
+
? t('messages.logoInvalidType')
|
|
172
|
+
: 'Selecione um arquivo de imagem válido';
|
|
173
|
+
const logoTooLargeMessage = t.has('messages.logoTooLarge')
|
|
174
|
+
? t('messages.logoTooLarge')
|
|
175
|
+
: 'O logo deve ter no máximo 5 MB';
|
|
122
176
|
|
|
123
177
|
const form = useForm<BankAccountFormValues>({
|
|
124
178
|
resolver: zodResolver(bankAccountFormSchema),
|
|
@@ -128,38 +182,204 @@ function NovaContaSheet({
|
|
|
128
182
|
conta: '',
|
|
129
183
|
tipo: '',
|
|
130
184
|
descricao: '',
|
|
185
|
+
logoFileId: null,
|
|
131
186
|
saldoInicial: 0,
|
|
132
187
|
},
|
|
133
188
|
});
|
|
134
189
|
|
|
190
|
+
const [logoFileId, setLogoFileId] = useState<number | null>(null);
|
|
191
|
+
const [persistedLogoFileId, setPersistedLogoFileId] = useState<number | null>(
|
|
192
|
+
null
|
|
193
|
+
);
|
|
194
|
+
const [logoPreviewUrl, setLogoPreviewUrl] = useState<string | null>(null);
|
|
195
|
+
const [isUploadingLogo, setIsUploadingLogo] = useState(false);
|
|
196
|
+
const [logoUploadProgress, setLogoUploadProgress] = useState(0);
|
|
197
|
+
const logoInputRef = useRef<HTMLInputElement | null>(null);
|
|
198
|
+
|
|
199
|
+
const getLogoUrl = (fileId?: number | null) => {
|
|
200
|
+
if (typeof fileId !== 'number' || fileId <= 0) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return `${String(process.env.NEXT_PUBLIC_API_BASE_URL || '')}/file/open/${fileId}`;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const deleteFileById = async (fileId?: number | null) => {
|
|
208
|
+
if (!fileId || fileId <= 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await request({
|
|
214
|
+
url: '/file',
|
|
215
|
+
method: 'DELETE',
|
|
216
|
+
data: { ids: [fileId] },
|
|
217
|
+
});
|
|
218
|
+
} catch {
|
|
219
|
+
// Ignore cleanup failures to keep the form stable.
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const cleanupUnsavedLogo = async () => {
|
|
224
|
+
if (logoFileId && logoFileId !== persistedLogoFileId) {
|
|
225
|
+
await deleteFileById(logoFileId);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const handleSelectLogo = () => {
|
|
230
|
+
if (!logoInputRef.current || isUploadingLogo) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
logoInputRef.current.value = '';
|
|
235
|
+
logoInputRef.current.click();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handleLogoUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
|
239
|
+
const file = event.target.files?.[0];
|
|
240
|
+
|
|
241
|
+
if (!file) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!file.type.startsWith('image/')) {
|
|
246
|
+
showToastHandler?.('error', logoInvalidTypeMessage);
|
|
247
|
+
event.target.value = '';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
252
|
+
showToastHandler?.('error', logoTooLargeMessage);
|
|
253
|
+
event.target.value = '';
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
setIsUploadingLogo(true);
|
|
258
|
+
setLogoUploadProgress(0);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const previousUploadedLogoId = logoFileId;
|
|
262
|
+
const formData = new FormData();
|
|
263
|
+
formData.append('file', file);
|
|
264
|
+
formData.append('destination', 'finance/bank-account/logo');
|
|
265
|
+
|
|
266
|
+
const { data } = await request<UploadedFilePayload>({
|
|
267
|
+
url: '/file',
|
|
268
|
+
method: 'POST',
|
|
269
|
+
data: formData,
|
|
270
|
+
headers: {
|
|
271
|
+
'Content-Type': 'multipart/form-data',
|
|
272
|
+
},
|
|
273
|
+
onUploadProgress: (progressEvent) => {
|
|
274
|
+
if (!progressEvent.total) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const progress = Math.round(
|
|
279
|
+
(progressEvent.loaded * 100) / progressEvent.total
|
|
280
|
+
);
|
|
281
|
+
setLogoUploadProgress(progress);
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const nextLogoFileId = Number(data?.id);
|
|
286
|
+
|
|
287
|
+
if (!nextLogoFileId) {
|
|
288
|
+
throw new Error('Logo upload failed');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
previousUploadedLogoId &&
|
|
293
|
+
previousUploadedLogoId !== persistedLogoFileId
|
|
294
|
+
) {
|
|
295
|
+
await deleteFileById(previousUploadedLogoId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setLogoFileId(nextLogoFileId);
|
|
299
|
+
setLogoPreviewUrl(`${getLogoUrl(nextLogoFileId)}?ts=${Date.now()}`);
|
|
300
|
+
setLogoUploadProgress(100);
|
|
301
|
+
form.setValue('logoFileId', nextLogoFileId, {
|
|
302
|
+
shouldDirty: true,
|
|
303
|
+
shouldValidate: true,
|
|
304
|
+
});
|
|
305
|
+
showToastHandler?.('success', logoUploadSuccessMessage);
|
|
306
|
+
} catch {
|
|
307
|
+
showToastHandler?.('error', logoUploadErrorMessage);
|
|
308
|
+
setLogoUploadProgress(0);
|
|
309
|
+
} finally {
|
|
310
|
+
setIsUploadingLogo(false);
|
|
311
|
+
event.target.value = '';
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const handleRemoveLogo = async () => {
|
|
316
|
+
if (isUploadingLogo) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (logoFileId && logoFileId !== persistedLogoFileId) {
|
|
321
|
+
await deleteFileById(logoFileId);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
setLogoFileId(null);
|
|
325
|
+
setLogoPreviewUrl(null);
|
|
326
|
+
setLogoUploadProgress(0);
|
|
327
|
+
form.setValue('logoFileId', null, {
|
|
328
|
+
shouldDirty: true,
|
|
329
|
+
shouldValidate: true,
|
|
330
|
+
});
|
|
331
|
+
showToastHandler?.('success', logoRemoveSuccessMessage);
|
|
332
|
+
};
|
|
333
|
+
|
|
135
334
|
useEffect(() => {
|
|
136
335
|
if (!open) {
|
|
137
336
|
return;
|
|
138
337
|
}
|
|
139
338
|
|
|
140
339
|
if (editingAccount) {
|
|
340
|
+
const currentLogoFileId = editingAccount.logoFileId ?? null;
|
|
341
|
+
|
|
342
|
+
setLogoFileId(currentLogoFileId);
|
|
343
|
+
setPersistedLogoFileId(currentLogoFileId);
|
|
344
|
+
setLogoPreviewUrl(
|
|
345
|
+
currentLogoFileId
|
|
346
|
+
? `${getLogoUrl(currentLogoFileId)}?ts=${Date.now()}`
|
|
347
|
+
: null
|
|
348
|
+
);
|
|
349
|
+
setLogoUploadProgress(0);
|
|
350
|
+
|
|
141
351
|
form.reset({
|
|
142
352
|
banco: editingAccount.banco,
|
|
143
353
|
agencia: editingAccount.agencia === '-' ? '' : editingAccount.agencia,
|
|
144
354
|
conta: editingAccount.conta === '-' ? '' : editingAccount.conta,
|
|
145
355
|
tipo: editingAccount.tipo,
|
|
146
356
|
descricao: editingAccount.descricao,
|
|
357
|
+
logoFileId: currentLogoFileId,
|
|
147
358
|
saldoInicial: editingAccount.saldoAtual,
|
|
148
359
|
});
|
|
149
360
|
return;
|
|
150
361
|
}
|
|
151
362
|
|
|
363
|
+
setLogoFileId(null);
|
|
364
|
+
setPersistedLogoFileId(null);
|
|
365
|
+
setLogoPreviewUrl(null);
|
|
366
|
+
setLogoUploadProgress(0);
|
|
367
|
+
|
|
152
368
|
form.reset({
|
|
153
369
|
banco: '',
|
|
154
370
|
agencia: '',
|
|
155
371
|
conta: '',
|
|
156
372
|
tipo: '',
|
|
157
373
|
descricao: '',
|
|
374
|
+
logoFileId: null,
|
|
158
375
|
saldoInicial: 0,
|
|
159
376
|
});
|
|
160
377
|
}, [editingAccount, form, open]);
|
|
161
378
|
|
|
162
379
|
const handleSubmit = async (values: BankAccountFormValues) => {
|
|
380
|
+
const nextLogoFileId = values.logoFileId ?? null;
|
|
381
|
+
const previousPersistedLogoId = persistedLogoFileId;
|
|
382
|
+
|
|
163
383
|
try {
|
|
164
384
|
if (editingAccount) {
|
|
165
385
|
await request({
|
|
@@ -171,6 +391,7 @@ function NovaContaSheet({
|
|
|
171
391
|
account: values.conta || undefined,
|
|
172
392
|
type: values.tipo,
|
|
173
393
|
description: values.descricao?.trim() || undefined,
|
|
394
|
+
logo_file_id: nextLogoFileId,
|
|
174
395
|
},
|
|
175
396
|
});
|
|
176
397
|
} else {
|
|
@@ -183,13 +404,36 @@ function NovaContaSheet({
|
|
|
183
404
|
account: values.conta || undefined,
|
|
184
405
|
type: values.tipo,
|
|
185
406
|
description: values.descricao?.trim() || undefined,
|
|
407
|
+
logo_file_id: nextLogoFileId,
|
|
186
408
|
initial_balance: values.saldoInicial,
|
|
187
409
|
},
|
|
188
410
|
});
|
|
189
411
|
}
|
|
190
412
|
|
|
191
413
|
await onCreated();
|
|
192
|
-
|
|
414
|
+
|
|
415
|
+
if (
|
|
416
|
+
previousPersistedLogoId &&
|
|
417
|
+
previousPersistedLogoId !== nextLogoFileId
|
|
418
|
+
) {
|
|
419
|
+
await deleteFileById(previousPersistedLogoId);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
setPersistedLogoFileId(nextLogoFileId);
|
|
423
|
+
setLogoFileId(nextLogoFileId);
|
|
424
|
+
setLogoPreviewUrl(
|
|
425
|
+
nextLogoFileId ? `${getLogoUrl(nextLogoFileId)}?ts=${Date.now()}` : null
|
|
426
|
+
);
|
|
427
|
+
setLogoUploadProgress(0);
|
|
428
|
+
form.reset({
|
|
429
|
+
banco: '',
|
|
430
|
+
agencia: '',
|
|
431
|
+
conta: '',
|
|
432
|
+
tipo: '',
|
|
433
|
+
descricao: '',
|
|
434
|
+
logoFileId: null,
|
|
435
|
+
saldoInicial: 0,
|
|
436
|
+
});
|
|
193
437
|
onOpenChange(false);
|
|
194
438
|
onEditingAccountChange(null);
|
|
195
439
|
showToastHandler?.(
|
|
@@ -205,6 +449,7 @@ function NovaContaSheet({
|
|
|
205
449
|
};
|
|
206
450
|
|
|
207
451
|
const handleCancel = () => {
|
|
452
|
+
void cleanupUnsavedLogo();
|
|
208
453
|
form.reset();
|
|
209
454
|
onEditingAccountChange(null);
|
|
210
455
|
onOpenChange(false);
|
|
@@ -214,6 +459,10 @@ function NovaContaSheet({
|
|
|
214
459
|
<Sheet
|
|
215
460
|
open={open}
|
|
216
461
|
onOpenChange={(nextOpen) => {
|
|
462
|
+
if (!nextOpen) {
|
|
463
|
+
void cleanupUnsavedLogo();
|
|
464
|
+
}
|
|
465
|
+
|
|
217
466
|
onOpenChange(nextOpen);
|
|
218
467
|
if (!nextOpen) {
|
|
219
468
|
onEditingAccountChange(null);
|
|
@@ -361,13 +610,79 @@ function NovaContaSheet({
|
|
|
361
610
|
</FormItem>
|
|
362
611
|
)}
|
|
363
612
|
/>
|
|
613
|
+
|
|
614
|
+
<FormItem>
|
|
615
|
+
<FormLabel>{t('fields.logo')}</FormLabel>
|
|
616
|
+
<div className="flex items-start gap-4 rounded-lg border p-3">
|
|
617
|
+
<div className="flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
|
|
618
|
+
{logoPreviewUrl ? (
|
|
619
|
+
<img
|
|
620
|
+
src={logoPreviewUrl}
|
|
621
|
+
alt={t('fields.logo')}
|
|
622
|
+
className="h-full w-full object-cover"
|
|
623
|
+
/>
|
|
624
|
+
) : (
|
|
625
|
+
<Landmark className="h-8 w-8 text-muted-foreground" />
|
|
626
|
+
)}
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<div className="flex-1 space-y-2">
|
|
630
|
+
<input
|
|
631
|
+
ref={logoInputRef}
|
|
632
|
+
type="file"
|
|
633
|
+
accept="image/*"
|
|
634
|
+
className="hidden"
|
|
635
|
+
onChange={handleLogoUpload}
|
|
636
|
+
/>
|
|
637
|
+
|
|
638
|
+
<div className="flex flex-wrap gap-2">
|
|
639
|
+
<Button
|
|
640
|
+
type="button"
|
|
641
|
+
variant="outline"
|
|
642
|
+
onClick={handleSelectLogo}
|
|
643
|
+
disabled={isUploadingLogo}
|
|
644
|
+
>
|
|
645
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
646
|
+
{t('fields.logoAction')}
|
|
647
|
+
</Button>
|
|
648
|
+
|
|
649
|
+
{logoFileId ? (
|
|
650
|
+
<Button
|
|
651
|
+
type="button"
|
|
652
|
+
variant="ghost"
|
|
653
|
+
onClick={() => void handleRemoveLogo()}
|
|
654
|
+
disabled={isUploadingLogo}
|
|
655
|
+
>
|
|
656
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
657
|
+
{t('fields.logoRemove')}
|
|
658
|
+
</Button>
|
|
659
|
+
) : null}
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<p className="text-xs text-muted-foreground">
|
|
663
|
+
{t('fields.logoHint')}
|
|
664
|
+
</p>
|
|
665
|
+
|
|
666
|
+
{isUploadingLogo ? (
|
|
667
|
+
<p className="text-xs text-muted-foreground">
|
|
668
|
+
{t('fields.logoUploading', {
|
|
669
|
+
progress: logoUploadProgress,
|
|
670
|
+
})}
|
|
671
|
+
</p>
|
|
672
|
+
) : null}
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
</FormItem>
|
|
364
676
|
</div>
|
|
365
677
|
|
|
366
678
|
<div className="flex justify-end gap-2 pt-4">
|
|
367
679
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
368
680
|
{t('common.cancel')}
|
|
369
681
|
</Button>
|
|
370
|
-
<Button
|
|
682
|
+
<Button
|
|
683
|
+
type="submit"
|
|
684
|
+
disabled={form.formState.isSubmitting || isUploadingLogo}
|
|
685
|
+
>
|
|
371
686
|
{t('common.save')}
|
|
372
687
|
</Button>
|
|
373
688
|
</div>
|
|
@@ -565,9 +880,7 @@ export default function ContasBancariasPage() {
|
|
|
565
880
|
<CardHeader>
|
|
566
881
|
<div className="flex items-center justify-between">
|
|
567
882
|
<div className="flex items-center gap-2">
|
|
568
|
-
<
|
|
569
|
-
<TipoIcon className="h-5 w-5" />
|
|
570
|
-
</div>
|
|
883
|
+
<BankAccountLogo account={conta} icon={TipoIcon} />
|
|
571
884
|
<div>
|
|
572
885
|
<div className="flex gap-4 items-center">
|
|
573
886
|
<CardTitle className="text-base">
|
|
@@ -655,21 +968,24 @@ export default function ContasBancariasPage() {
|
|
|
655
968
|
{t('accountCard.reconcile')}
|
|
656
969
|
</Link>
|
|
657
970
|
</Button>
|
|
658
|
-
<div className="ml-auto flex shrink-0
|
|
659
|
-
<Button variant="outline" size="sm">
|
|
660
|
-
<Upload className="h-4 w-4" />
|
|
661
|
-
</Button>
|
|
971
|
+
<div className="ml-auto inline-flex shrink-0 overflow-hidden rounded-md border bg-background shadow-sm">
|
|
662
972
|
<Button
|
|
663
|
-
variant="
|
|
973
|
+
variant="ghost"
|
|
664
974
|
size="sm"
|
|
975
|
+
className="rounded-none border-0 px-3 hover:bg-muted"
|
|
665
976
|
onClick={() => handleEdit(conta)}
|
|
977
|
+
aria-label={t('common.edit')}
|
|
978
|
+
title={t('common.edit')}
|
|
666
979
|
>
|
|
667
980
|
<Pencil className="h-4 w-4" />
|
|
668
981
|
</Button>
|
|
669
982
|
<Button
|
|
670
|
-
variant="
|
|
983
|
+
variant="ghost"
|
|
671
984
|
size="sm"
|
|
985
|
+
className="rounded-none border-0 border-l px-3 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
672
986
|
onClick={() => setAccountIdToDelete(conta.id)}
|
|
987
|
+
aria-label={deleteDialogTitle}
|
|
988
|
+
title={deleteDialogTitle}
|
|
673
989
|
>
|
|
674
990
|
<Trash2 className="h-4 w-4" />
|
|
675
991
|
</Button>
|
|
@@ -38,8 +38,44 @@
|
|
|
38
38
|
"default": {
|
|
39
39
|
"title": "Default",
|
|
40
40
|
"description": "Overdue titles"
|
|
41
|
+
},
|
|
42
|
+
"pendingApprovals": {
|
|
43
|
+
"title": "Approvals",
|
|
44
|
+
"countLabel": "open",
|
|
45
|
+
"description": "In review: {value}"
|
|
46
|
+
},
|
|
47
|
+
"pendingReconciliation": {
|
|
48
|
+
"title": "Reconciliation",
|
|
49
|
+
"countLabel": "pending",
|
|
50
|
+
"description": "Active accounts: {value}"
|
|
41
51
|
}
|
|
42
52
|
},
|
|
53
|
+
"bankReconciliation": {
|
|
54
|
+
"title": "Bank Reconciliation",
|
|
55
|
+
"description": "Pending statements and differences by account",
|
|
56
|
+
"pending": "Pending",
|
|
57
|
+
"reconciled": "Reconciled",
|
|
58
|
+
"accounts": "Accounts",
|
|
59
|
+
"difference": "Difference to review",
|
|
60
|
+
"allClear": "All active accounts are fully reconciled.",
|
|
61
|
+
"unnamedAccount": "Unnamed account"
|
|
62
|
+
},
|
|
63
|
+
"approvalQueue": {
|
|
64
|
+
"title": "Approval Queue",
|
|
65
|
+
"description": "Most recent financial approvals waiting for action",
|
|
66
|
+
"defaultPolicy": "Financial approval",
|
|
67
|
+
"empty": "No approvals pending right now."
|
|
68
|
+
},
|
|
69
|
+
"aging": {
|
|
70
|
+
"title": "Receivable Aging",
|
|
71
|
+
"description": "Delinquency split by overdue range",
|
|
72
|
+
"totalLabel": "Total overdue",
|
|
73
|
+
"clientsLabel": "Clients impacted",
|
|
74
|
+
"range0to30": "0-30 days",
|
|
75
|
+
"range31to60": "31-60 days",
|
|
76
|
+
"range61to90": "61-90 days",
|
|
77
|
+
"range90plus": "90+ days"
|
|
78
|
+
},
|
|
43
79
|
"cashFlow": {
|
|
44
80
|
"title": "Cash Flow",
|
|
45
81
|
"description": "Predicted vs Actual"
|
|
@@ -837,7 +873,12 @@
|
|
|
837
873
|
"updateSuccess": "Bank account updated successfully",
|
|
838
874
|
"updateError": "Failed to update bank account",
|
|
839
875
|
"deleteSuccess": "Bank account deactivated successfully",
|
|
840
|
-
"deleteError": "Failed to deactivate bank account"
|
|
876
|
+
"deleteError": "Failed to deactivate bank account",
|
|
877
|
+
"logoUploadSuccess": "Logo uploaded successfully",
|
|
878
|
+
"logoUploadError": "Failed to upload logo",
|
|
879
|
+
"logoRemoveSuccess": "Logo removed successfully",
|
|
880
|
+
"logoInvalidType": "Please select a valid image file",
|
|
881
|
+
"logoTooLarge": "The logo must be 5 MB or smaller"
|
|
841
882
|
},
|
|
842
883
|
"deleteDialog": {
|
|
843
884
|
"title": "Deactivate bank account",
|
|
@@ -863,7 +904,12 @@
|
|
|
863
904
|
"type": "Type",
|
|
864
905
|
"description": "Description",
|
|
865
906
|
"descriptionPlaceholder": "Example: Main Account",
|
|
866
|
-
"initialBalance": "Initial Balance"
|
|
907
|
+
"initialBalance": "Initial Balance",
|
|
908
|
+
"logo": "Logo",
|
|
909
|
+
"logoAction": "Upload logo",
|
|
910
|
+
"logoRemove": "Remove logo",
|
|
911
|
+
"logoHint": "Optional. Prefer a square image for the best result.",
|
|
912
|
+
"logoUploading": "Uploading logo... {progress}%"
|
|
867
913
|
},
|
|
868
914
|
"header": {
|
|
869
915
|
"title": "Bank Accounts",
|
|
@@ -38,8 +38,44 @@
|
|
|
38
38
|
"default": {
|
|
39
39
|
"title": "Inadimplência",
|
|
40
40
|
"description": "Títulos vencidos"
|
|
41
|
+
},
|
|
42
|
+
"pendingApprovals": {
|
|
43
|
+
"title": "Aprovações",
|
|
44
|
+
"countLabel": "abertas",
|
|
45
|
+
"description": "Em análise: {value}"
|
|
46
|
+
},
|
|
47
|
+
"pendingReconciliation": {
|
|
48
|
+
"title": "Conciliação",
|
|
49
|
+
"countLabel": "pendentes",
|
|
50
|
+
"description": "Contas ativas: {value}"
|
|
41
51
|
}
|
|
42
52
|
},
|
|
53
|
+
"bankReconciliation": {
|
|
54
|
+
"title": "Conciliação Bancária",
|
|
55
|
+
"description": "Extratos pendentes e diferenças por conta",
|
|
56
|
+
"pending": "Pendentes",
|
|
57
|
+
"reconciled": "Conciliados",
|
|
58
|
+
"accounts": "Contas",
|
|
59
|
+
"difference": "Diferença a revisar",
|
|
60
|
+
"allClear": "Todas as contas ativas estão conciliadas.",
|
|
61
|
+
"unnamedAccount": "Conta sem nome"
|
|
62
|
+
},
|
|
63
|
+
"approvalQueue": {
|
|
64
|
+
"title": "Fila de Aprovações",
|
|
65
|
+
"description": "Aprovações financeiras mais recentes aguardando ação",
|
|
66
|
+
"defaultPolicy": "Aprovação financeira",
|
|
67
|
+
"empty": "Não há aprovações pendentes no momento."
|
|
68
|
+
},
|
|
69
|
+
"aging": {
|
|
70
|
+
"title": "Aging de Recebíveis",
|
|
71
|
+
"description": "Inadimplência segmentada por faixa de atraso",
|
|
72
|
+
"totalLabel": "Total em atraso",
|
|
73
|
+
"clientsLabel": "Clientes impactados",
|
|
74
|
+
"range0to30": "0-30 dias",
|
|
75
|
+
"range31to60": "31-60 dias",
|
|
76
|
+
"range61to90": "61-90 dias",
|
|
77
|
+
"range90plus": "90+ dias"
|
|
78
|
+
},
|
|
43
79
|
"cashFlow": {
|
|
44
80
|
"title": "Fluxo de Caixa",
|
|
45
81
|
"description": "Previsto vs Realizado"
|
|
@@ -837,7 +873,12 @@
|
|
|
837
873
|
"updateSuccess": "Conta bancária atualizada com sucesso",
|
|
838
874
|
"updateError": "Erro ao atualizar conta bancária",
|
|
839
875
|
"deleteSuccess": "Conta bancária inativada com sucesso",
|
|
840
|
-
"deleteError": "Erro ao inativar conta bancária"
|
|
876
|
+
"deleteError": "Erro ao inativar conta bancária",
|
|
877
|
+
"logoUploadSuccess": "Logo enviado com sucesso",
|
|
878
|
+
"logoUploadError": "Erro ao enviar logo",
|
|
879
|
+
"logoRemoveSuccess": "Logo removido com sucesso",
|
|
880
|
+
"logoInvalidType": "Selecione um arquivo de imagem válido",
|
|
881
|
+
"logoTooLarge": "O logo deve ter no máximo 5 MB"
|
|
841
882
|
},
|
|
842
883
|
"deleteDialog": {
|
|
843
884
|
"title": "Inativar conta bancária",
|
|
@@ -863,7 +904,12 @@
|
|
|
863
904
|
"type": "Tipo",
|
|
864
905
|
"description": "Descrição",
|
|
865
906
|
"descriptionPlaceholder": "Ex: Conta Principal",
|
|
866
|
-
"initialBalance": "Saldo Inicial"
|
|
907
|
+
"initialBalance": "Saldo Inicial",
|
|
908
|
+
"logo": "Logo",
|
|
909
|
+
"logoAction": "Enviar logo",
|
|
910
|
+
"logoRemove": "Remover logo",
|
|
911
|
+
"logoHint": "Opcional. Use preferencialmente uma imagem quadrada.",
|
|
912
|
+
"logoUploading": "Enviando logo... {progress}%"
|
|
867
913
|
},
|
|
868
914
|
"header": {
|
|
869
915
|
"title": "Contas Bancárias",
|
|
@@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
5
5
|
import { useWidgetData } from '@/hooks/use-widget-data';
|
|
6
6
|
import { AlertTriangle } from 'lucide-react';
|
|
7
7
|
import { useLocale, useTranslations } from 'next-intl';
|
|
8
|
-
import { WidgetWrapper } from '
|
|
8
|
+
import { WidgetWrapper } from '@/app/(app)/(libraries)/core/dashboard/components';
|
|
9
9
|
|
|
10
10
|
interface FinanceData {
|
|
11
11
|
titulosPagar?: Array<{
|