feature/app-selector
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
73
frontend/src/lib/components/sidebar/app-launcher.svelte
Normal file
73
frontend/src/lib/components/sidebar/app-launcher.svelte
Normal 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>
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user