Merge pull request 'feature/alerta-no-registros-agentes-clientes' (#179) from fix/svelte-a11y-warnings into development

Reviewed-on: ADUANASOFT/anexo76#179
This commit is contained in:
2026-03-03 20:02:10 +00:00
11 changed files with 221 additions and 18 deletions

BIN
backend/celerybeat-schedule Normal file

Binary file not shown.

View File

@@ -265,12 +265,7 @@ services:
options:
max-size: "10m"
max-file: "3"
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
# celery
celery_worker:

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { CircleAlert } from 'lucide-svelte';
let {
open = $bindable(false),
agentsCount = 0,
clientsCount = 0,
onAccept,
onCancel
}: {
open?: boolean;
agentsCount?: number;
clientsCount?: number;
onAccept?: () => void;
onCancel?: () => void;
} = $props();
const showModal = $derived(agentsCount === 0 || clientsCount === 0);
const message = $derived(
agentsCount === 0 && clientsCount === 0
? 'No hay Agentes aduanales ni Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.'
: agentsCount === 0
? 'No hay Agentes aduanales registrados. Debes darlos de alta para poder trabajar en este módulo.'
: 'No hay Clientes registrados. Debes darlos de alta para poder trabajar en este módulo.'
);
function handleOpenChange(newOpen: boolean) {
open = newOpen;
}
function handleCancel() {
onCancel?.();
}
function handleAccept() {
onAccept?.();
}
</script>
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
<AlertDialog.Content>
<AlertDialog.Header>
<div class="flex items-center gap-3">
<CircleAlert class="h-6 w-6 shrink-0 text-amber-500" />
<AlertDialog.Title>Aviso</AlertDialog.Title>
</div>
<AlertDialog.Description class="space-y-3 pt-1">
<p>{message}</p>
<p class="text-sm text-muted-foreground">
Puedes registrarlos en
<a
href="/dashboard/customs_brokers"
class="font-medium text-primary underline underline-offset-4 hover:no-underline"
>
Agentes Aduanales
</a>
y
<a
href="/dashboard/clients_and_providers"
class="font-medium text-primary underline underline-offset-4 hover:no-underline"
>
Clientes y Proveedores
</a>.
</p>
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel onclick={handleCancel}>Cancelar</AlertDialog.Cancel>
<AlertDialog.Action onclick={handleAccept}>Aceptar</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>

View File

@@ -121,7 +121,7 @@
</script>
<Sheet.Root bind:open={helpStore.isOpen}>
<Sheet.Trigger>
<Sheet.Trigger asChild>
<button
class="fixed right-6 bottom-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-transform hover:scale-110 active:scale-95"
aria-label="Ayuda"

View File

@@ -7,11 +7,11 @@
// Group local shortcuts
let localShortcuts = $derived($store.shortcuts);
let globalList: HTMLDivElement;
let localList: HTMLDivElement;
let modalRef: HTMLDivElement;
let globalList = $state<HTMLDivElement | undefined>();
let localList = $state<HTMLDivElement | undefined>();
let modalRef = $state<HTMLDivElement | undefined>();
function handleArrowScroll(event: KeyboardEvent, target: HTMLDivElement) {
function handleArrowScroll(event: KeyboardEvent, target: HTMLDivElement | undefined) {
if (!target) return;
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
@@ -51,6 +51,7 @@
>
<div
class="w-full max-w-2xl rounded-xl bg-white p-6 shadow-2xl dark:bg-gray-900 text-gray-900 dark:text-gray-100 max-h-[80vh] overflow-y-auto"
role="document"
bind:this={modalRef}
onkeydown={handleFocusTrap}
>
@@ -88,6 +89,8 @@
</h3>
<div
class="space-y-2 max-h-64 overflow-y-auto pr-1"
role="region"
aria-label="Global Navigation shortcuts"
bind:this={globalList}
tabindex="0"
onkeydown={(event) => handleArrowScroll(event, globalList)}
@@ -124,6 +127,8 @@
{:else}
<div
class="space-y-2 max-h-64 overflow-y-auto pr-1"
role="region"
aria-label="Active Actions shortcuts"
bind:this={localList}
tabindex="0"
onkeydown={(event) => handleArrowScroll(event, localList)}

View File

@@ -101,7 +101,7 @@
{#if item.icon}
<item.icon />
{:else}
<div class="size-4" />
<div class="size-4"></div>
{/if}
<!-- Ocultamos el texto en modo colapsado para asegurar que solo sea el icono -->
<span class="sr-only">{item.title}</span>
@@ -127,7 +127,7 @@
{#if item.icon}
<item.icon class="size-4 shrink-0" />
{:else}
<div class="size-4 shrink-0" />
<div class="size-4 shrink-0"></div>
{/if}
</div>

View File

@@ -85,7 +85,6 @@
bind:this={searchInputRef}
value={searchQuery}
placeholder={searchPlaceholder}
autofocus={autoFocusSearch}
class="placeholder:text-muted-foreground flex h-8 w-full rounded-md border border-input bg-background px-3 py-1 text-sm outline-none focus:border-ring focus:ring-1 focus:ring-ring"
oninput={(event) => updateQuery((event.currentTarget as HTMLInputElement).value)}
onclick={(e) => e.stopPropagation()}

View File

@@ -0,0 +1,55 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
throw redirect(302, '/login');
}
const companyId = await getActiveCompanyId(cookies, fetch);
if (!companyId) {
throw error(400, 'No se encontró una compañía seleccionada');
}
let agentsCount = 0;
let clientsCount = 0;
try {
const [brokersRes, clientsRes] = await Promise.all([
authenticatedFetch(
`v1/a76/customs-brokers?company_id=${companyId}&page=1&page_size=1`,
{},
cookies,
fetch
),
authenticatedFetch(
`v1/a76/clients-providers?company_id=${companyId}&page=1&page_size=1`,
{},
cookies,
fetch
)
]);
if (brokersRes.ok) {
const brokersData = await brokersRes.json();
agentsCount = brokersData.total ?? 0;
}
if (clientsRes.ok) {
const clientsData = await clientsRes.json();
clientsCount = clientsData.total ?? 0;
}
} catch (e) {
console.error('Error fetching prerequisites count for parts:', e);
}
const isCreate = params.id === 'new' || params.id === undefined;
return {
agentsCount,
clientsCount,
isCreate
};
};

View File

@@ -1,9 +1,36 @@
<script lang="ts">
import { page } from '$app/stores';
import PartForm from '$lib/components/dashboard/goods/parts/partForm.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import PartForm from '$lib/components/dashboard/goods/parts/partForm.svelte';
import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte';
let id = $derived($page.params.id === 'new' ? null : Number($page.params.id));
let type: 'inv' | 'fa' = 'fa';
let id = $derived($page.params.id === 'new' ? null : Number($page.params.id));
let type: 'inv' | 'fa' = 'fa';
let { data }: { data: { agentsCount?: number; clientsCount?: number; isCreate?: boolean } } = $props();
let showPrerequisitesModal = $state(false);
let hasTriedToShowPrerequisitesModal = $state(false);
$effect(() => {
if (hasTriedToShowPrerequisitesModal) return;
if (!data?.isCreate) return;
const agentsCount = data.agentsCount ?? 0;
const clientsCount = data.clientsCount ?? 0;
if (agentsCount === 0 || clientsCount === 0) {
hasTriedToShowPrerequisitesModal = true;
showPrerequisitesModal = true;
}
});
</script>
{#if data?.isCreate}
<PrerequisitesModal
bind:open={showPrerequisitesModal}
agentsCount={data?.agentsCount ?? 0}
clientsCount={data?.clientsCount ?? 0}
onAccept={() => (showPrerequisitesModal = false)}
onCancel={() => goto('/dashboard/goods/parts')}
/>
{/if}
<PartForm partId={id} formType={type} />

View File

@@ -39,6 +39,7 @@
import { useShortcuts } from '$lib/hooks/use-shortcuts';
import { obtenerAtajosEdicionFactura } from '$lib/config/shortcuts/dashboard/invoices/edit';
import { api } from '$lib/api';
import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte';
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
@@ -793,6 +794,19 @@
manejarRegresar: handleBack
})
);
// Prerrequisitos: modal cuando no hay agentes o clientes
const agentsCount = $derived(data.customsBrokers?.length ?? 0);
const clientsCount = $derived(data.clients?.length ?? 0);
let showPrerequisitesModal = $state(false);
let hasTriedToShowPrerequisitesModal = $state(false);
$effect(() => {
if (hasTriedToShowPrerequisitesModal) return;
if (agentsCount === 0 || clientsCount === 0) {
hasTriedToShowPrerequisitesModal = true;
showPrerequisitesModal = true;
}
});
</script>
<div class="space-y-3">
@@ -1042,3 +1056,11 @@
</div>
</div>
</div>
<PrerequisitesModal
bind:open={showPrerequisitesModal}
{agentsCount}
{clientsCount}
onAccept={() => (showPrerequisitesModal = false)}
onCancel={() => goto('/dashboard/invoices')}
/>

View File

@@ -79,6 +79,7 @@
import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate';
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import { companyStore } from '$lib/stores/company.svelte';
import PrerequisitesModal from '$lib/components/dashboard/PrerequisitesModal.svelte';
let { data }: { data: ExtendedPageData } = $props();
@@ -118,6 +119,20 @@
let showExchangeRateDialog = $state(false);
let missingExchangeRateDate = $state('');
// Prerrequisitos: modal solo en creación cuando no hay agentes o clientes
const agentsCount = $derived(data.customsBrokers?.length ?? 0);
const clientsCount = $derived(data.clients?.length ?? 0);
let showPrerequisitesModal = $state(false);
let hasTriedToShowPrerequisitesModal = $state(false);
$effect(() => {
if (hasTriedToShowPrerequisitesModal) return;
if (!data.isCreate) return;
if (agentsCount === 0 || clientsCount === 0) {
hasTriedToShowPrerequisitesModal = true;
showPrerequisitesModal = true;
}
});
async function checkPaymentDateRate(date: string): Promise<boolean> {
if (!date || !companyStore.activeCompany?.id) return true;
@@ -1249,4 +1264,15 @@
</div>
</div>
</Tabs.Root>
<!-- Modal de prerrequisitos: agentes aduanales y clientes -->
{#if data.isCreate}
<PrerequisitesModal
bind:open={showPrerequisitesModal}
{agentsCount}
{clientsCount}
onAccept={() => (showPrerequisitesModal = false)}
onCancel={() => goto('/dashboard/pedimentos')}
/>
{/if}
</div>