Merge pull request 'fixed/logo_empresa' (#70) from fixed/logo_empresa into development
Reviewed-on: ADUANASOFT/anexo76#70
This commit is contained in:
@@ -295,7 +295,7 @@ export interface UnitOfMeasureListResponse {
|
||||
|
||||
export async function getUnitsOfMeasure(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
pageSize: number = 1000,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureListResponse>> {
|
||||
|
||||
@@ -16,15 +16,27 @@
|
||||
} = $props();
|
||||
|
||||
let items = $state<UnitOfMeasure[]>([]);
|
||||
let allItems = $state<UnitOfMeasure[]>([]); // Store full dataset
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state("");
|
||||
let page = $state(1);
|
||||
let totalPages = $state(1);
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// Derived state for filtering
|
||||
$effect(() => {
|
||||
if (!searchTerm) {
|
||||
items = allItems;
|
||||
} else {
|
||||
const lowerTerm = searchTerm.toLowerCase();
|
||||
items = allItems.filter(item =>
|
||||
item.code.toLowerCase().includes(lowerTerm) ||
|
||||
(item.description && item.description.toLowerCase().includes(lowerTerm)) ||
|
||||
(item.description_en && item.description_en.toLowerCase().includes(lowerTerm))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open && companyStore.activeCompany) {
|
||||
if (open && companyStore.activeCompany && allItems.length === 0) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
@@ -34,17 +46,12 @@
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const filters = searchTerm ? { code: searchTerm } : {};
|
||||
// Nota: Si tu backend soporta búsqueda por descripción, úsalo aquí.
|
||||
// Por ahora asumo búsqueda por 'code' o 'description' según tu filtro backend.
|
||||
|
||||
const response = await getUnitsOfMeasure(page, 10, companyStore.activeCompany.id, {
|
||||
q: searchTerm // Asumiendo que tu backend tiene un filtro genérico 'q' o usa 'code'/'description'
|
||||
});
|
||||
// Fetch everything once using the high limit
|
||||
const response = await getUnitsOfMeasure(1, 1000, companyStore.activeCompany.id, {});
|
||||
|
||||
if (response.data) {
|
||||
items = response.data.items;
|
||||
totalPages = response.data.pages;
|
||||
allItems = response.data.items;
|
||||
items = allItems; // Initialize view
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cargando unidades:", error);
|
||||
@@ -56,32 +63,13 @@
|
||||
function handleSearch(e: Event) {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
searchTerm = value;
|
||||
page = 1;
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
loadData();
|
||||
}, 500);
|
||||
// No network call needed, $effect handles filtering
|
||||
}
|
||||
|
||||
function handleSelect(item: UnitOfMeasure) {
|
||||
onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page < totalPages) {
|
||||
page++;
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
@@ -104,7 +92,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border h-[300px] overflow-auto relative">
|
||||
<div class="rounded-md border h-[400px] overflow-auto relative">
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-primary" />
|
||||
@@ -146,27 +134,9 @@
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-muted-foreground">Página {page} de {totalPages}</span>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page === 1 || loading}
|
||||
onclick={prevPage}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages || loading}
|
||||
onclick={nextPage}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted-foreground text-center">
|
||||
Mostrando {items.length} registros
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Derivar la URL del logo
|
||||
// Derivar la URL del logo usando el endpoint específico
|
||||
let activeCompanyLogoUrl = $derived(
|
||||
companyStore.activeCompany?.logo
|
||||
? getBackendAssetUrl(companyStore.activeCompany.logo)
|
||||
? getBackendAssetUrl(`v1/a76/company/${companyStore.activeCompany.id}/logo/image?t=${new Date().getTime()}`)
|
||||
: null
|
||||
);
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="flex size-6 items-center justify-center rounded-md border overflow-hidden">
|
||||
{#if company.logo}
|
||||
<img
|
||||
src={getBackendAssetUrl(company.logo)}
|
||||
src={getBackendAssetUrl(`v1/a76/company/${company.id}/logo/image`)}
|
||||
alt={company.name}
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
|
||||
@@ -144,6 +144,30 @@ class CompanyStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza los datos de une empresa en el store localmente
|
||||
* Útil para reflejar cambios inmediatos (ej: cambio de logo) sin recargar
|
||||
*/
|
||||
updateCompany(id: number, data: Partial<Company>) {
|
||||
// 1. Actualizar en la lista
|
||||
const index = this._companies.findIndex(c => c.id === id);
|
||||
if (index !== -1) {
|
||||
this._companies[index] = { ...this._companies[index], ...data };
|
||||
|
||||
// 2. Si es la activa, actualizar también
|
||||
if (this._activeCompany?.id === id) {
|
||||
this._activeCompany = { ...this._activeCompany, ...data };
|
||||
// Actualizar persistencia si es necesario
|
||||
if (typeof window !== 'undefined') {
|
||||
// Disparar evento para notificar cambios a componentes que no usan el store reactivo directo (si los hay)
|
||||
window.dispatchEvent(new CustomEvent('companyChanged', {
|
||||
detail: { companyId: id }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura la compañía activa desde localStorage
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
uploadCompanyLogo,
|
||||
type Company
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { getBackendAssetUrl } from '$lib/utils';
|
||||
import { ArrowLeft, LoaderCircle, Save, Upload, X, Building2, FileText, User, Settings } from 'lucide-svelte';
|
||||
|
||||
@@ -25,15 +26,18 @@
|
||||
let loading = $state(false);
|
||||
let uploading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let activeTab = $state('general');
|
||||
|
||||
let logoFile = $state<File | null>(null);
|
||||
let logoPreview = $state<string | null>(null);
|
||||
let currentLogo = $state<string | null>(null);
|
||||
let uploadingLogo = $state(false);
|
||||
let activeTab = $state('general');
|
||||
|
||||
|
||||
// URL completa del logo derivada
|
||||
let currentLogoUrl = $derived(
|
||||
logoPreview || getBackendAssetUrl(currentLogo) || ''
|
||||
logoPreview
|
||||
? logoPreview
|
||||
: (currentLogo ? getBackendAssetUrl(`v1/a76/company/${id}/logo/image?t=${new Date().getTime()}`) : '')
|
||||
);
|
||||
|
||||
// 2. Estado Inicial (Reset)
|
||||
@@ -104,7 +108,15 @@
|
||||
is_service_company: item.is_service_company || false,
|
||||
order_format_type: item.order_format_type || '',
|
||||
ctpat_svi: item.ctpat_svi || '',
|
||||
trusted_exporter_number: item.trusted_exporter_number || ''
|
||||
trusted_exporter_number: item.trusted_exporter_number || '',
|
||||
logo: item.logo || '',
|
||||
previous_code: item.previous_code || 0,
|
||||
client_name: item.client_name || '',
|
||||
subassembly_mode: item.subassembly_mode || '',
|
||||
broker_company: item.broker_company || '',
|
||||
inter_db_name: item.inter_db_name || '',
|
||||
prevalidator_key: item.prevalidator_key || '',
|
||||
seventh_amendment: item.seventh_amendment || false
|
||||
};
|
||||
// Guardar la URL del logo actual si existe
|
||||
if (item.logo) {
|
||||
@@ -118,6 +130,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
function handleLogoChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
@@ -170,6 +185,9 @@
|
||||
currentLogo = response.data.logo_path;
|
||||
logoFile = null;
|
||||
logoPreview = null;
|
||||
|
||||
// Actualizar el store reactivamente
|
||||
companyStore.updateCompany(companyId, { logo: response.data.logo_path });
|
||||
}
|
||||
} catch (e: any) {
|
||||
error = `Error al subir el logo: ${e.message}`;
|
||||
@@ -178,33 +196,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
async function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const file = input.files[0];
|
||||
if (!isEdit) {
|
||||
alert("Primero debes guardar la empresa antes de subir un logo.");
|
||||
return;
|
||||
}
|
||||
|
||||
uploading = true;
|
||||
try {
|
||||
const res = await uploadCompanyLogo(Number(id), file);
|
||||
if (res.data) {
|
||||
formData.logo = res.data.path;
|
||||
} else if (res.error) {
|
||||
alert("Error al subir imagen: " + res.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Error al intentar subir la imagen");
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
@@ -222,6 +213,7 @@
|
||||
main_activity: clean(formData.main_activity),
|
||||
program: clean(formData.program),
|
||||
program_number: clean(formData.program_number),
|
||||
prosec: Number(formData.prosec) || 0,
|
||||
prosec_authorization: clean(formData.prosec_authorization),
|
||||
responsible_name: clean(formData.responsible_name),
|
||||
responsible_last_name: clean(formData.responsible_last_name),
|
||||
@@ -239,7 +231,10 @@
|
||||
broker_company: clean(formData.broker_company),
|
||||
inter_db_name: clean(formData.inter_db_name),
|
||||
prevalidator_key: clean(formData.prevalidator_key),
|
||||
seventh_amendment: formData.seventh_amendment
|
||||
seventh_amendment: formData.seventh_amendment,
|
||||
// Ensure optional booleans are passed correctly or default to false/null if needed
|
||||
has_express_line: formData.has_express_line,
|
||||
is_service_company: formData.is_service_company
|
||||
};
|
||||
|
||||
const response = isEdit
|
||||
@@ -285,6 +280,8 @@
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<div class="min-h-[400px]">
|
||||
<Tabs.Content value="general" class="space-y-4 pt-4">
|
||||
|
||||
|
||||
<!-- Logo Upload Section -->
|
||||
<div class="grid gap-4 p-4 border rounded-lg bg-muted/30">
|
||||
<Label>Logo de la Empresa</Label>
|
||||
@@ -344,36 +341,9 @@
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="logo">Ruta del Logo</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input id="logo" bind:value={formData.logo} placeholder="/path/to/logo.png" />
|
||||
{#if isEdit}
|
||||
<div class="relative">
|
||||
<Button variant="outline" size="icon" disabled={uploading}>
|
||||
{#if uploading}
|
||||
<LoaderCircle class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Upload class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onchange={handleFileSelect}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-[0.8rem] text-muted-foreground">Sube una imagen para obtener su ruta local.</p>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="client_name">Nombre Cliente (Maquila)</Label>
|
||||
<Input id="client_name" bind:value={formData.client_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="client_name">Nombre Cliente (Maquila)</Label>
|
||||
<Input id="client_name" bind:value={formData.client_name} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user