feat(clients-providers): improve Programs tab UI and fix certified company toggle

This commit is contained in:
Galindo97
2026-02-25 09:00:15 -06:00
parent 2028c87645
commit 97d868e863
3 changed files with 242 additions and 79 deletions

View File

@@ -46,7 +46,7 @@ class ClientProviderProgramsDTO(BaseModel):
program_number: Optional[str] = Field(
None, max_length=40, description="Program number"
)
prosec: Optional[int] = Field(None, description="PROSEC")
prosec: Optional[str] = Field(None, max_length=8, description="PROSEC")
prosec_authorization: Optional[str] = Field(
None, max_length=20, description="PROSEC authorization"
)

View File

@@ -136,7 +136,7 @@ class ClientProviderPrograms(Base, TenantScopedMixin, TimestampMixin):
# Program information
program: Mapped[Optional[str]] = mapped_column(String(7))
program_number: Mapped[Optional[str]] = mapped_column(String(40))
prosec: Mapped[Optional[int]] = mapped_column(SmallInteger)
prosec: Mapped[Optional[str]] = mapped_column(String(8))
prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20))
secon_auth_date: Mapped[Optional[int]] = mapped_column(Integer)
manufacturer_id: Mapped[Optional[str]] = mapped_column(String(25))

View File

