Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into fix/clientes-provedores-traduccion

This commit is contained in:
2026-05-12 16:53:15 -05:00
33 changed files with 1500 additions and 882 deletions

View File

@@ -886,13 +886,21 @@ export const api = {
// CSV import for Clientes y Proveedores (flujo propio en clients_and_providers/imports)
clientProviderImports: {
upload: (file: File, companyId: number, uploadOptions?: CsvFormDataUploadOptions) => {
upload: (
file: File,
companyId: number,
options?: { actualizar?: boolean; onUploadProgress?: CsvFormDataUploadOptions['onUploadProgress'] }
) => {
const formData = new FormData();
formData.append('file', file);
const params = new URLSearchParams({ company_id: String(companyId) });
if (options?.actualizar !== undefined) params.set('actualizar', String(options.actualizar));
const uploadOpts =
options?.onUploadProgress != null ? { onUploadProgress: options.onUploadProgress } : undefined;
return fetchApiFormDataPost(
`/v1/a76/clients-providers/imports/upload?company_id=${companyId}`,
`/v1/a76/clients-providers/imports/upload?${params.toString()}`,
formData,
uploadOptions
uploadOpts
);
},
status: (jobId: string) => api.get(`/v1/a76/clients-providers/imports/${jobId}/status`),

View File

@@ -67,7 +67,19 @@ export interface CreateClientProviderData extends Omit<ClientProvider, 'id' | 'a
export interface UpdateClientProviderData extends Partial<CreateClientProviderData> {}
export const clientsProvidersApi = {
list: (companyId: number, page = 1, pageSize = 50, filters?: Record<string, any>) => {
list: (
companyId: number,
page = 1,
pageSize = 50,
filters?: Partial<{
name: string;
rfc: string;
/** Clave corta; el backend filtra por coincidencia exacta sin distinguir mayúsculas/minúsculas */
short_name: string;
type: string;
active: string | boolean;
}>
) => {
const params = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),

View File

@@ -117,13 +117,13 @@
}
async function handleDownloadScanErrorsCsv() {
const errors = scanResults?.errors;
if (!Array.isArray(errors) || errors.length === 0) return;
const jobId = scanResults?.job_id;
const errorCount = Number(scanResults?.error_count) || 0;
const errors = scanResults?.errors;
// Try backend download (sin límite). If it fails, fallback to preview CSV.
if (jobId) {
// Descarga completa desde el backend (JSONL → CSV) siempre que el scan reportó errores y hay job_id.
// No depende del preview acotado en la respuesta JSON.
if (jobId && errorCount > 0) {
try {
let blob: Blob;
switch (scanErrorsDownloadType) {
@@ -178,17 +178,28 @@
document.body.removeChild(a);
URL.revokeObjectURL(url);
return;
} catch (e) {
// fallback below
} catch {
// Si falla la API, intentar CSV desde preview si existe.
}
}
if (!Array.isArray(errors) || errors.length === 0) return;
const filename = `errores_${Date.now()}.csv`;
const rows = errors.map((e: Record<string, unknown>) => ({
line: e.line,
col: e.col,
msg: e.msg,
solution: e.solution,
advertencia: e.warning ? 'si' : 'no',
codigo: e.code ?? '',
not_found_reason: e.not_found_reason ?? ''
}));
downloadCsv(
filename,
['linea', 'columna', 'mensaje', 'solucion'],
errors,
['line', 'col', 'msg', 'solution']
['linea', 'columna', 'mensaje', 'solucion', 'advertencia', 'codigo', 'not_found_reason'],
rows,
['line', 'col', 'msg', 'solution', 'advertencia', 'codigo', 'not_found_reason']
);
}
@@ -334,18 +345,19 @@
</p>
</div>
</div>
{#if scanResults.errors && scanResults.errors.length > 0}
<div class="border rounded-lg overflow-hidden shadow-sm">
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
{csvMsg('modal.errors_heading')}
</h5>
<div class="flex items-center gap-2">
<span
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
>
{csvFmt('modal.errors_badge', { shown: scanErrorsShown, total: scanErrorsTotal })}
</span>
{#if scanResults.errors && scanResults.errors.length > 0}
<span
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
>
{csvFmt('modal.errors_badge', { shown: scanErrorsShown, total: scanErrorsTotal })}
</span>
{/if}
<Button
variant="ghost"
onclick={handleDownloadScanErrorsCsv}
@@ -356,41 +368,42 @@
</Button>
</div>
</div>
<div class="max-h-60 overflow-y-auto bg-card relative">
<table class="w-full text-xs text-left">
<thead
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
>
<tr>
<th class="px-4 py-2 w-16">{csvMsg('modal.th_line')}</th>
<th class="px-4 py-2 w-40">{csvMsg('modal.th_column')}</th>
<th class="px-4 py-2">{csvMsg('modal.th_message')}</th>
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
</tr>
</thead>
<tbody class="divide-y">
{#each scanResults.errors as err}
<tr class="hover:bg-muted/30 transition-colors {err.warning ? 'opacity-70' : ''}">
<td class="px-4 py-2 font-mono text-muted-foreground">{err.line}</td>
<td class="px-4 py-2 font-mono font-medium text-foreground">{err.col || '-'}</td>
<td class="px-4 py-2 {err.warning ? 'text-amber-600 dark:text-amber-400' : 'text-destructive'}">{err.msg || '-'}</td>
<td class="px-4 py-2 text-muted-foreground whitespace-pre-wrap">{err.solution || '-'}</td>
{#if scanResults.errors && scanResults.errors.length > 0}
<div class="max-h-60 overflow-y-auto bg-card relative">
<table class="w-full text-xs text-left">
<thead
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
>
<tr>
<th class="px-4 py-2 w-16">{csvMsg('modal.th_line')}</th>
<th class="px-4 py-2 w-40">{csvMsg('modal.th_column')}</th>
<th class="px-4 py-2">{csvMsg('modal.th_message')}</th>
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
</tr>
{/each}
</tbody>
</table>
</div>
{#if scanErrorsTruncated}
<p class="text-xs text-muted-foreground px-4 py-2">
{csvMsg('modal.errors_truncated')}
</thead>
<tbody class="divide-y">
{#each scanResults.errors as err}
<tr class="hover:bg-muted/30 transition-colors {err.warning ? 'opacity-70' : ''}">
<td class="px-4 py-2 font-mono text-muted-foreground">{err.line}</td>
<td class="px-4 py-2 font-mono font-medium text-foreground">{err.col || '-'}</td>
<td class="px-4 py-2 {err.warning ? 'text-amber-600 dark:text-amber-400' : 'text-destructive'}">{err.msg || '-'}</td>
<td class="px-4 py-2 text-muted-foreground whitespace-pre-wrap">{err.solution || '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#if scanErrorsTruncated}
<p class="text-xs text-muted-foreground px-4 py-2">
{csvMsg('modal.errors_truncated')}
</p>
{/if}
{:else}
<p class="text-sm text-muted-foreground px-4 py-3">
{csvFmt('modal.errors_missing_detail', { count: scanResults.error_count })}
</p>
{/if}
</div>
{:else}
<p class="text-sm text-muted-foreground mt-1">
{csvFmt('modal.errors_missing_detail', { count: scanResults.error_count })}
</p>
{/if}
{:else}
<div class="rounded-md bg-primary/5 border border-primary/10 p-4 flex items-start gap-3">
<CheckCircle2 class="w-5 h-5 text-primary mt-0.5 shrink-0" />

View File

@@ -649,7 +649,10 @@
if (useClientProviderImport) {
try {
const catalogosSettings = allSettings['catalogos'] || {};
const actualizar = catalogosSettings['mode'] === 'update';
const res = await api.clientProviderImports.upload(file, companyId, {
actualizar,
onUploadProgress: onCsvFileUploadProgress
});
if (res.data?.job_id) {