- Implemented the partidas tab form in Svelte for managing partidas. - Added functionality to create, edit, delete, and list partidas. - Integrated a dialog for adding and editing partidas with validation. - Included a checkbox component for selecting partidas. - Added total calculations for partidas in the UI. - Enhanced UI with buttons for various actions and a responsive table layout.
531 lines
14 KiB
Svelte
531 lines
14 KiB
Svelte
<script lang="ts">
|
|
import { Card, CardContent } from '$lib/components/ui/card';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Label } from '$lib/components/ui/label';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '$lib/components/ui/table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter
|
|
} from '$lib/components/ui/dialog';
|
|
import * as Select from '$lib/components/ui/select';
|
|
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
|
import { Textarea } from '$lib/components/ui/textarea';
|
|
import {
|
|
Plus,
|
|
Pencil,
|
|
Trash2,
|
|
FileSpreadsheet,
|
|
FileText,
|
|
Check,
|
|
RotateCw,
|
|
ArrowUpDown
|
|
} from 'lucide-svelte';
|
|
|
|
interface Factura {
|
|
id?: number;
|
|
numero_factura: string;
|
|
fecha: string;
|
|
proveedor: string;
|
|
pais: string;
|
|
moneda: string;
|
|
valor_factura: number;
|
|
valor_usd: number;
|
|
es_consolidado: boolean;
|
|
arribo_consolidado: boolean;
|
|
inicio?: string;
|
|
fin?: string;
|
|
detalle_e_document?: string;
|
|
}
|
|
|
|
let {
|
|
formData = $bindable({
|
|
facturas: [] as Factura[]
|
|
})
|
|
}: {
|
|
formData: {
|
|
facturas: Factura[];
|
|
};
|
|
} = $props();
|
|
|
|
let isDialogOpen = $state(false);
|
|
let editingIndex = $state<number | null>(null);
|
|
let selectedFacturas = $state<Set<number>>(new Set());
|
|
|
|
let currentFactura = $state<Factura>({
|
|
numero_factura: '',
|
|
fecha: '',
|
|
proveedor: '',
|
|
pais: '',
|
|
moneda: 'USD',
|
|
valor_factura: 0,
|
|
valor_usd: 0,
|
|
es_consolidado: false,
|
|
arribo_consolidado: false,
|
|
inicio: '',
|
|
fin: '',
|
|
detalle_e_document: ''
|
|
});
|
|
|
|
const monedas = ['USD', 'MXN', 'EUR', 'CAD', 'JPY', 'GBP', 'CHF'];
|
|
const paises = [
|
|
'Estados Unidos',
|
|
'México',
|
|
'Canadá',
|
|
'China',
|
|
'Alemania',
|
|
'Japón',
|
|
'Reino Unido',
|
|
'Francia',
|
|
'Italia',
|
|
'España',
|
|
'Corea del Sur',
|
|
'India'
|
|
];
|
|
|
|
const totalFacturas = $derived(formData?.facturas?.length ?? 0);
|
|
const sumaPesos = $derived(
|
|
formData?.facturas?.reduce((sum, f) => sum + (f.valor_factura || 0), 0) ?? 0
|
|
);
|
|
const sumaDolares = $derived(formData?.facturas?.reduce((sum, f) => sum + (f.valor_usd || 0), 0) ?? 0);
|
|
|
|
function openNewFactura() {
|
|
editingIndex = null;
|
|
currentFactura = {
|
|
numero_factura: '',
|
|
fecha: '',
|
|
proveedor: '',
|
|
pais: '',
|
|
moneda: 'USD',
|
|
valor_factura: 0,
|
|
valor_usd: 0,
|
|
es_consolidado: false,
|
|
arribo_consolidado: false,
|
|
inicio: '',
|
|
fin: '',
|
|
detalle_e_document: ''
|
|
};
|
|
isDialogOpen = true;
|
|
}
|
|
|
|
function openEditFactura(index: number) {
|
|
if (!formData?.facturas) return;
|
|
editingIndex = index;
|
|
currentFactura = { ...formData.facturas[index] };
|
|
isDialogOpen = true;
|
|
}
|
|
|
|
function saveFactura() {
|
|
if (!formData?.facturas) return;
|
|
if (editingIndex !== null) {
|
|
formData.facturas[editingIndex] = { ...currentFactura };
|
|
} else {
|
|
formData.facturas = [...formData.facturas, { ...currentFactura }];
|
|
}
|
|
isDialogOpen = false;
|
|
}
|
|
|
|
function deleteFactura(index: number) {
|
|
if (!formData?.facturas) return;
|
|
if (confirm('¿Está seguro de eliminar esta factura?')) {
|
|
formData.facturas = formData.facturas.filter((_, i) => i !== index);
|
|
}
|
|
}
|
|
|
|
function clearForm() {
|
|
currentFactura = {
|
|
numero_factura: '',
|
|
fecha: '',
|
|
proveedor: '',
|
|
pais: '',
|
|
moneda: 'USD',
|
|
valor_factura: 0,
|
|
valor_usd: 0,
|
|
es_consolidado: false,
|
|
arribo_consolidado: false,
|
|
inicio: '',
|
|
fin: '',
|
|
detalle_e_document: ''
|
|
};
|
|
}
|
|
|
|
function toggleSelectFactura(index: number) {
|
|
if (selectedFacturas.has(index)) {
|
|
selectedFacturas.delete(index);
|
|
} else {
|
|
selectedFacturas.add(index);
|
|
}
|
|
selectedFacturas = new Set(selectedFacturas);
|
|
}
|
|
|
|
function selectAllWithoutCove() {
|
|
// Esta función seleccionaría facturas sin COVE
|
|
// Por ahora seleccionamos todas
|
|
if (!formData?.facturas) return;
|
|
selectedFacturas = new Set(formData.facturas.map((_, i) => i));
|
|
}
|
|
|
|
function deleteSelectedFacturas() {
|
|
if (!formData?.facturas) return;
|
|
if (selectedFacturas.size === 0) {
|
|
alert('No hay facturas seleccionadas');
|
|
return;
|
|
}
|
|
if (confirm(`¿Está seguro de eliminar ${selectedFacturas.size} factura(s)?`)) {
|
|
formData.facturas = formData.facturas.filter((_, i) => !selectedFacturas.has(i));
|
|
selectedFacturas = new Set();
|
|
}
|
|
}
|
|
|
|
function validateVentanillaUnica() {
|
|
alert('Validación en Ventanilla Única no implementada aún');
|
|
}
|
|
|
|
function importFacturas() {
|
|
alert('Importación de facturas no implementada aún');
|
|
}
|
|
|
|
function overwriteFacturas() {
|
|
alert('Sobre escribir facturas no implementado aún');
|
|
}
|
|
|
|
function reorderFacturas() {
|
|
alert('Reordenar facturas no implementado aún');
|
|
}
|
|
|
|
function datosSonana() {
|
|
alert('Datos Sonana no implementado aún');
|
|
}
|
|
</script>
|
|
|
|
<Card>
|
|
<CardContent class="p-6 space-y-4">
|
|
<!-- Barra de herramientas superior -->
|
|
<div class="flex flex-wrap gap-2 border-b pb-4">
|
|
<Button size="sm" variant="outline" onclick={clearForm}>
|
|
<RotateCw class="mr-1.5" size={14} />
|
|
Limpiar
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={selectAllWithoutCove}>
|
|
<Check class="mr-1.5" size={14} />
|
|
Selec. sin cove
|
|
</Button>
|
|
<Button size="sm" onclick={openNewFactura}>
|
|
<Plus class="mr-1.5" size={14} />
|
|
Nuevo
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={selectedFacturas.size !== 1}
|
|
onclick={() => {
|
|
const index = Array.from(selectedFacturas)[0];
|
|
if (index !== undefined) openEditFactura(index);
|
|
}}
|
|
>
|
|
<Pencil class="mr-1.5" size={14} />
|
|
Editar
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
disabled={selectedFacturas.size !== 1}
|
|
onclick={() => {
|
|
const index = Array.from(selectedFacturas)[0];
|
|
if (index !== undefined) deleteFactura(index);
|
|
}}
|
|
>
|
|
<Trash2 class="mr-1.5" size={14} />
|
|
Borrar
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
disabled={selectedFacturas.size === 0}
|
|
onclick={deleteSelectedFacturas}
|
|
>
|
|
<Trash2 class="mr-1.5" size={14} />
|
|
Borrar Facturas Selec.
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={validateVentanillaUnica}>
|
|
<Check class="mr-1.5" size={14} />
|
|
Validar en Ventanilla Única
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={importFacturas}>
|
|
<FileSpreadsheet class="mr-1.5" size={14} />
|
|
Imp Facturas
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={overwriteFacturas}>
|
|
<FileText class="mr-1.5" size={14} />
|
|
Sobre escribir
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={reorderFacturas}>
|
|
<ArrowUpDown class="mr-1.5" size={14} />
|
|
Reordenar
|
|
</Button>
|
|
<Button size="sm" variant="outline" onclick={datosSonana}>
|
|
<FileText class="mr-1.5" size={14} />
|
|
Datos Sonana
|
|
</Button>
|
|
</div>
|
|
|
|
<!-- Tabla de facturas -->
|
|
<div class="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead class="w-[50px]">Sel.</TableHead>
|
|
<TableHead>Núm. Factura</TableHead>
|
|
<TableHead>Fecha</TableHead>
|
|
<TableHead>Proveedor</TableHead>
|
|
<TableHead>País</TableHead>
|
|
<TableHead>Moneda</TableHead>
|
|
<TableHead class="text-right">Valor Factura</TableHead>
|
|
<TableHead class="text-right">Valor USD</TableHead>
|
|
<TableHead class="text-center">Consolidado</TableHead>
|
|
<TableHead class="text-center">Arribo Consol.</TableHead>
|
|
<TableHead class="w-[100px]">Acciones</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{#if !formData?.facturas || formData.facturas.length === 0}
|
|
<TableRow>
|
|
<TableCell colspan={11} class="text-center text-muted-foreground py-8">
|
|
No hay facturas registradas. Haga clic en "Nuevo" para agregar una.
|
|
</TableCell>
|
|
</TableRow>
|
|
{:else}
|
|
{#each formData.facturas as factura, index}
|
|
<TableRow>
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedFacturas.has(index)}
|
|
onCheckedChange={() => toggleSelectFactura(index)}
|
|
/>
|
|
</TableCell>
|
|
<TableCell class="font-medium">{factura.numero_factura}</TableCell>
|
|
<TableCell>{factura.fecha}</TableCell>
|
|
<TableCell>{factura.proveedor}</TableCell>
|
|
<TableCell>{factura.pais}</TableCell>
|
|
<TableCell>{factura.moneda}</TableCell>
|
|
<TableCell class="text-right"
|
|
>{factura.valor_factura.toLocaleString('es-MX', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})}</TableCell
|
|
>
|
|
<TableCell class="text-right"
|
|
>{factura.valor_usd.toLocaleString('es-MX', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})}</TableCell
|
|
>
|
|
<TableCell class="text-center">
|
|
{#if factura.es_consolidado}
|
|
<Check class="inline" size={16} />
|
|
{/if}
|
|
</TableCell>
|
|
<TableCell class="text-center">
|
|
{#if factura.arribo_consolidado}
|
|
<Check class="inline" size={16} />
|
|
{/if}
|
|
</TableCell>
|
|
<TableCell>
|
|
<div class="flex gap-1">
|
|
<Button size="sm" variant="ghost" onclick={() => openEditFactura(index)}>
|
|
<Pencil size={14} />
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onclick={() => deleteFactura(index)}>
|
|
<Trash2 size={14} />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
{/each}
|
|
{/if}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<!-- Totales -->
|
|
<div class="flex gap-6 text-sm font-medium border-t pt-4">
|
|
<div>Facturas: <span class="text-primary">{totalFacturas}</span></div>
|
|
<div>
|
|
Suma Pesos: <span class="text-primary"
|
|
>{sumaPesos.toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
MXN</span
|
|
>
|
|
</div>
|
|
<div>
|
|
Suma USD: <span class="text-primary"
|
|
>{sumaDolares.toLocaleString('es-MX', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})} USD</span
|
|
>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<!-- Dialog para agregar/editar factura -->
|
|
<Dialog bind:open={isDialogOpen}>
|
|
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{editingIndex !== null ? 'Editar Factura' : 'Nueva Factura'}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div class="grid gap-4 py-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="numero_factura">Núm. de Factura *</Label>
|
|
<Input
|
|
id="numero_factura"
|
|
bind:value={currentFactura.numero_factura}
|
|
placeholder="Ej: INV-2024-001"
|
|
/>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="fecha">Fecha *</Label>
|
|
<Input id="fecha" type="date" bind:value={currentFactura.fecha} />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="proveedor">Proveedor *</Label>
|
|
<Input
|
|
id="proveedor"
|
|
bind:value={currentFactura.proveedor}
|
|
placeholder="Nombre del proveedor"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="pais">País *</Label>
|
|
<Select.Root
|
|
type="single"
|
|
value={currentFactura.pais}
|
|
onValueChange={(v: string | undefined) => v && (currentFactura.pais = v)}
|
|
>
|
|
<Select.Trigger id="pais">
|
|
{currentFactura.pais || 'Seleccionar país'}
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
{#each paises as pais}
|
|
<Select.Item value={pais}>{pais}</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="moneda">Moneda *</Label>
|
|
<Select.Root
|
|
type="single"
|
|
value={currentFactura.moneda}
|
|
onValueChange={(v: string | undefined) => v && (currentFactura.moneda = v)}
|
|
>
|
|
<Select.Trigger id="moneda">
|
|
{currentFactura.moneda || 'Seleccionar moneda'}
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
{#each monedas as moneda}
|
|
<Select.Item value={moneda}>{moneda}</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="valor_factura">Valor Factura *</Label>
|
|
<Input
|
|
id="valor_factura"
|
|
type="number"
|
|
step="0.01"
|
|
bind:value={currentFactura.valor_factura}
|
|
placeholder="0.00"
|
|
/>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="valor_usd">Valor USD *</Label>
|
|
<Input
|
|
id="valor_usd"
|
|
type="number"
|
|
step="0.01"
|
|
bind:value={currentFactura.valor_usd}
|
|
placeholder="0.00"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="es_consolidado"
|
|
checked={currentFactura.es_consolidado}
|
|
onCheckedChange={(checked: boolean | 'indeterminate') => (currentFactura.es_consolidado = checked === true)}
|
|
/>
|
|
<Label for="es_consolidado" class="font-normal cursor-pointer">Es Consolidado</Label>
|
|
</div>
|
|
<div class="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="arribo_consolidado"
|
|
checked={currentFactura.arribo_consolidado}
|
|
onCheckedChange={(checked: boolean | 'indeterminate') => (currentFactura.arribo_consolidado = checked === true)}
|
|
/>
|
|
<Label for="arribo_consolidado" class="font-normal cursor-pointer"
|
|
>Arribo Consolidado</Label
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div class="space-y-2">
|
|
<Label for="inicio">Inicio</Label>
|
|
<Input id="inicio" type="date" bind:value={currentFactura.inicio} />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="fin">Fin</Label>
|
|
<Input id="fin" type="date" bind:value={currentFactura.fin} />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<Label for="detalle_e_document">Detalle de E_Document</Label>
|
|
<Textarea
|
|
id="detalle_e_document"
|
|
bind:value={currentFactura.detalle_e_document}
|
|
placeholder="Detalles adicionales del documento electrónico"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onclick={() => (isDialogOpen = false)}>Cancelar</Button>
|
|
<Button
|
|
onclick={saveFactura}
|
|
disabled={!currentFactura.numero_factura ||
|
|
!currentFactura.fecha ||
|
|
!currentFactura.proveedor ||
|
|
!currentFactura.pais ||
|
|
!currentFactura.moneda}
|
|
>
|
|
{editingIndex !== null ? 'Actualizar' : 'Guardar'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|