Merge pull request 'fixed/logo_empresa' (#70) from fixed/logo_empresa into development

Reviewed-on: ADUANASOFT/anexo76#70
This commit is contained in:
2026-01-22 23:30:06 +00:00
13 changed files with 404 additions and 355 deletions

View File

@@ -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>> {

View File

@@ -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>

View File

@@ -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"
/>

View File

@@ -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
*/

View File

@@ -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>