@@ -22,10 +22,29 @@
FileText,
Settings,
User,
Trash2
Trash2,
Search,
Globe,
MapPin as MapPinIcon,
Factory,
Calendar,
Hash,
ShieldCheck,
Award,
Fingerprint,
Briefcase
} from 'lucide-svelte';
import { toast } from 'svelte-sonner';
// Componentes Compartidos (Modales)
import CountrySelectorDialog from '$lib/components/dashboard/goods/modales/country-selector-dialog.svelte';
import StateSelectorDialog from '$lib/components/dashboard/shared/modals/state-selector-dialog.svelte';
import SectorSelectorDialog from '$lib/components/dashboard/shared/modals/sector-selector-dialog.svelte';
import { type Country } from '$lib/api/dashboard/reference_data/countries';
import { type State } from '$lib/api/dashboard/reference_data/states';
import { type Sector } from '$lib/api/dashboard/reference_data/sectors';
// API & Stores
import { clientsProvidersApi } from '$lib/api/dashboard/a76/clients-providers';
import { companyStore } from '$lib/stores/company.svelte';
@@ -73,17 +92,30 @@
program: '',
program_number: '',
authorization_date_str: '', // String para input date
prosec: 0,
prosec: '',
manufacturer_id: '',
tax_id: '',
ctpat_svi: '',
is_certified_company: '0'
is_certified_company: false
});
let formData = $state(getEmptyForm());
let loading = $state(false);
let error = $state<string | null>(null);
// --- ESTADO PARA MODALES ---
let countryModalOpen = $state(false);
let stateModalOpen = $state(false);
let sectorModalOpen = $state(false);
const scaiiPrograms = [
{ value: 'IMMEX', label: 'IMMEX' },
{ value: 'PROSEC', label: 'PROSEC' },
{ value: 'ALTEX', label: 'ALTEX' },
{ value: 'ECEX', label: 'ECEX' },
{ value: 'DRAWBACK', label: 'DRAWBACK' }
];
// --- UTILIDADES ---
const intDateToString = (d?: number | null) =>
d
@@ -138,11 +170,11 @@
program: prog.program || '',
program_number: prog.program_number || '',
authorization_date_str: intDateToString(prog.secon_auth_date),
prosec: prog.prosec || 0,
prosec: prog.prosec ? String(prog.prosec) : '',
manufacturer_id: prog.manufacturer_id || '',
tax_id: prog.tax_id || '',
ctpat_svi: prog.ctpat_svi || '',
is_certified_company: prog.is_certified_company || '0'
is_certified_company: prog.is_certified_company === '1'
};
}
} catch (e: any) {
@@ -202,11 +234,11 @@
program: clean(formData.program),
program_number: clean(formData.program_number),
secon_auth_date: stringDateToInt(formData.authorization_date_str),
prosec: Number(formData.prosec) || null,
prosec: clean(formData.prosec),
manufacturer_id: clean(formData.manufacturer_id),
tax_id: clean(formData.tax_id),
ctpat_svi: clean(formData.ctpat_svi),
is_certified_company: clean(formData.is_certified_company)
is_certified_company: formData.is_certified_company ? '1' : '0'
}
};
@@ -470,17 +502,44 @@
</div>
<div class="grid gap-2">
<Label for="state">Estado</Label>
<Input id="state" bind:value={formData.state} maxlength={30} disabled={loading} />
<div class="flex gap-2">
<Input
id="state"
bind:value={formData.state}
maxlength={30}
disabled={loading}
/>
<Button
variant="outline"
size="icon"
onclick={() => (stateModalOpen = true)}
disabled={loading}
title="Buscar Estado"
>
<Search size={16} />
</Button>
</div>
</div>
<div class="grid gap-2">
<Label for="country">País (ISO)</Label>
<Input
id="country"
bind:value={formData.country}
placeholder="MEX"
maxlength={3}
disabled={loading}
/>
<div class="flex gap-2">
<Input
id="country"
bind:value={formData.country}
placeholder="MEX"
maxlength={3}
disabled={loading}
/>
<Button
variant="outline"
size="icon"
onclick={() => (countryModalOpen = true)}
disabled={loading}
title="Buscar País"
>
<Globe size={16} />
</Button>
</div>
</div>
</div>
@@ -509,77 +568,160 @@
>Información sobre IMMEX, PROSEC y otras certificaciones.</Card.Description
>
</Card.Header>
<Card.Content class="space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="program">Programa</Label>
<Input
id="program"
bind:value={formData.program}
placeholder="IMMEX"
maxlength={7}
disabled={loading}
/>
<Card.Content class="space-y-8">
<!-- Section: Promotion Programs -->
<div class="space-y-4">
<div class="flex items-center gap-2 text-sm font-semibold text-white">
<Briefcase size={18} />
<span>Programas de Fomento</span>
</div>
<div class="grid gap-2">
<Label for="program_num">Número de Programa</Label>
<Input
id="program_num"
bind:value={formData.program_number}
maxlength={40}
disabled={loading}
/>
<Separator />
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
<div class="grid gap-2 lg:col-span-1">
<Label for="program" class="flex items-center gap-2">
<Building2 size={14} class="text-muted-foreground" />
Programa
</Label>
<Select.Root
type="single"
value={formData.program}
onValueChange={(v) => (formData.program = v)}
disabled={loading}
>
<Select.Trigger id="program" class="w-full">
{formData.program || 'Selecciona un programa'}
</Select.Trigger>
<Select.Content>
{#each scaiiPrograms as prog}
<Select.Item value={prog.value} label={prog.label}>
{prog.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="grid gap-2">
<Label for="program_num" class="flex items-center gap-2">
<Hash size={14} class="text-muted-foreground" />
Número de Programa
</Label>
<Input
id="program_num"
bind:value={formData.program_number}
maxlength={40}
disabled={loading}
placeholder="P. ej. 1234-2024"
/>
</div>
<div class="grid gap-2">
<Label for="auth_date" class="flex items-center gap-2">
<Calendar size={14} class="text-muted-foreground" />
Fecha Autorización
</Label>
<Input
id="auth_date"
type="date"
bind:value={formData.authorization_date_str}
disabled={loading}
/>
</div>
<div class="grid gap-2">
<Label for="prosec" class="flex items-center gap-2">
<Factory size={14} class="text-muted-foreground" />
PROSEC (Sector)
</Label>
<div class="flex gap-2">
<Input
id="prosec"
bind:value={formData.prosec}
disabled={loading}
placeholder="Ej: XII, IV"
/>
<Button
variant="outline"
size="icon"
onclick={() => (sectorModalOpen = true)}
disabled={loading}
title="Buscar Sector"
>
<Search size={16} />
</Button>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="grid gap-2">
<Label for="auth_date">Fecha Autorización</Label>
<Input
id="auth_date"
type="date"
bind:value={formData.authorization_date_str}
disabled={loading}
/>
<!-- Section: Industrial Identification -->
<div class="space-y-4 pt-4">
<div class="flex items-center gap-2 text-sm font-semibold text-white">
<Fingerprint size={18} />
<span>Identificación Industrial</span>
</div>
<div class="grid gap-2">
<Label for="tax_id">Tax ID (Extranjero)</Label>
<Input
id="tax_id"
bind:value={formData.tax_id}
maxlength={30}
disabled={loading}
/>
</div>
<div class="grid gap-2">
<Label for="man_id">Manufacturer ID</Label>
<Input
id="man_id"
bind:value={formData.manufacturer_id}
maxlength={25}
disabled={loading}
/>
<Separator />
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="grid gap-2">
<Label for="tax_id" class="flex items-center gap-2">
<Globe size={14} class="text-muted-foreground" />
Tax ID (Extranjero)
</Label>
<Input
id="tax_id"
bind:value={formData.tax_id}
maxlength={30}
disabled={loading}
placeholder="Identificador fiscal extranjero"
/>
</div>
<div class="grid gap-2">
<Label for="man_id" class="flex items-center gap-2">
<FileText size={14} class="text-muted-foreground" />
Manufacturer ID (MID)
</Label>
<Input
id="man_id"
bind:value={formData.manufacturer_id}
maxlength={25}
disabled={loading}
placeholder="P. ej. MXABCD12345"
/>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
<Label for="ctpat">C-TPAT / SVI</Label>
<Input
id="ctpat"
bind:value={formData.ctpat_svi}
maxlength={100}
disabled={loading}
/>
<!-- Section: Certifications & Security -->
<div class="space-y-4 pt-4">
<div class="flex items-center gap-2 text-sm font-semibold text-white">
<ShieldCheck size={18} />
<span>Certificaciones y Seguridad</span>
</div>
<div class="grid gap-2">
<Label for="prosec">PROSEC (Sector)</Label>
<Input
id="prosec"
type="number"
bind:value={formData.prosec}
disabled={loading}
/>
<Separator />
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div class="grid gap-2">
<Label for="ctpat" class="flex items-center gap-2">
<Award size={14} class="text-muted-foreground" />
C-TPAT / SVI
</Label>
<Input
id="ctpat"
bind:value={formData.ctpat_svi}
maxlength={100}
disabled={loading}
placeholder="P. ej. SVI-12345"
/>
</div>
<div class="flex items-center gap-3 rounded-lg border bg-card/50 p-4">
<Switch
id="is_certified"
bind:checked={formData.is_certified_company}
disabled={loading}
/>
<div class="grid gap-0.5">
<Label for="is_certified">Empresa Certificada</Label>
<p class="text-xs text-muted-foreground">
Indica si cuenta con certificación de empresa
</p>
</div>
</div>
</div>
</div>
</Card.Content>
@@ -670,3 +812,24 @@
</div>
</div>
</div>
<!-- MODALES DE SELECCIÓN -->
<CountrySelectorDialog
bind:open={countryModalOpen}
onSelect={(country) => (formData.country = country.m3_key)}
/>
<StateSelectorDialog
bind:open={stateModalOpen}
onSelect={(state) => {
formData.state = state.description;
if (state.m3_key && !formData.country) {
formData.country = state.m3_key;
}
}}
/>
<SectorSelectorDialog
bind:open={sectorModalOpen}
onSelect={(sector) => (formData.prosec = sector.key)}
/>