feature/app-selector

This commit is contained in:
2026-05-26 16:59:32 -06:00
parent 904fa965c8
commit dbf68986da
34 changed files with 792 additions and 169 deletions

View File

@@ -36,9 +36,18 @@
initialData?: any;
externalError?: string;
onClearError?: () => void;
mode?: 'fixed_asset' | 'inventory';
}
let { onSave, onCancel, initialData, externalError = '', onClearError }: Props = $props();
let {
onSave,
onCancel,
initialData,
externalError = '',
onClearError,
mode = 'fixed_asset'
}: Props = $props();
const isInventoryMode = $derived(mode === 'inventory');
// Tipo para unidad de medida con todos los campos
interface UnitOfMeasure {
@@ -286,7 +295,9 @@
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const response = await materialTypesApi.list(companyId, 1, 100, 'ACTIVO FIJO');
const response = isInventoryMode
? await materialTypesApi.list(companyId, 1, 100)
: await materialTypesApi.list(companyId, 1, 100, 'ACTIVO FIJO');
if (response.data) {
materialTypes = response.data.items;
}
@@ -546,7 +557,9 @@
}
if (!formData.material_key?.trim()) {
errors.material_key = 'El tipo de activo fijo es obligatorio';
errors.material_key = isInventoryMode
? 'El tipo de material es obligatorio'
: 'El tipo de activo fijo es obligatorio';
}
if (!formData.unit_of_measure?.trim()) {
@@ -585,7 +598,9 @@
break;
case 'material_key':
if (!formData.material_key?.trim()) {
errors.material_key = 'El tipo de activo fijo es obligatorio';
errors.material_key = isInventoryMode
? 'El tipo de material es obligatorio'
: 'El tipo de activo fijo es obligatorio';
} else {
delete errors.material_key;
}
@@ -685,7 +700,7 @@
<div class="space-y-2">
<Label for="material_key" class="font-bold">
Tipo de Activo Fijo: <span class="text-red-500">*</span>
{isInventoryMode ? 'Tipo de Material' : 'Tipo de Activo Fijo'}: <span class="text-red-500">*</span>
</Label>
<div class="flex items-center gap-2">
<Input
@@ -833,48 +848,50 @@
</div>
</div>
<!-- Tasa depreciación y ECCN (misma fila que en pantalla legacy) -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="annual_depreciation_rate">Tasa Anual de Depreciación:</Label>
<div class="flex items-center gap-2">
{#if !isInventoryMode}
<!-- Campos exclusivos del flujo de Activo Fijo -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="annual_depreciation_rate">Tasa Anual de Depreciación:</Label>
<div class="flex items-center gap-2">
<Input
id="annual_depreciation_rate"
type="number"
bind:value={formData.annual_depreciation_rate}
placeholder="0.00"
class="flex-1"
step="0.01"
/>
<span class="text-sm">%</span>
<Button type="button" variant="outline" size="icon" onclick={openDepreciationSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="eccn_code">ECCN:</Label>
<Input
id="annual_depreciation_rate"
type="number"
bind:value={formData.annual_depreciation_rate}
placeholder="0.00"
class="flex-1"
step="0.01"
id="eccn_code"
bind:value={formData.eccn_code}
placeholder="Código ECCN"
class="w-full uppercase"
maxlength={20}
/>
<span class="text-sm">%</span>
<Button type="button" variant="outline" size="icon" onclick={openDepreciationSearch}>
</div>
</div>
<!-- Clave FDA -->
<div class="space-y-2">
<Label for="fda_key">Clave FDA:</Label>
<div class="flex items-center gap-2">
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="eccn_code">ECCN:</Label>
<Input
id="eccn_code"
bind:value={formData.eccn_code}
placeholder="Código ECCN"
class="w-full uppercase"
maxlength={20}
/>
</div>
</div>
<!-- Clave FDA -->
<div class="space-y-2">
<Label for="fda_key">Clave FDA:</Label>
<div class="flex items-center gap-2">
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
{/if}
<!-- Fracción exenta de IVA (Sí / No + código opcional máx. 4) -->
<div class="space-y-2">
@@ -930,7 +947,7 @@
<Dialog.Root bind:open={showMaterialDialog}>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>CATALOGO DE ACTIVO FIJO</Dialog.Title>
<Dialog.Title>{isInventoryMode ? 'CATALOGO DE MATERIALES' : 'CATALOGO DE ACTIVO FIJO'}</Dialog.Title>
</Dialog.Header>
<div class="space-y-4">
<div>

View File

@@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData } from '$lib/api/dashboard/a76/invoices';
import { m } from '$lib/i18n/messages';
import { normalizeInvoiceFieldPath } from './focus-invoice-field';
import { systemStore } from '$lib/stores/system.svelte';
interface FormDataSet {
InvoiceTopFieldsFormData: any;
@@ -161,9 +162,14 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise<SaveInvo
function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateInvoiceData {
const { InvoiceTopFieldsFormData, generalFormData, observationFormData, itemsFormData, othersFormData, continuationFormData } = formData;
// Sistema activo del selector de app (SCAF / SCAII).
// Si por alguna razón no está inicializado, mantenemos el comportamiento actual.
const activeSystem = systemStore.activeSystem;
const resolvedSystem = activeSystem ?? 'fixed_asset';
const payload: any = {
// Datos generales desde InvoiceTopFieldsFormData
system: 'fixed_asset',
system: resolvedSystem,
operation_type: InvoiceTopFieldsFormData?.operation_type || undefined,
invoice_type: InvoiceTopFieldsFormData?.invoice_type || undefined,
document_type: generalFormData?.document_type || undefined,

View File

@@ -0,0 +1,73 @@
<script lang="ts">
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import CheckIcon from '@lucide/svelte/icons/check';
import LayoutGridIcon from '@lucide/svelte/icons/layout-grid';
import PackageIcon from '@lucide/svelte/icons/package';
import BarChart3Icon from '@lucide/svelte/icons/bar-chart-3';
import { systemStore, SYSTEM_LABELS, type SystemType } from '$lib/stores/system.svelte';
import { invalidateAll } from '$app/navigation';
const ICONS: Record<SystemType, any> = {
fixed_asset: PackageIcon,
inventory: BarChart3Icon,
};
async function switchSystem(sys: SystemType) {
if (sys === systemStore.activeSystem || systemStore.switching) return;
const ok = await systemStore.setActiveSystem(sys);
if (ok) await invalidateAll();
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<button
{...props}
class="inline-flex size-8 shrink-0 items-center justify-center rounded-md
text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground
transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring
data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
title="Aplicaciones"
aria-label="Abrir selector de aplicaciones"
>
<LayoutGridIcon class="size-4" />
</button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-64 rounded-xl p-3"
align="end"
side="bottom"
sideOffset={8}
>
<p class="mb-3 px-1 text-xs font-medium text-muted-foreground">Tus aplicaciones</p>
<div class="grid grid-cols-2 gap-2">
{#each systemStore.allowedSystems as sys (sys)}
{@const label = SYSTEM_LABELS[sys]}
{@const Icon = ICONS[sys]}
{@const active = systemStore.activeSystem === sys}
{@const canSwitch = systemStore.canSwitch}
<button
onclick={() => switchSystem(sys)}
disabled={!canSwitch || systemStore.switching}
class="flex flex-col items-center gap-1.5 rounded-lg p-3 text-center transition-colors
disabled:cursor-default
{canSwitch ? 'hover:bg-accent' : ''}
{active ? 'ring-2 ring-primary bg-accent/50' : ''}
{systemStore.switching ? 'opacity-50' : ''}"
>
<div class="flex size-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Icon class="size-5" />
</div>
<span class="text-sm font-medium leading-tight">{label.name}</span>
<span class="text-xs text-muted-foreground">{label.code}</span>
{#if active}
<CheckIcon class="size-3 text-primary" />
{/if}
</button>
{/each}
</div>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,6 +8,8 @@
import NavProjects from "./nav-projects.svelte";
import NavUser from "./nav-user.svelte";
import TeamSwitcher from "./team-switcher.svelte";
import AppLauncher from "./app-launcher.svelte";
import { systemStore } from "$lib/stores/system.svelte";
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
import type { ComponentProps } from "svelte";

View File

@@ -23,10 +23,13 @@ import {
import { m } from '$lib/i18n/messages';
import { Title } from '../ui/alert';
export type SystemContext = 'fixed_asset' | 'inventory';
export interface NavItem {
title: string;
url: string;
permission?: string;
systemContext?: SystemContext;
}
export interface NavMainItem {
@@ -35,6 +38,7 @@ export interface NavMainItem {
icon: any;
isActive?: boolean;
permission?: string;
systemContext?: SystemContext;
items?: NavItem[];
}

View File

@@ -5,6 +5,7 @@
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import ChevronRight from '@lucide/svelte/icons/chevron-right';
import { authStore, userHasPermission } from '$lib/auth';
import { systemStore } from '$lib/stores/system.svelte';
import { page } from '$app/state';
let {
@@ -16,10 +17,12 @@
icon?: any;
isActive?: boolean;
permission?: string;
systemContext?: 'fixed_asset' | 'inventory';
items?: {
title: string;
url: string;
permission?: string;
systemContext?: 'fixed_asset' | 'inventory';
}[];
}[];
} = $props();
@@ -29,13 +32,20 @@
return pathname === url || pathname.startsWith(url + '/');
}
// Filtrar items según permisos (si el item tiene la propiedad 'permission')
function matchesSystem(ctx: 'fixed_asset' | 'inventory' | undefined): boolean {
if (!ctx) return true;
if (!systemStore.activeSystem) return false;
return ctx === systemStore.activeSystem;
}
// Filtrar items según permisos y sistema activo
const filteredItems = $derived(
items
.map((item) => ({
...item,
items: item.items?.filter((subItem) => {
if (subItem.permission && !userHasPermission($authStore.user, subItem.permission)) return false;
if (!matchesSystem(subItem.systemContext)) return false;
return true;
})
}))
@@ -43,7 +53,10 @@
// 1. Filtrar por permiso explícito del item principal
if (item.permission && !userHasPermission($authStore.user, item.permission)) return false;
// 2. Ocultar categorías (url="#") que se quedaron sin sub-items visibles
// 2. Filtrar por sistema activo
if (!matchesSystem(item.systemContext)) return false;
// 3. Ocultar categorías (url="#") que se quedaron sin sub-items visibles
if (item.url === '#' && item.items && item.items.length === 0) return false;
return true;