feat: enhance invoice creation and editing experience
- Added a new `isCreate` prop to manage the invoice creation state in the invoice top fields component. - Implemented a conditional invoice type selection for the export operation in the invoice top fields. - Updated sidebar module links to simplify URL parameters for invoice exports. - Refactored invoice type handling in the dashboard to improve dynamic title generation based on operation type and invoice type. - Adjusted server-side logic to enforce required parameters for invoice creation based on operation type. - Cleaned up unused filters in the invoice page to streamline the user interface.
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
defaultInvoiceType = undefined,
|
||||
invoiceType = undefined,
|
||||
isSettings = false,
|
||||
isCreate = false,
|
||||
highlightFieldId = null,
|
||||
onDismissHighlightForField = undefined
|
||||
}: {
|
||||
@@ -29,6 +30,7 @@
|
||||
defaultInvoiceType?: string | null;
|
||||
invoiceType?: string;
|
||||
isSettings?: boolean;
|
||||
isCreate?: boolean;
|
||||
highlightFieldId?: string | null;
|
||||
onDismissHighlightForField?: (fieldKey: string) => void;
|
||||
} = $props();
|
||||
@@ -233,16 +235,38 @@
|
||||
id="invoice-field-invoice_type"
|
||||
tabindex="-1"
|
||||
class={cn(
|
||||
'min-w-[100px] flex-1 space-y-1 rounded-md outline-none',
|
||||
'min-w-[140px] flex-[1.2] space-y-1 rounded-md outline-none',
|
||||
hl('invoice_type')
|
||||
)}
|
||||
>
|
||||
<Label class="text-xs text-muted-foreground"
|
||||
>{m.invoice_edit_form_invoice_type_label()}</Label
|
||||
>
|
||||
<p class="flex h-8 items-center text-sm font-medium">
|
||||
{formData.invoice_type ? `${formData.invoice_type}` : '...'}
|
||||
</p>
|
||||
{#if isCreate && !isSettings && formData.operation_type === 'exp'}
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.invoice_type || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.invoice_type = v || '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class={cn('h-8 w-full text-sm', hl('invoice_type'))}>
|
||||
<span class="truncate">
|
||||
{filteredInvoiceTypes.find((t) => t.key === formData.invoice_type)?.description ||
|
||||
m.invoice_edit_form_invoice_type_placeholder()}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[min(60vh,320px)]">
|
||||
{#each filteredInvoiceTypes as t}
|
||||
<Select.Item value={t.key}>{t.description}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{:else}
|
||||
<p class="flex h-8 items-center text-sm font-medium">
|
||||
{formData.invoice_type ? `${formData.invoice_type}` : '...'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -447,7 +447,7 @@ export function getSidebarData(): SidebarData {
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.export_invoices.exportation"](),
|
||||
url: "/dashboard/invoices?operation_type=exp&invoice_type=EXDEF",
|
||||
url: "/dashboard/invoices?operation_type=exp",
|
||||
permission: "invoice.exp.view"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -73,32 +73,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeIdentity(value: string | undefined | null): string {
|
||||
return (value ?? '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
let tenantIdentitySet = $derived.by(() => {
|
||||
const set = new Set<string>();
|
||||
for (const tenant of userTenants) {
|
||||
set.add(normalizeIdentity(tenant.name));
|
||||
set.add(normalizeIdentity(tenant.slug));
|
||||
}
|
||||
set.delete('');
|
||||
return set;
|
||||
});
|
||||
|
||||
// Excluir del listado de companias cualquier registro que realmente represente al tenant.
|
||||
let myCompanies = $derived(
|
||||
companyStore.companies.filter((company) => !tenantIdentitySet.has(normalizeIdentity(company.name)))
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const active = companyStore.activeCompany;
|
||||
if (!active) return;
|
||||
if (!tenantIdentitySet.has(normalizeIdentity(active.name))) return;
|
||||
if (myCompanies.length === 0) return;
|
||||
void companyStore.setActiveCompany(myCompanies[0], true);
|
||||
});
|
||||
// Misma lista que devuelve my-companies; no filtrar por tenant (nombre/slug equivalentes
|
||||
// ocultaban la empresa creada al primer login).
|
||||
let myCompanies = $derived(companyStore.companies);
|
||||
</script>
|
||||
|
||||
<Sidebar.Menu>
|
||||
|
||||
@@ -275,26 +275,39 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Título dinámico según el tipo de operación y factura
|
||||
// Título dinámico según el tipo de operación y tipo de factura (i18n para casos clásicos; catálogo API para el resto)
|
||||
const viewTitle = $derived.by(() => {
|
||||
const op = filters.operation_type;
|
||||
const type = filters.invoice_type;
|
||||
|
||||
const type = (filters.invoice_type || '').trim();
|
||||
|
||||
const base = m['invoice_list.titles.base']();
|
||||
|
||||
|
||||
const titleFromInvoiceTypesCatalog = (): string | null => {
|
||||
if (!type || !data.invoiceTypes?.length) return null;
|
||||
const hit = data.invoiceTypes.find((t: { key: string }) => t.key === type);
|
||||
if (!hit?.description) return null;
|
||||
return `${base} DE ${String(hit.description).trim().toUpperCase()}`;
|
||||
};
|
||||
|
||||
if (op === 'imp') {
|
||||
if (type === 'TEM') return `${base} ${m['invoice_list.titles.import_temporal']()}`;
|
||||
if (type === 'DEF') return `${base} ${m['invoice_list.titles.import_definitive']()}`;
|
||||
if (type === 'MEX') return `${base} ${m['invoice_list.titles.import_mexican']()}`;
|
||||
if (type === 'CR') return `${base} ${m['invoice_list.titles.import_regime_change']()}`;
|
||||
if (type === 'REP') return `${base} ${m['invoice_list.titles.import_repair']()}`;
|
||||
const catalogTitle = titleFromInvoiceTypesCatalog();
|
||||
if (catalogTitle) return catalogTitle;
|
||||
return `${base} ${m['invoice_list.titles.import']()}`;
|
||||
} else if (op === 'exp') {
|
||||
}
|
||||
|
||||
if (op === 'exp') {
|
||||
if (type === 'EXDEF') return `${base} ${m['invoice_list.titles.export_definitive']()}`;
|
||||
if (type === 'REPAR') return `${base} ${m['invoice_list.titles.export_repair']()}`;
|
||||
const catalogTitle = titleFromInvoiceTypesCatalog();
|
||||
if (catalogTitle) return catalogTitle;
|
||||
return `${base} ${m['invoice_list.titles.export']()}`;
|
||||
}
|
||||
|
||||
|
||||
return m['invoice_list.header.title']();
|
||||
});
|
||||
|
||||
@@ -1355,13 +1368,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Opciones de tipo de operación para el filtro
|
||||
const operationTypeOptions = $derived.by(() => [
|
||||
{ value: '', label: m.invoice_list_operation_types_all() },
|
||||
{ value: 'imp', label: m.invoice_list_operation_types_import() },
|
||||
{ value: 'exp', label: m.invoice_list_operation_types_export() }
|
||||
]);
|
||||
|
||||
// Todas las opciones de tipo de factura con su operación correspondiente
|
||||
const allInvoiceTypeOptions = $derived(() => {
|
||||
const options = [{ value: '', label: m.invoice_list_operation_types_all(), operation: 'both' }];
|
||||
@@ -1445,35 +1451,6 @@
|
||||
<p class="text-muted-foreground">{m.invoice_list_header_description()}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<!-- Filtros ocultos por requerimiento -->
|
||||
<div class="hidden">
|
||||
<select
|
||||
id="filter-operation-type"
|
||||
bind:value={filters.operation_type}
|
||||
class="flex h-9 w-[180px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title={m.invoice_list_filters_operation_label()}
|
||||
>
|
||||
{#each operationTypeOptions as option}
|
||||
<option value={option.value}>
|
||||
{option.value === '' ? m.invoice_list_filters_operation_all_option() : option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-9 w-[220px] rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title={m.invoice_list_filters_invoice_type_label()}
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>
|
||||
{option.value === '' ? m.invoice_list_filters_invoice_type_all_option() : option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" class="h-9" onclick={() => goto('/dashboard/invoices/settings')}>
|
||||
<Settings class="mr-2" size={16} />
|
||||
{m.invoice_list_actions_parameters()}
|
||||
@@ -1503,6 +1480,20 @@
|
||||
<Card.Title>{m.invoice_list_card_invoice_list_title()}</Card.Title>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{#if filters.operation_type === 'exp'}
|
||||
<select
|
||||
id="filter-invoice-type"
|
||||
bind:value={filters.invoice_type}
|
||||
class="flex h-9 w-[min(100%,260px)] shrink-0 rounded-md border border-input bg-card px-3 py-1 text-sm shadow-sm ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
|
||||
title={m.invoice_list_filters_invoice_type_label()}
|
||||
>
|
||||
{#each invoiceTypeOptions() as option}
|
||||
<option value={option.value}>
|
||||
{option.value === '' ? m.invoice_list_filters_invoice_type_all_option() : option.label}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
<Input
|
||||
bind:value={filters.invoice_number}
|
||||
placeholder={m.invoice_list_filters_invoice_number_placeholder()}
|
||||
|
||||
@@ -26,8 +26,11 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
||||
parsedOperationType = operationTypeParam;
|
||||
}
|
||||
|
||||
// Para creación ('new'), es obligatorio tener operation_type e invoice_type
|
||||
if (params.id === 'new' && (!parsedOperationType || !invoiceTypeParam)) {
|
||||
// Para creación ('new'): siempre operation_type; invoice_type obligatorio solo en importación.
|
||||
if (params.id === 'new' && !parsedOperationType) {
|
||||
throw redirect(302, '/dashboard');
|
||||
}
|
||||
if (params.id === 'new' && parsedOperationType === 'imp' && !invoiceTypeParam) {
|
||||
throw redirect(302, '/dashboard');
|
||||
}
|
||||
|
||||
|
||||
@@ -498,14 +498,20 @@
|
||||
let lastFetchedDate = $state('');
|
||||
let originalInvoiceDate = $state(data.invoice?.invoice_date || '');
|
||||
|
||||
// Derivados reactivos para el tipo de factura y operación
|
||||
// Tipo de factura efectivo (cabecera + tabs): prioridad campos superiores, luego URL/factura.
|
||||
// En importación sin tipo aún, se asume TEM como antes; en exportación sin tipo, cadena vacía (sin forzar TEM).
|
||||
let invoiceType = $derived.by(() => {
|
||||
return (
|
||||
InvoiceTopFieldsFormData?.invoice_type ||
|
||||
data.filters?.invoice_type ||
|
||||
data.invoice?.invoice_type ||
|
||||
'TEM'
|
||||
); // Default to TEM if not found
|
||||
const top = InvoiceTopFieldsFormData?.invoice_type;
|
||||
if (top !== undefined && top !== null && String(top).trim() !== '') {
|
||||
return String(top).trim();
|
||||
}
|
||||
if (data.filters?.invoice_type) return String(data.filters.invoice_type).trim();
|
||||
if (data.invoice?.invoice_type) return String(data.invoice.invoice_type).trim();
|
||||
const op =
|
||||
InvoiceTopFieldsFormData?.operation_type ||
|
||||
data.invoice?.operation_type ||
|
||||
data.filters?.operation_type;
|
||||
return op === 'imp' ? 'TEM' : '';
|
||||
});
|
||||
|
||||
let operationTypeText = $derived.by(() => {
|
||||
@@ -1044,19 +1050,18 @@
|
||||
</Button>
|
||||
<h1 class="text-3xl font-bold tracking-tight">
|
||||
{#if data.isCreate}
|
||||
{@const invoiceType = generalFormData?.invoice_type || data.filters?.invoice_type}
|
||||
{@const invoiceTypeInfo = invoiceType
|
||||
? data.invoiceTypes?.find((t) => t.key === invoiceType)
|
||||
: null}
|
||||
{m.invoice_edit_new_title()}
|
||||
{#if invoiceTypeInfo}
|
||||
<span class="text-2xl font-normal text-muted-foreground">
|
||||
- {invoiceTypeInfo.description}
|
||||
</span>
|
||||
{/if}
|
||||
{:else}
|
||||
{m.invoice_edit_page_invoice_prefix()}{data.invoice.id}
|
||||
{/if}
|
||||
{#if invoiceType}
|
||||
{@const headerInvoiceTypeInfo = data.invoiceTypes?.find((t) => t.key === invoiceType)}
|
||||
{#if headerInvoiceTypeInfo}
|
||||
<span class="text-2xl font-normal text-muted-foreground">
|
||||
- {headerInvoiceTypeInfo.description}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</h1>
|
||||
<Badge variant="outline" class="px-3 py-1 text-sm font-bold {operationColorClass}">
|
||||
{operationTypeText}
|
||||
@@ -1088,6 +1093,7 @@
|
||||
defaultOperationType={data.filters?.operation_type ?? undefined}
|
||||
defaultInvoiceType={data.filters?.invoice_type ?? undefined}
|
||||
{invoiceType}
|
||||
isCreate={data.isCreate === true}
|
||||
highlightFieldId={validationHighlightField}
|
||||
onDismissHighlightForField={dismissValidationHighlight}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user