fix: solución de bloqueos y estandarización de permisos

This commit is contained in:
2026-04-27 10:47:30 -05:00
parent fa3cbb4d0a
commit 5c2a84e95d
312 changed files with 13889 additions and 7911 deletions

View File

@@ -1,4 +1,4 @@
import { BACKEND_URL } from '$lib/config/backend';
import { api } from '$lib/api';
export interface AppSettingsRequest {
tenant_id?: number | null;
@@ -11,24 +11,17 @@ export const appSettingsApi = {
* Resolves settings merging Global -> Tenant -> Company hierarchy
*/
async getResolved(tenantId: number, companyId: number): Promise<Record<string, any>> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`);
if (!response.ok) throw new Error('Error al obtener configuraciones');
return response.json();
const res = await api.get<Record<string, any>>(`/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`);
if (res.error) throw new Error(res.error);
return res.data || {};
},
/**
* Upserts an override at a specific level
*/
async upsert(payload: AppSettingsRequest): Promise<any> {
const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/upsert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al guardar configuración');
}
return response.json();
const res = await api.post<any>(`/v1/a76/app-settings/upsert`, payload);
if (res.error) throw new Error(res.error);
return res.data;
}
};

View File

@@ -37,6 +37,9 @@ export interface PortUpdate {
export interface PortListResponse {
items: Port[];
total: number;
page: number;
page_size: number;
pages: number;
}
class PortsApi {

View File

@@ -142,59 +142,59 @@ export interface MovementItemDetailed extends MovementItem {
export const invoiceMovementsApi = {
// Temporary Imports
getTemporaryImports: (filters: ImportTemporaryFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/temporary', filters),
getTemporaryImports: (companyId: number, filters: ImportTemporaryFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/temporary?company_id=${companyId}`, filters),
getTemporaryImportsDetailed: (filters: ImportTemporaryFilter) =>
getTemporaryImportsDetailed: (companyId: number, filters: ImportTemporaryFilter) =>
api.post<MovementItemDetailed[]>(
'/v1/a76/reports/movements/invoices/temporary-detailed',
`/v1/a76/reports/movements/invoices/temporary-detailed?company_id=${companyId}`,
filters
),
// Definitive Imports
getDefinitiveImports: (filters: ImportDefinitiveFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/definitive', filters),
getDefinitiveImports: (companyId: number, filters: ImportDefinitiveFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/definitive?company_id=${companyId}`, filters),
getDefinitiveImportsDetailed: (filters: ImportDefinitiveFilter) =>
getDefinitiveImportsDetailed: (companyId: number, filters: ImportDefinitiveFilter) =>
api.post<MovementItemDetailed[]>(
'/v1/a76/reports/movements/invoices/definitive-detailed',
`/v1/a76/reports/movements/invoices/definitive-detailed?company_id=${companyId}`,
filters
),
// Repair Imports
getRepairImports: (filters: ImportRepairFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/repair', filters),
getRepairImports: (companyId: number, filters: ImportRepairFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/repair?company_id=${companyId}`, filters),
getRepairImportsDetailed: (filters: ImportRepairFilter) =>
getRepairImportsDetailed: (companyId: number, filters: ImportRepairFilter) =>
api.post<MovementItemDetailed[]>(
'/v1/a76/reports/movements/invoices/repair-detailed',
`/v1/a76/reports/movements/invoices/repair-detailed?company_id=${companyId}`,
filters
),
// Exports
getExports: (filters: ExportFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/export', filters),
getExports: (companyId: number, filters: ExportFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/export?company_id=${companyId}`, filters),
getExportsDetailed: (filters: ExportFilter) =>
api.post<MovementItemDetailed[]>('/v1/a76/reports/movements/invoices/export-detailed', filters),
getExportsDetailed: (companyId: number, filters: ExportFilter) =>
api.post<MovementItemDetailed[]>(`/v1/a76/reports/movements/invoices/export-detailed?company_id=${companyId}`, filters),
// Export Repairs
getExportRepairs: (filters: ExportRepairFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/export-repair', filters),
getExportRepairs: (companyId: number, filters: ExportRepairFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/export-repair?company_id=${companyId}`, filters),
getExportRepairsDetailed: (filters: ExportRepairFilter) =>
getExportRepairsDetailed: (companyId: number, filters: ExportRepairFilter) =>
api.post<MovementItemDetailed[]>(
'/v1/a76/reports/movements/invoices/export-repair-detailed',
`/v1/a76/reports/movements/invoices/export-repair-detailed?company_id=${companyId}`,
filters
),
// All Movements
getAllMovements: (filters: AllMovementsFilter) =>
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/all', filters),
getAllMovements: (companyId: number, filters: AllMovementsFilter) =>
api.post<MovementItem[]>(`/v1/a76/reports/movements/invoices/all?company_id=${companyId}`, filters),
// Async Generation
generateReportAsync: (filters: AllMovementsFilter) =>
api.post<{ task_id: string }>('/v1/a76/reports/movements/invoices/generate', filters),
generateReportAsync: (companyId: number, filters: AllMovementsFilter) =>
api.post<{ task_id: string }>(`/v1/a76/reports/movements/invoices/generate?company_id=${companyId}`, filters),
getTaskStatus: (taskId: string) =>
api.get<{ task_id: string; status: string; result?: any; meta?: any }>(

View File

@@ -384,7 +384,7 @@ export const invoicesApi = {
const params = new URLSearchParams({
company_id: companyId.toString()
});
return api.put<Invoice>(`/v1/a76/invoices/${invoiceId}/?${params.toString()}`, data);
return api.put<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data);
},
/**

View File

@@ -16,8 +16,7 @@ export interface MaterialTypeListResponse {
}
export const materialTypesApi = {
list: async (page = 1, pageSize = 100) => {
return api.get<MaterialTypeListResponse>(`/v1/public/reference-data/material-types/?page=${page}&page_size=${pageSize}`);
list: async (companyId: number, page = 1, pageSize = 100) => {
return api.get<MaterialTypeListResponse>(`/v1/public/reference_data/material-types/?company_id=${companyId}&page=${page}&page_size=${pageSize}`);
}
};

View File

@@ -2,7 +2,7 @@
* API para gestión de roles por compañía
*/
import { api } from '$lib/api';
import { api, type ApiResponse } from '$lib/api';
export interface CompanyRole {
id: number;
@@ -49,45 +49,41 @@ export const rolesAPI = {
is_active?: boolean;
search?: string;
}
): Promise<RoleListResponse> {
): Promise<ApiResponse<RoleListResponse>> {
const queryParams = new URLSearchParams();
queryParams.set('company_id', companyId.toString());
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.page_size) queryParams.set('page_size', params.page_size.toString());
if (params?.is_active !== undefined) queryParams.set('is_active', params.is_active.toString());
if (params?.search) queryParams.set('search', params.search);
const response = await api.get(`/v1/core/permissions/roles?${queryParams.toString()}`);
return response.data;
return api.get(`/v1/core/permissions/roles?${queryParams.toString()}`);
},
/**
* Obtener un rol por ID
*/
async getById(id: number, companyId: number): Promise<CompanyRole> {
const response = await api.get(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
return response.data;
async getById(id: number, companyId: number): Promise<ApiResponse<CompanyRole>> {
return api.get(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
},
/**
* Crear un nuevo rol
*/
async create(companyId: number, data: CreateRoleData): Promise<CompanyRole> {
const response = await api.post(`/v1/core/permissions/roles?company_id=${companyId}`, data);
return response.data;
async create(companyId: number, data: CreateRoleData): Promise<ApiResponse<CompanyRole>> {
return api.post(`/v1/core/permissions/roles?company_id=${companyId}`, data);
},
/**
* Actualizar un rol
*/
async update(id: number, companyId: number, data: UpdateRoleData): Promise<CompanyRole> {
const response = await api.patch(`/v1/core/permissions/roles/${id}?company_id=${companyId}`, data);
return response.data;
async update(id: number, companyId: number, data: UpdateRoleData): Promise<ApiResponse<CompanyRole>> {
return api.patch(`/v1/core/permissions/roles/${id}?company_id=${companyId}`, data);
},
/**
* Eliminar un rol
*/
async delete(id: number, companyId: number): Promise<void> {
await api.delete(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
async delete(id: number, companyId: number): Promise<ApiResponse<void>> {
return api.delete(`/v1/core/permissions/roles/${id}?company_id=${companyId}`);
}
};

View File

@@ -1,4 +1,5 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface Sector {
id: number;
@@ -23,7 +24,7 @@ export async function getSectors(
pageSize = 50,
companyId: number,
search?: string
): Promise<SectorListResponse> {
): Promise<ApiResponse<SectorListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
@@ -34,7 +35,5 @@ export async function getSectors(
params.append('key', search);
}
const response = await api.get<SectorListResponse>(`/v1/a76/sectors/?${params.toString()}`);
if (!response.data) throw new Error('Error fetching sectors');
return response.data;
return await api.get<SectorListResponse>(`/v1/a76/sectors/?${params.toString()}`);
}

View File

@@ -41,12 +41,13 @@ export interface UpdateCountryData {
export const countriesApi = {
/**
* Lista todos los países con paginación
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/countries/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/countries/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}

View File

@@ -32,12 +32,13 @@ export interface UpdateCustomsSectionData {
export const customsSectionsApi = {
/**
* Lista todas las secciones aduaneras con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}&company_id=${companyId}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -45,12 +46,13 @@ export const customsSectionsApi = {
},
/**
* Obtiene una sección aduanera por código
* @param customs_code - Código de la sección aduanera
* Obtiene una sección aduanera por key
* @param companyId - ID de la empresa
* @param sectionCode - Código de la sección
*/
get: (customs_code: string) =>
get: (companyId: number, sectionCode: string) =>
// CORREGIDO: Añadido '/' final
api.get<CustomsSection>(`/v1/public/reference_data/customs-sections/${customs_code}/`),
api.get<CustomsSection>(`/v1/public/reference_data/customs-sections/${sectionCode}/?company_id=${companyId}`),
/**
* Crea una nueva sección aduanera

View File

@@ -35,13 +35,15 @@
export const materialTypesApi = {
/**
* Lista todos los tipos de material con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param type - Filtrar por tipo (ACTIVO FIJO, MATERIALES, PRODUCTOS)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, type?: string, search?: string) => {
list: (companyId: number, page = 1, pageSize = 50, type?: string, search?: string) => {
const params = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString()
});

View File

@@ -30,14 +30,8 @@ export interface UpdatePaymentMethodData {
* API para Payment Methods
*/
export const paymentMethodsApi = {
/**
* Lista todos los métodos de pago con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}&company_id=${companyId}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -46,11 +40,12 @@ export const paymentMethodsApi = {
/**
* Obtiene un método de pago por key
* @param companyId - ID de la empresa
* @param key - Clave del método de pago
*/
get: (key: string) =>
get: (companyId: number, key: string) =>
// CORREGIDO: Añadido '/' final
api.get<PaymentMethod>(`/v1/public/reference_data/payment-methods/${key}/`),
api.get<PaymentMethod>(`/v1/public/reference_data/payment-methods/${key}/?company_id=${companyId}`),
/**
* Crea un nuevo método de pago

View File

@@ -36,8 +36,8 @@ export const pedimentoCodesApi = {
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}&company_id=${companyId}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -46,34 +46,35 @@ export const pedimentoCodesApi = {
/**
* Obtiene una clave de pedimento por code
* @param companyId - ID de la empresa
* @param code - Código de la clave de pedimento
*/
get: (code: string) =>
get: (companyId: number, code: string) =>
// CORREGIDO: Añadido '/' final
api.get<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/`),
api.get<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/?company_id=${companyId}`),
/**
* Crea una nueva clave de pedimento
* @param companyId - ID de la empresa
* @param data - Datos de la clave de pedimento a crear
*/
create: (data: CreatePedimentoCodeData) =>
// CORREGIDO: Añadido '/' final
api.post<PedimentoCode>('/v1/public/reference_data/pedimento-codes/', data),
create: (companyId: number, data: CreatePedimentoCodeData) =>
api.post<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/?company_id=${companyId}`, data),
/**
* Actualiza una clave de pedimento existente
* @param companyId - ID de la empresa
* @param code - Código de la clave de pedimento a actualizar
* @param data - Datos a actualizar
*/
update: (code: string, data: UpdatePedimentoCodeData) =>
// CORREGIDO: Añadido '/' después del código
api.put<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/`, data),
update: (companyId: number, code: string, data: UpdatePedimentoCodeData) =>
api.put<PedimentoCode>(`/v1/public/reference_data/pedimento-codes/${code}/?company_id=${companyId}`, data),
/**
* Elimina una clave de pedimento
* @param companyId - ID de la empresa
* @param code - Código de la clave de pedimento a eliminar
*/
delete: (code: string) =>
// CORREGIDO: Añadido '/' después del código
api.delete(`/v1/public/reference_data/pedimento-codes/${code}/`)
delete: (companyId: number, code: string) =>
api.delete(`/v1/public/reference_data/pedimento-codes/${code}/?company_id=${companyId}`)
};

View File

@@ -32,12 +32,13 @@ export interface UpdatePedimentoRegimenData {
export const pedimentoRegimensApi = {
/**
* Lista todos los regímenes de pedimento con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}&company_id=${companyId}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -46,16 +47,13 @@ export const pedimentoRegimensApi = {
/**
* Obtiene un régimen de pedimento por code
* @param companyId - ID de la empresa
* @param code - Código del régimen de pedimento
*/
get: (code: string) =>
get: (companyId: number, code: string) =>
// CORREGIDO: Añadido '/' final
api.get<PedimentoRegimen>(`/v1/public/reference_data/pedimento-regimens/${code}/`),
api.get<PedimentoRegimen>(`/v1/public/reference_data/pedimento-regimens/${code}/?company_id=${companyId}`),
/**
* Crea un nuevo régimen de pedimento
* @param data - Datos del régimen de pedimento a crear
*/
create: (data: CreatePedimentoRegimenData) =>
// CORREGIDO: Añadido '/' final
api.post<PedimentoRegimen>('/v1/public/reference_data/pedimento-regimens/', data),

View File

@@ -5,31 +5,31 @@
import { api } from '$lib/api';
export interface State {
m3_key: string;
description: string;
mex_key?: string | null;
ame_key?: string | null;
m3_key: string;
description: string;
mex_key?: string | null;
ame_key?: string | null;
}
export interface StateListResponse {
items: State[];
total: number;
page: number;
page_size: number;
items: State[];
total: number;
page: number;
page_size: number;
}
export interface CreateStateData {
m3_key: string;
description: string;
mex_key?: string | null;
ame_key?: string | null;
m3_key: string;
description: string;
mex_key?: string | null;
ame_key?: string | null;
}
export interface UpdateStateData {
m3_key?: string;
description?: string;
mex_key?: string | null;
ame_key?: string | null;
m3_key?: string;
description?: string;
mex_key?: string | null;
ame_key?: string | null;
}
/**
@@ -38,12 +38,10 @@ export interface UpdateStateData {
export const statesApi = {
/**
* Lista todos los estados con paginación y búsqueda
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/states/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -52,34 +50,29 @@ export const statesApi = {
/**
* Obtiene un estado por m3_key
* @param m3Key - Clave M3 del estado
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
get: (m3Key: string) =>
// CORREGIDO: Añadido '/' al final
api.get<State>(`/v1/public/reference_data/states/${m3Key}/`),
get: (companyId: number, m3Key: string) =>
api.get<State>(`/v1/public/reference_data/states/${m3Key}/?company_id=${companyId}`),
/**
* Crea un nuevo estado
* @param data - Datos del estado a crear
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
create: (data: CreateStateData) =>
// CORREGIDO: Añadido '/' al final
api.post<State>('/v1/public/reference_data/states/', data),
create: (companyId: number, data: CreateStateData) =>
api.post<State>(`/v1/public/reference_data/states/?company_id=${companyId}`, data),
/**
* Actualiza un estado existente
* @param m3Key - Clave M3 del estado a actualizar
* @param data - Datos a actualizar
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
update: (m3Key: string, data: UpdateStateData) =>
// CORREGIDO: Añadido '/' después de la variable
api.put<State>(`/v1/public/reference_data/states/${m3Key}/`, data),
update: (companyId: number, m3Key: string, data: UpdateStateData) =>
api.put<State>(`/v1/public/reference_data/states/${m3Key}/?company_id=${companyId}`, data),
/**
* Elimina un estado
* @param m3Key - Clave M3 del estado a eliminar
* 🛡️ CORREGIDO: Ahora requiere companyId
*/
delete: (m3Key: string) =>
// CORREGIDO: Añadido '/' después de la variable
api.delete(`/v1/public/reference_data/states/${m3Key}/`)
delete: (companyId: number, m3Key: string) =>
api.delete(`/v1/public/reference_data/states/${m3Key}/?company_id=${companyId}`)
};

View File

@@ -32,12 +32,13 @@ export interface UpdateTransportModeData {
export const transportModesApi = {
/**
* Lista todos los modos de transporte con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-modes/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-modes/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}
@@ -46,30 +47,34 @@ export const transportModesApi = {
/**
* Obtiene un modo de transporte por key
* @param companyId - ID de la empresa
* @param key - Clave del modo de transporte
*/
get: (key: string) =>
api.get<TransportMode>(`/v1/public/reference_data/transport-modes/${key}/`),
get: (companyId: number, key: string) =>
api.get<TransportMode>(`/v1/public/reference_data/transport-modes/${key}/?company_id=${companyId}`),
/**
* Crea un nuevo modo de transporte
* @param companyId - ID de la empresa
* @param data - Datos del modo de transporte a crear
*/
create: (data: CreateTransportModeData) =>
api.post<TransportMode>('/v1/public/reference_data/transport-modes/', data),
create: (companyId: number, data: CreateTransportModeData) =>
api.post<TransportMode>(`/v1/public/reference_data/transport-modes/?company_id=${companyId}`, data),
/**
* Actualiza un modo de transporte existente
* @param companyId - ID de la empresa
* @param key - Clave del modo de transporte a actualizar
* @param data - Datos a actualizar
*/
update: (key: string, data: UpdateTransportModeData) =>
api.put<TransportMode>(`/v1/public/reference_data/transport-modes/${key}/`, data),
update: (companyId: number, key: string, data: UpdateTransportModeData) =>
api.put<TransportMode>(`/v1/public/reference_data/transport-modes/${key}/?company_id=${companyId}`, data),
/**
* Elimina un modo de transporte
* @param companyId - ID de la empresa
* @param key - Clave del modo de transporte a eliminar
*/
delete: (key: string) =>
api.delete(`/v1/public/reference_data/transport-modes/${key}/`)
delete: (companyId: number, key: string) =>
api.delete(`/v1/public/reference_data/transport-modes/${key}/?company_id=${companyId}`)
};

View File

@@ -32,12 +32,13 @@ export interface UpdateTransportTypeData {
export const transportTypesApi = {
/**
* Lista todos los tipos de transporte con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-types/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/transport-types/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}

View File

@@ -32,12 +32,13 @@ export interface UpdateValuationMethodData {
export const valuationMethodsApi = {
/**
* Lista todos los métodos de valoración con paginación y búsqueda
* @param companyId - ID de la empresa
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
* @param search - Término de búsqueda (opcional)
*/
list: (page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}`;
list: (companyId: number, page = 1, pageSize = 50, search?: string) => {
let url = `/v1/public/reference_data/valuation-methods/?company_id=${companyId}&page=${page}&page_size=${pageSize}`;
if (search) {
url += `&search=${encodeURIComponent(search)}`;
}

View File

@@ -23,6 +23,7 @@ export interface User {
name?: string;
tenantId?: number;
roles: string[];
permissions: string[];
}
export interface AuthState {
@@ -114,6 +115,14 @@ export const authStore = createAuthStore();
export const isAuthenticated = derived(authStore, ($a) => $a.isAuthenticated);
export const currentUser = derived(authStore, ($a) => $a.user);
/**
* Verifica si el usuario tiene un permiso específico
*/
export function userHasPermission(user: User | null, permission: string): boolean {
if (!user) return false;
return user.roles.includes('admin') || user.permissions.includes(permission);
}
// ─────────────────────────────────────────────────────────
// Inicialización
// ─────────────────────────────────────────────────────────
@@ -134,7 +143,7 @@ export const initAuth = async (): Promise<boolean> => {
if (cookieToken) {
authStore.setToken(cookieToken);
authStore.setAuthenticated(true);
await loadUserInfo(cookieToken).catch(() => {});
await loadUserInfo(cookieToken).catch(() => { });
authStore.setLoading(false);
return true;
}
@@ -196,24 +205,40 @@ const updateAuthState = async () => {
const tenantChanged = previousTenantId !== undefined && previousTenantId !== tenantId;
// Obtener permisos actuales para evitar que el SSO los borre si fallara el fetch posterior
let currentPerms: string[] = [];
try {
const { get } = await import('svelte/store');
const currentState = get(authStore);
currentPerms = currentState.user?.permissions || [];
} catch { }
const user: User = {
id: profile.id ?? '',
username: profile.username ?? '',
email: profile.email,
name: `${profile.firstName ?? ''} ${profile.lastName ?? ''}`.trim(),
tenantId,
roles
roles,
permissions: parsed?.permissions?.length ? parsed.permissions : currentPerms
};
authStore.setAuthenticated(true);
authStore.setUser(user);
authStore.setToken(token);
// ⚠️ IMPORTANTE: El SSO original de Keycloak no inyecta los permisos granulares
// que viven en la base de datos de PostgreSQL en nuestro `permissions: parsed?.permissions`.
// Necesitamos hacer polling a /auth/me para que `user.permissions` se rellene.
if (token) {
await loadUserInfo(token).catch(() => { });
}
if (tenantChanged && browser) {
try {
const { companyStore } = await import('./stores/company.svelte');
companyStore.clear();
} catch {}
} catch { }
}
previousTenantId = tenantId;
@@ -244,7 +269,7 @@ const setupKeycloakTokenHooks = () => {
.then(({ getSessionManager }) => {
getSessionManager()?.updateToken(keycloakInstance!.token!);
})
.catch(() => {});
.catch(() => { });
}
})
.catch(() => {
@@ -322,6 +347,15 @@ export const login = async (credentials: {
// User info
// ─────────────────────────────────────────────────────────
export const refreshPermissions = async () => {
const token = getToken();
if (token) {
await loadUserInfo(token);
return true;
}
return false;
};
const loadUserInfo = async (token: string) => {
try {
authStore.setToken(token);
@@ -335,7 +369,8 @@ const loadUserInfo = async (token: string) => {
email: d.email,
name: d.name,
tenantId: d.tenant_id,
roles: d.realm_access?.roles ?? []
roles: d.roles ?? [],
permissions: d.permissions ?? []
});
}
} catch (err) {
@@ -355,13 +390,13 @@ export const logout = async () => {
try {
const { destroySessionManager } = await import('./session-manager');
destroySessionManager();
} catch {}
} catch { }
// Limpiar store de compañías
try {
const { companyStore } = await import('./stores/company.svelte');
companyStore.clear();
} catch {}
} catch { }
// Limpiar estado en memoria
authStore.reset();
@@ -374,7 +409,7 @@ export const logout = async () => {
if (keycloakInstance?.authenticated) {
try {
await fetch('/logout', { method: 'POST' });
} catch {}
} catch { }
await keycloakInstance.logout({
redirectUri: window.location.origin + '/login'
});

View File

@@ -0,0 +1,158 @@
<script lang="ts">
import { ShieldAlert, AlertTriangle, RefreshCw, ArrowLeft, Home } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { fade, fly } from 'svelte/transition';
import { companyStore } from '$lib/stores/company.svelte';
let {
status = 0,
error = '',
onRetry = () => { if (typeof window !== 'undefined') window.location.reload(); },
onBack = () => { if (typeof window !== 'undefined') window.history.back(); }
}: {
status?: number,
error?: string,
onRetry?: () => void,
onBack?: () => void
} = $props();
// Determinar si es un error de permisos (403)
let isForbidden = $derived(status === 403 || error.toLowerCase().includes('permission') || error.toLowerCase().includes('acceso denegado') || error.includes('403'));
// Determinar si es un error de servidor (500)
let isServerError = $derived(status >= 500 || (error && (error.toLowerCase().includes('server error') || error.toLowerCase().includes('error interno'))));
// Extraer el código del permiso si viene en el error
let permissionCode = $derived.by(() => {
if (!isForbidden) return null;
// Buscar patrones como "cat_example.view" o "Missing required permissions: cat_example.view"
const missing = error.match(/Missing required permissions?:\s*([a-z0-9_.]+)/i);
if (missing) return missing[1];
const legacy = error.match(/([a-z0-9_.]+\.[a-z0-9_.]+)/i);
if (legacy) return legacy[0];
const simple = error.match(/(cat_|settings_)[a-z0-9_.]+/i);
return simple ? simple[0] : null;
});
let title = $derived(isForbidden ? 'Acceso Restringido' : isServerError ? 'Error del Servidor' : 'Algo salió mal');
let displayError = $derived.by(() => {
if (isForbidden) {
return 'No tienes los permisos necesarios para acceder a esta sección de la plataforma.';
}
if (isServerError) {
return 'Estamos experimentando dificultades técnicas en nuestros servidores.';
}
return error || 'Ocurrió un error inesperado al intentar procesar tu solicitud.';
});
const activeCompany = $derived(companyStore.activeCompany);
</script>
<div
class="flex items-center justify-center p-6 min-h-[450px] w-full"
in:fade={{ duration: 400 }}
>
<Card.Root class="max-w-md w-full border-2 bg-card/60 backdrop-blur-md shadow-2xl overflow-hidden {isForbidden ? 'border-dashed' : 'border-destructive/20'}">
<!-- Barra de progreso decorativa -->
<div class="h-1.5 w-full bg-gradient-to-r {isForbidden ? 'from-primary/80 via-primary/40 to-primary/80' : 'from-destructive/80 via-destructive/40 to-destructive/80'} animate-gradient-x"></div>
<Card.Header class="flex flex-col items-center gap-5 pt-10 text-center">
<div class="relative">
<div
class="relative"
in:fly={{ y: 20, duration: 600, delay: 100 }}
>
<div class="absolute -inset-6 {isForbidden ? 'bg-primary/15' : 'bg-destructive/15'} rounded-full blur-2xl animate-pulse"></div>
<div class="relative bg-background p-5 rounded-full border shadow-xl">
{#if isForbidden}
<ShieldAlert class="h-14 w-14 text-primary" />
{:else}
<AlertTriangle class="h-14 w-14 text-destructive" />
{/if}
</div>
</div>
</div>
<div class="space-y-3" in:fade={{ delay: 300 }}>
<Card.Title class="text-3xl font-black tracking-tight {isForbidden ? 'text-foreground' : 'text-destructive'}">
{title}
</Card.Title>
<Card.Description class="text-base text-muted-foreground px-6 leading-relaxed">
{displayError}
</Card.Description>
</div>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-8 pb-10 pt-4">
<div
class="flex flex-col items-center gap-7 w-full"
in:fade={{ delay: 500 }}
>
{#if isForbidden && permissionCode}
<div class="flex flex-col items-center gap-2.5">
<span class="text-[10px] uppercase font-bold tracking-[0.1em] text-muted-foreground/80">Identificador de Permiso</span>
<Badge variant="outline" class="font-mono text-xs bg-muted/70 border-primary/30 text-primary px-4 py-1.5 shadow-sm">
{permissionCode}
</Badge>
</div>
{/if}
{#if isServerError && error && !isForbidden}
<div class="w-full px-6">
<div class="rounded-lg bg-destructive/5 border border-destructive/10 p-4">
<p class="text-[11px] font-mono text-destructive/70 break-all text-center leading-tight">
{error.length > 150 ? error.substring(0, 150) + '...' : error}
</p>
</div>
</div>
{/if}
<div class="flex flex-col sm:flex-row items-center justify-center gap-3 w-full px-6">
{#if isServerError}
<Button variant="default" class="w-full sm:w-auto min-w-[140px] gap-2 shadow-lg hover:scale-105 transition-transform" onclick={onRetry}>
<RefreshCw class="h-4 w-4" />
Reintentar
</Button>
{/if}
<Button variant="outline" class="w-full sm:w-auto min-w-[140px] gap-2 shadow-sm" onclick={onBack}>
<ArrowLeft class="h-4 w-4" />
Regresar
</Button>
</div>
<a href="/dashboard" class="text-xs text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5">
<Home class="h-3 w-3" />
Ir al Inicio del Dashboard
</a>
</div>
</Card.Content>
<Card.Footer class="bg-muted/40 border-t py-5 justify-center">
<div class="flex flex-col items-center gap-1">
<p class="text-[10px] text-muted-foreground/80 text-center max-w-[280px]">
Si consideras que esto es un error o el problema persiste, contacta al soporte técnico.
</p>
{#if activeCompany}
<p class="text-[9px] text-muted-foreground/40 font-mono">
CID: {activeCompany.id} | TS: {new Date().toISOString()}
</p>
{/if}
</div>
</Card.Footer>
</Card.Root>
</div>
<style>
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient-x {
background-size: 200% 200%;
animation: gradient-x 5s ease infinite;
}
</style>

View File

@@ -0,0 +1,99 @@
<script lang="ts">
import { ShieldAlert, ArrowLeft, RefreshCw } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { fade, fly } from 'svelte/transition';
let {
error = '',
onRetry = () => {},
onBack = () => history.back()
} = $props();
// Extraer el código del permiso si viene en el error (ej: "Missing required permissions: cat_packages.view")
let permissionCode = $derived.by(() => {
const missing = error.match(/Missing required permissions?:\s*([a-z0-9_.]+)/i);
if (missing) return missing[1];
const legacy = error.match(/(cat_|settings_)[a-z0-9_.]+/i);
return legacy ? legacy[0] : null;
});
let displayError = $derived(
error.toLowerCase().includes('permission') || error.toLowerCase().includes('acceso denegado') || error.includes('403')
? 'No tienes los permisos necesarios para acceder a esta información.'
: error || 'Ocurrió un error inesperado al cargar los datos.'
);
</script>
<div
class="flex items-center justify-center p-8 min-h-[400px] w-full"
in:fade={{ duration: 300 }}
>
<Card.Root class="max-w-md w-full border-dashed border-2 bg-card/50 backdrop-blur-sm shadow-xl overflow-hidden">
<div class="h-1.5 w-full bg-gradient-to-r from-primary via-destructive/50 to-primary/30 animate-gradient-x"></div>
<Card.Header class="flex flex-col items-center gap-4 pt-8 text-center">
<div class="relative">
<div
class="relative"
in:fly={{ y: 20, duration: 500, delay: 200 }}
>
<div class="absolute -inset-4 bg-primary/10 rounded-full blur-xl animate-pulse"></div>
<div class="relative bg-background p-4 rounded-full border shadow-inner">
<ShieldAlert class="h-12 w-12 text-primary" />
</div>
</div>
</div>
<div in:fade={{ delay: 400 }}>
<div class="space-y-2">
<Card.Title class="text-2xl font-bold tracking-tight">Acceso Restringido</Card.Title>
<Card.Description class="text-sm text-muted-foreground px-4">
{displayError}
</Card.Description>
</div>
</div>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-6 pb-8 pt-2">
<div
class="flex flex-col items-center gap-6 w-full"
in:fade={{ delay: 600 }}
>
{#if permissionCode}
<div class="flex flex-col items-center gap-2">
<span class="text-[10px] uppercase tracking-wider text-muted-foreground font-semibold">Identificador de Permiso</span>
<Badge variant="outline" class="font-mono text-[11px] bg-muted/50 border-primary/20 text-primary px-3 py-1">
{permissionCode}
</Badge>
</div>
{/if}
<div class="flex items-center justify-center w-full px-6 text-center">
<Button variant="outline" class="w-full max-w-[200px] gap-2 shadow-sm" onclick={() => onBack()}>
<ArrowLeft class="h-4 w-4" />
Regresar al Dashboard
</Button>
</div>
</div>
</Card.Content>
<Card.Footer class="bg-muted/30 border-t py-4 justify-center">
<p class="text-[11px] text-muted-foreground text-center">
Si consideras que esto es un error, contacta al administrador del sistema.
</p>
</Card.Footer>
</Card.Root>
</div>
<style>
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient-x {
background-size: 200% 200%;
animation: gradient-x 5s ease infinite;
}
</style>

View File

@@ -3,7 +3,10 @@ import type { ExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/excha
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<ExchangeRate>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<ExchangeRate>[] {
return [
{
accessorKey: 'date',

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: ExchangeRate;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -74,19 +78,23 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,25 +3,25 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<ClassificationConcept>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<ClassificationConcept>[] {
return [
{
accessorKey: 'classification',
header: 'Clasificación',
cell: ({ row }) => row.original.classification || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: ClassificationConcept;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -62,19 +66,28 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
{#if canEdit}
<DropdownMenu.Separator />
{/if}
Eliminar
</DropdownMenu.Item>
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
{#if !canEdit && !canDelete}
<DropdownMenu.Item disabled>Sin permisos</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,7 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Company>[] {
export function createColumns(onSuccess?: () => void, permissions?: { canEdit: boolean, canDelete: boolean }): ColumnDef<Company>[] {
return [
{
accessorKey: 'name',

View File

@@ -3,7 +3,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Concept>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Concept>[] {
return [
{
accessorKey: 'code',
@@ -31,7 +34,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Concept>[] {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Concept;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -62,19 +66,23 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerConcept>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<CustomsBrokerConcept>[] {
return [
{
accessorKey: 'broker_key',
@@ -31,7 +34,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerCo
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: CustomsBrokerConcept;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -19,7 +23,7 @@
let dialogOpen = $state(false);
async function handleDelete() {
if (!confirm(`¿Estás seguro de eliminar el concepto "${item.code}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
if (!confirm(`¿Estás seguro de eliminar el concepto "${item.concept}"?\n\nNota: No se puede eliminar si tiene registros relacionados.`)) {
return;
}
@@ -62,19 +66,28 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
{#if canEdit}
<DropdownMenu.Separator />
{/if}
Eliminar
</DropdownMenu.Item>
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
{#if !canEdit && !canDelete}
<DropdownMenu.Item disabled>Sin permisos</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -82,4 +95,5 @@
bind:open={dialogOpen}
item={item}
onSuccess={onSuccess}
companyId={companyStore.activeCompany?.id ?? 0}
/>

View File

@@ -18,7 +18,10 @@ function formatDate(date?: string | null): string {
}
}
export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<Doda>[] {
return [
{
accessorKey: 'id',
@@ -126,7 +129,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Doda>[] {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
});
}
}

View File

@@ -10,10 +10,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Doda;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -61,18 +65,22 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/doda/edit/${item.id}`)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/doda/edit/${item.id}`)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,9 +2,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import type { ElectronicNotice } from '$lib/api/dashboard/a76/general_catalogs/electronic-notices';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Head } from '$lib/components/ui/table';
export function createColumns(onSuccess?: () => void): ColumnDef<ElectronicNotice>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<ElectronicNotice>[] {
return [
{
accessorKey: 'notice_number',
@@ -33,11 +34,13 @@ export function createColumns(onSuccess?: () => void): ColumnDef<ElectronicNotic
},
{
id: 'actions',
Headers: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
});
}
}

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: ElectronicNotice;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -60,18 +64,22 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -6,11 +6,15 @@ import CatalogDataTableActions from './catalog-data-table-actions.svelte';
export function createCatalogColumns({
onInsertItems,
onEdit,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
onInsertItems: (equivalency: Equivalency) => void;
onEdit: (equivalency: Equivalency) => void;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
}): ColumnDef<Equivalency>[] {
return [
{
@@ -25,13 +29,15 @@ export function createCatalogColumns({
},
{
id: 'actions',
Header: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(CatalogDataTableActions, {
item: row.original,
onInsertItems,
onEdit,
onSuccess
onSuccess,
canEdit,
canDelete
});
}
}

View File

@@ -10,12 +10,16 @@
item,
onInsertItems,
onEdit,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Equivalency;
onInsertItems: (equivalency: Equivalency) => void;
onEdit: (equivalency: Equivalency) => void;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -54,18 +58,22 @@
<DropdownMenu.Item onclick={() => onInsertItems(item)}>
<span>Insertar items</span>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => onEdit(item)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Borrar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => onEdit(item)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Borrar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,7 +2,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<EquivalencyItem>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<EquivalencyItem>[] {
return [
{
accessorKey: 'original_field',
@@ -21,11 +24,13 @@ export function createColumns(onSuccess?: () => void): ColumnDef<EquivalencyItem
},
{
id: 'actions',
Header: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
});
}
}

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: EquivalencyItem;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -65,18 +69,22 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,10 @@ import type { ErrorCatalog } from '$lib/api/dashboard/a76/general_catalogs/error
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<ErrorCatalog>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<ErrorCatalog>[] {
return [
{
accessorKey: 'code',
@@ -24,7 +27,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<ErrorCatalog>[]
header: 'Acciones',
cell: ({ row }) => renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
})
}
];

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: ErrorCatalog;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -71,19 +75,23 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -4,7 +4,10 @@ import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Header } from '$lib/components/ui/alert-dialog';
export function createColumns(onSuccess?: () => void): ColumnDef<Identifier>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Identifier>[] {
return [
{
accessorKey: 'code',
@@ -28,11 +31,13 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Identifier>[] {
},
{
id: 'actions',
Header: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Identifier;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -50,7 +54,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -60,18 +64,22 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,7 @@ import type { INPC } from '$lib/api/dashboard/a76/general_catalogs/inpc';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<INPC>[] {
export function createColumns(onSuccess?: () => void, permissions?: { canEdit: boolean, canDelete: boolean }): ColumnDef<INPC>[] {
return [
{
accessorKey: 'year',

View File

@@ -3,27 +3,30 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Legend>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code?.toString() || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
});
}
export const createColumns = (
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Legend>[] => [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code?.toString() || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
},
{
id: 'actions',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
];
}
}
];

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Legend;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -63,19 +67,23 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,9 +2,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import type { Prevalidator } from '$lib/api/dashboard/a76/general_catalogs/prevalidators';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Header } from '$lib/components/ui/alert-dialog';
export function createColumns(onSuccess?: () => void): ColumnDef<Prevalidator>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<Prevalidator>[] {
return [
{
accessorKey: 'code',
@@ -28,11 +29,13 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Prevalidator>[]
},
{
id: 'actions',
Header: 'Acciones',
header: 'Acciones',
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
});
}
}

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Prevalidator;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -60,18 +64,22 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-red-600">
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -7,11 +7,24 @@
import { getSectors, type Sector } from '$lib/api/dashboard/general_catalogs/sectors';
import { Loader2, Search } from 'lucide-svelte';
import Badge from '$lib/components/ui/badge/badge.svelte';
import { companyStore } from '$lib/stores/company.svelte';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import { currentUser, userHasPermission } from '$lib/auth';
let { title = 'Sectores' }: { title?: string } = $props();
let sectors = $state<Sector[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
let status = $state<number>(200);
// Permisos
const canView = $derived(userHasPermission($currentUser, 'cat_sectors.view') || userHasPermission($currentUser, 'frac_sectors.view'));
const canCreate = $derived(userHasPermission($currentUser, 'cat_sectors.create'));
const canEdit = $derived(userHasPermission($currentUser, 'cat_sectors.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'cat_sectors.delete'));
const isError = $derived(!canView || status >= 400 || error);
let searchTerm = $state('');
let page = $state(1);
let pageSize = 50;
@@ -19,11 +32,15 @@
let total = $state(0);
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let sentinel = $state<HTMLDivElement | null>(null);
async function loadSectors(reset = false) {
const companyId = companyStore.activeCompany?.id;
if (!companyId || !canView) return;
if (loading || (!hasMore && !reset)) return;
loading = true;
error = null;
if (reset) {
page = 1;
@@ -34,21 +51,28 @@
}
try {
const response = await getSectors(page, pageSize, searchTerm || undefined);
const response = await getSectors(page, pageSize, companyId, searchTerm || undefined);
const newItems = response.items || [];
if (reset) {
sectors = newItems;
if (response.data) {
status = 200;
const newItems = response.data.items || [];
if (reset) {
sectors = newItems;
} else {
sectors = [...sectors, ...newItems];
}
total = response.data.total;
hasMore = newItems.length === pageSize && sectors.length < total;
} else {
sectors = [...sectors, ...newItems];
error = response.error || 'Failed to load';
status = response.status || 500;
hasMore = false;
}
total = response.total;
// Safer end-of-data detection
hasMore = newItems.length === pageSize && sectors.length < total;
} catch (error) {
console.error('Error loading sectors:', error);
toast.error('Error al cargar sectores');
} catch (err: any) {
console.error('Error loading sectors:', err);
error = err.message || 'Error al cargar sectores';
status = err.status || 500;
hasMore = false;
} finally {
loading = false;
@@ -88,9 +112,12 @@
if (sentinel) observer.observe(sentinel);
}
// Initial load
// Initial load and reload when company changes
$effect(() => {
untrack(() => loadSectors(true));
const id = companyStore.activeCompany?.id;
if (id) {
untrack(() => loadSectors(true));
}
});
// Setup observer only when sentinel is available
@@ -104,87 +131,106 @@
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-sm text-muted-foreground">
Gestiona los sectores de la tarifa.
</p>
</div>
</div>
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Sectores</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="h-9 w-44 bg-card pl-8 lg:w-64"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
{#if isError}
<ErrorState
status={!canView ? 403 : status}
error={!canView ? 'No tienes permiso para ver este catálogo' : (error || '')}
onRetry={() => loadSectors(true)}
/>
{:else}
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
<Card.Header>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por clave o descripción..."
class="h-9 w-44 bg-card pl-8 lg:w-64"
bind:value={searchTerm}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-0 flex-1 overflow-auto">
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row inTabOrder={false}>
<Table.Head class="catalog-table-head-cell w-[100px]">Clave</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">Estatus</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if loading && page === 1}
</Card.Header>
<Card.Content class="min-h-0 p-0 overflow-hidden flex-1 flex flex-col">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden border-none text-nowrap">
<div class="min-h-0 flex-1 overflow-auto">
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
<Table.Head class="catalog-table-head-cell w-[100px]">Clave</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">Estatus</Table.Head>
</Table.Row>
{:else if sectors.length === 0}
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-24 text-center">
No se encontraron sectores.
</Table.Cell>
</Table.Row>
{:else}
{#each sectors as sector}
<Table.Row inTabOrder={false} class="catalog-table-row">
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
<Table.Cell>{sector.description}</Table.Cell>
<Table.Cell class="text-right">
{#if sector.authorized}
<Badge variant="default">Autorizado</Badge>
{:else}
<Badge variant="secondary">No Autorizado</Badge>
{/if}
</Table.Header>
<Table.Body>
{#if loading && page === 1}
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading && page > 1}
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-12 text-center">
<div class="flex justify-center">
<Loader2 class="h-4 w-4 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
{:else if sectors.length === 0}
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-24 text-center">
No se encontraron sectores.
</Table.Cell>
</Table.Row>
{:else}
{#each sectors as sector}
<Table.Row inTabOrder={false} class="catalog-table-row">
<Table.Cell class="font-medium">{sector.key}</Table.Cell>
<Table.Cell>{sector.description}</Table.Cell>
<Table.Cell class="text-right">
{#if sector.authorized}
<Badge variant="default">Autorizado</Badge>
{:else}
<Badge variant="secondary">No Autorizado</Badge>
{/if}
</Table.Cell>
</Table.Row>
{/each}
{/if}
{#if loading && page > 1}
<Table.Row inTabOrder={false}>
<Table.Cell colspan={3} class="h-12 text-center">
<div class="flex justify-center">
<Loader2 class="h-4 w-4 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {sectors.length} de {total} registros</div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {sectors.length} de {total} registros</div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
<!-- Infinite Scroll Sentinel -->
<div bind:this={sentinel} class="h-4 w-full"></div>
{/if}
</div>

View File

@@ -3,7 +3,10 @@ import type { Signature } from '$lib/api/dashboard/a76/general_catalogs/signatur
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Signature>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<Signature>[] {
return [
{
accessorKey: 'code',
@@ -25,7 +28,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Signature>[] {
header: 'Acciones',
cell: ({ row }) => renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
})
}
];

View File

@@ -9,10 +9,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Signature;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -67,19 +71,23 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -2,9 +2,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import type { UnitConversion } from '$lib/api/dashboard/a76/general_catalogs/unit-conversions';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
import { Header } from '$lib/components/ui/alert-dialog';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<UnitConversion>[] {
return [
{
accessorKey: 'from_unit_code',
@@ -24,7 +25,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitConversion>
cell: ({ row }) =>
renderComponent(DataTableActions, {
conversion: row.original,
onSuccess
onSuccess,
canEdit,
canDelete
})
}
];

View File

@@ -9,10 +9,14 @@
let {
conversion,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
conversion: UnitConversion;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -58,19 +62,23 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureACE>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<UnitOfMeasureACE>[] {
return [
{
accessorKey: 'code',
@@ -21,7 +24,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAC
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: UnitOfMeasureACE;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -59,19 +63,23 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => dialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={handleDelete} disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 class="mr-2 h-4 w-4" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,10 @@ import type { UnitOfMeasureAmerican } from "$lib/api/dashboard/a76/general_catal
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAmerican>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<UnitOfMeasureAmerican>[] {
return [
{
accessorKey: "code",
@@ -16,11 +19,14 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureAm
{
id: "actions",
header: "Acciones",
cell: ({ row }) =>
renderComponent(DataTableActions, {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
},
},
];
}

View File

@@ -10,9 +10,11 @@
interface Props {
unit: UnitOfMeasureAmerican;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
}
let { unit, onSuccess }: Props = $props();
let { unit, onSuccess, canEdit = true, canDelete = true }: Props = $props();
let dialogOpen = $state(false);
async function handleDelete() {
@@ -42,15 +44,19 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete}>
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => (dialogOpen = true)}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive focus:text-destructive">
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -3,7 +3,10 @@ import type { UnitOfMeasureCustoms } from "$lib/api/dashboard/a76/general_catalo
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureCustoms>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<UnitOfMeasureCustoms>[] {
return [
{
accessorKey: "code",
@@ -20,11 +23,14 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureCu
{
id: "actions",
header: "Acciones",
cell: ({ row }) =>
renderComponent(DataTableActions, {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
},
];
}

View File

@@ -3,7 +3,10 @@ import type { UnitOfMeasureGeneral } from '$lib/api/dashboard/a76/general_catalo
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureGeneral>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<UnitOfMeasureGeneral>[] {
return [
{
accessorKey: 'code',
@@ -21,7 +24,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureGe
cell: ({ row }) => {
return renderComponent(DataTableActions, {
unit: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -3,7 +3,10 @@ import type { UnitOfMeasureOMA } from "$lib/api/dashboard/a76/general_catalogs/u
import { renderComponent } from "$lib/components/ui/data-table/index.js";
import DataTableActions from "./data-table-actions.svelte";
export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureOMA>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<UnitOfMeasureOMA>[] {
return [
{
accessorKey: "code",
@@ -16,11 +19,14 @@ export function createColumns(onSuccess?: () => void): ColumnDef<UnitOfMeasureOM
{
id: "actions",
header: "Acciones",
cell: ({ row }) =>
renderComponent(DataTableActions, {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
unit: row.original,
onSuccess
}),
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
},
];
}

View File

@@ -281,7 +281,7 @@
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
const response = await materialTypesApi.list(1, 100, 'ACTIVO FIJO');
const response = await materialTypesApi.list(companyId, 1, 100, 'ACTIVO FIJO');
if (response.data) {
materialTypes = response.data.items;
}

View File

@@ -5,15 +5,24 @@
deleteCanadianFraction,
type CanadianFraction
} from '$lib/api/dashboard/general_catalogs/canadian';
import * as Table from '$lib/components/ui/table';
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '$lib/components/ui/table';
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import CanadianFractionDialog from './CanadianFractionDialog.svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { currentUser, userHasPermission } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
let fractions = $state<CanadianFraction[]>([]);
let loading = $state(false);
@@ -22,11 +31,13 @@
let totalItems = $state(0);
let totalPages = $state(0);
let pageSize = 50;
let error = $state<string | null>(null);
let status = $state<number>(200);
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
let sentinel = $state<HTMLDivElement | null>(null);
let scrollContainer = $state<HTMLDivElement | null>(null);
// Infinite scroll state
let hasMore = $state(true);
@@ -35,6 +46,16 @@
let dialogOpen = $state(false);
let editingFraction = $state<CanadianFraction | null>(null);
let deletingFractionId = $state<number | null>(null);
let showDeleteConfirm = $state(false);
let fractionToDelete = $state<CanadianFraction | null>(null);
// Permissions
const canView = $derived(userHasPermission($currentUser, 'frac_canadian.view'));
const canCreate = $derived(userHasPermission($currentUser, 'frac_canadian.create'));
const canEdit = $derived(userHasPermission($currentUser, 'frac_canadian.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'frac_canadian.delete'));
const isError = $derived(!canView || status >= 400 || error);
async function loadFractions(reset = false) {
const companyId = companyStore.activeCompany?.id;
@@ -42,6 +63,7 @@
if (loading) return;
loading = true;
error = null;
if (reset) {
page = 1;
@@ -71,19 +93,16 @@
// Safer end-of-data detection
hasMore = newItems.length === pageSize && fractions.length < totalItems;
} catch (error) {
console.error('Error loading Canadian fractions:', error);
toast.error('Error al cargar fracciones canadienses');
} catch (err: any) {
console.error('Error loading Canadian fractions:', err);
error = err.message || 'Error al cargar fracciones canadienses';
status = err.status || 500;
hasMore = false;
} finally {
loading = false;
}
}
function handleSearch() {
loadFractions(true);
}
function handleSearchInput() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
@@ -94,7 +113,7 @@
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter') {
clearTimeout(searchTimeout);
handleSearch();
loadFractions(true);
}
}
@@ -108,22 +127,29 @@
dialogOpen = true;
}
async function handleDelete(fraction: CanadianFraction) {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
function confirmDelete(fraction: CanadianFraction) {
fractionToDelete = fraction;
showDeleteConfirm = true;
}
if (!confirm(`¿Estás seguro de eliminar la fracción ${fraction.fraction}?`)) return;
async function handleDelete() {
if (!fractionToDelete || !companyStore.activeCompany?.id) return;
try {
deletingFractionId = fraction.id;
await deleteCanadianFraction(companyId, fraction.id);
deletingFractionId = fractionToDelete.id;
await deleteCanadianFraction(companyStore.activeCompany.id, fractionToDelete.id);
toast.success('Fracción eliminada correctamente');
loadFractions(true);
} catch (error) {
console.error('Error deleting Canadian fraction:', error);
toast.error('Error al eliminar la fracción');
} catch (err: any) {
console.error('Error deleting Canadian fraction:', err);
const msg = err.status === 403
? 'No tienes permiso para eliminar este registro'
: 'Error al eliminar la fracción';
toast.error(msg);
} finally {
deletingFractionId = null;
showDeleteConfirm = false;
fractionToDelete = null;
}
}
@@ -146,8 +172,6 @@
if (sentinel) observer.observe(sentinel);
}
// Removed onMount as we use $effect for company changes which covers initial load
$effect(() => {
const companyId = companyStore.activeCompany?.id;
if (companyId) {
@@ -164,103 +188,151 @@
});
</script>
<div class="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden">
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Canadienses</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar por fracción o descripción..."
class="h-9 w-44 bg-card pl-9 lg:w-64"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">Listado de Fracciones Canadienses</h1>
<p class="text-sm text-muted-foreground">
Gestiona las fracciones arancelarias de la tarifa canadiense.
</p>
</div>
<div class="flex items-center gap-3">
{#if !isError && canCreate}
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
</div>
</div>
{#if isError}
<ErrorState
status={!canView ? 403 : status}
error={!canView ? 'No tienes permiso para ver este catálogo' : (error || '')}
onRetry={() => loadFractions(true)}
/>
{:else}
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden shadow-sm">
<Card.Header>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar..."
class="h-9 w-44 bg-card pl-9 lg:w-64"
bind:value={searchQuery}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Descripción</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Unidad</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">ADV</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.fraction}</Table.Cell>
<Table.Cell>{fraction.description || '-'}</Table.Cell>
<TableCell>{fraction.country_code}</TableCell>
<TableCell>{fraction.unit_of_measure || '-'}</TableCell>
<TableCell class="text-right">{fraction.ad_valorem ?? '-'}</TableCell>
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</TableCell>
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={6} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</Table.Cell>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
<div bind:this={sentinel} class="h-4 w-full"></div>
</Card.Header>
<Card.Content class="min-h-0 p-0 overflow-hidden flex-1 flex flex-col">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden border-none">
<div class="flex-1 overflow-auto" bind:this={scrollContainer}>
<Table>
<TableHeader class="catalog-table-header sticky top-0 bg-background z-10">
<TableRow>
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
<TableHead class="catalog-table-head-cell">País</TableHead>
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
<TableHead class="catalog-table-head-cell text-right">ADV</TableHead>
{#if canEdit || canDelete}
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !loading}
<TableRow>
<TableCell colspan={canEdit || canDelete ? 6 : 5} class="h-24 text-center">No se encontraron resultados</TableCell>
</TableRow>
{:else}
{#each fractions as fraction}
<TableRow class="catalog-table-row">
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>{fraction.description || '-'}</TableCell>
<TableCell>{fraction.country_code}</TableCell>
<TableCell>{fraction.unit_of_measure || '-'}</TableCell>
<TableCell class="text-right">{fraction.ad_valorem ?? '-'}</TableCell>
{#if canEdit || canDelete}
<TableCell>
<div class="flex items-center gap-2">
{#if canEdit}
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
{/if}
{#if canDelete}
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
{/if}
</div>
</TableCell>
{/if}
</TableRow>
{/each}
{/if}
{#if loading}
<TableRow>
<TableCell colspan={canEdit || canDelete ? 6 : 5} class="h-24 text-center">
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalItems} registros</div>
<CanadianFractionDialog
bind:open={dialogOpen}
fraction={editingFraction}
onSuccess={handleSuccess}
/>
<div class="flex-none text-sm text-muted-foreground mt-2">Mostrando {fractions.length} de {totalItems} registros</div>
{/if}
</div>
<AlertDialog.Root bind:open={showDeleteConfirm}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
<AlertDialog.Description>
Esta acción no se puede deshacer. Se eliminará la fracción arancelaria canadiense permanentemente.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Cancelar</AlertDialog.Cancel>
<AlertDialog.Action
class="text-destructive-foreground bg-destructive hover:bg-destructive/90"
onclick={handleDelete}
>
Eliminar
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<CanadianFractionDialog
bind:open={dialogOpen}
fraction={editingFraction}
onSuccess={handleSuccess}
/>

View File

@@ -12,14 +12,24 @@
import { Search, Loader2, Plus, Pencil, Trash2 } from 'lucide-svelte';
import { toast } from 'svelte-sonner';
import HistoricalFractionDialog from './HistoricalFractionDialog.svelte';
import { currentUser, userHasPermission } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
import { companyStore } from '$lib/stores/company.svelte';
import { companyStore } from '$lib/stores/company.svelte';
let { title = 'Fracciones históricas' }: { title?: string } = $props();
let { title = 'Fracciones históricas' }: { title?: string } = $props();
let fractions = $state<HistoricalFraction[]>([]);
let loading = $state(false);
let historicalFraction = $state('');
// Permisos
const canView = $derived(userHasPermission($currentUser, 'frac_historical.view'));
const canCreate = $derived(userHasPermission($currentUser, 'frac_historical.create'));
const canEdit = $derived(userHasPermission($currentUser, 'frac_historical.edit'));
const canDelete = $derived(userHasPermission($currentUser, 'frac_historical.delete'));
let fractions = $state<HistoricalFraction[]>([]);
let loading = $state(false);
let error = $state<string | null>(null);
let status = $state<number>(200);
let historicalFraction = $state('');
let page = $state(1);
let totalItems = $state(0);
let totalPages = $state(0);
@@ -27,8 +37,8 @@ let pageSize = 50;
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
let sentinel = $state<HTMLDivElement | null>(null);
let scrollContainer = $state<HTMLDivElement | null>(null);
// Infinite scroll state
let hasMore = $state(true);
@@ -172,7 +182,6 @@ let scrollContainer: HTMLDivElement;
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
@@ -180,88 +189,101 @@ let scrollContainer: HTMLDivElement;
Gestiona las fracciones históricas de la tarifa.
</p>
</div>
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{#if canView && canCreate}
<Button class="h-9" onclick={handleCreate}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
</div>
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones Históricas</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="h-9 w-40 bg-card pl-9 lg:w-56"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
{#if !canView}
<ErrorState status={403} error="No tienes permiso para ver este catálogo" onRetry={() => loadFractions(true)} />
{:else}
<Card.Root class="flex min-h-0 flex-1 flex-col border bg-background">
<Card.Header>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
id="search-fraction"
type="text"
placeholder="Buscar fracción..."
class="h-9 w-40 bg-card pl-9 lg:w-56"
bind:value={historicalFraction}
oninput={handleSearchInput}
onkeydown={handleKeyDown}
/>
</div>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Tipo</Table.Head>
<Table.Head class="catalog-table-head-cell">UM</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Pub.</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Fin</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGI</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGE</Table.Head>
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-y-auto" bind:this={scrollContainer}>
<Table.Root>
<Table.Header class="catalog-table-header">
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">No se encontraron resultados</Table.Cell>
<Table.Head class="catalog-table-head-cell">Fracción</Table.Head>
<Table.Head class="catalog-table-head-cell">Tipo</Table.Head>
<Table.Head class="catalog-table-head-cell">UM</Table.Head>
<Table.Head class="catalog-table-head-cell">País</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Pub.</Table.Head>
<Table.Head class="catalog-table-head-cell">Fecha Fin</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGI</Table.Head>
<Table.Head class="catalog-table-head-cell text-right">IGE</Table.Head>
{#if canEdit || canDelete}
<Table.Head class="catalog-table-head-cell w-[100px]">Acciones</Table.Head>
{/if}
</Table.Row>
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell>{fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell>{fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
<Table.Cell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
</div>
</Table.Cell>
</Table.Header>
<Table.Body>
{#if fractions.length === 0 && !loading}
<Table.Row>
<Table.Cell colspan={canEdit || canDelete ? 9 : 8} class="h-24 text-center">No se encontraron resultados</Table.Cell>
</Table.Row>
{/each}
{/if}
{:else}
{#each fractions as fraction}
<Table.Row class="catalog-table-row">
<Table.Cell class="font-medium">{fraction.historical_fraction}</Table.Cell>
<Table.Cell>{fraction.fraction_type || '-'}</Table.Cell>
<Table.Cell>{fraction.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{fraction.country || '-'}</Table.Cell>
<Table.Cell>{fraction.publication_date ? new Date(fraction.publication_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell>{fraction.end_date ? new Date(fraction.end_date).toLocaleDateString() : '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.import_tax_rate ?? '-'}</Table.Cell>
<Table.Cell class="text-right">{fraction.export_tax_rate ?? '-'}</Table.Cell>
{#if canEdit || canDelete}
<Table.Cell>
<div class="flex items-center gap-2">
{#if canEdit}
<Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => handleEdit(fraction)}>
<Pencil class="h-4 w-4" />
</Button>
{/if}
{#if canDelete}
<Button
variant="ghost"
size="icon"
class="h-8 w-8 text-destructive hover:text-destructive"
onclick={() => handleDelete(fraction)}
disabled={deletingFractionId === fraction.id}
>
{#if deletingFractionId === fraction.id}
<Loader2 class="h-4 w-4 animate-spin" />
{:else}
<Trash2 class="h-4 w-4" />
{/if}
</Button>
{/if}
</div>
</Table.Cell>
{/if}
</Table.Row>
{/each}
{/if}
{#if loading}
<Table.Row>
<Table.Cell colspan={9} class="h-24 text-center">
@@ -286,4 +308,5 @@ let scrollContainer: HTMLDivElement;
fraction={editingFraction}
onSuccess={handleSuccess}
/>
{/if}
</div>

View File

@@ -21,17 +21,21 @@
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import TariffFractionFormDialog from './TariffFractionFormDialog.svelte';
import { toast } from 'svelte-sonner';
import { currentUser, userHasPermission } from '$lib/auth';
import ErrorState from '$lib/components/dashboard/common/error-state.svelte';
let {
title = 'Fracciones Arancelarias',
catalog = 'mex', // 'mex' or 'usa'
levelFilter = null, // null or number
readOnly = false
readOnly = false,
basePerm: customBasePerm = null
}: {
title?: string;
catalog?: string;
levelFilter?: number | null;
readOnly?: boolean;
basePerm?: string | null;
} = $props();
let fractions = $state<TariffFraction[]>([]);
@@ -39,11 +43,31 @@
let currentPage = $state(1);
let pageSize = 50;
let isLoading = $state(false);
let error = $state<string | null>(null);
let status = $state<number>(200);
// Permisos
const permMap: Record<string, string> = {
'mex': 'frac_sitar',
'usa': 'frac_sitar_us',
'american': 'frac_american',
'canadian': 'frac_canadian',
'historical': 'frac_historical'
};
const basePerm = $derived(customBasePerm || permMap[catalog] || 'frac_sitar');
const canView = $derived(userHasPermission($currentUser, `${basePerm}.view`));
const canCreate = $derived(userHasPermission($currentUser, `${basePerm}.create`));
const canEdit = $derived(userHasPermission($currentUser, `${basePerm}.edit`));
const canDelete = $derived(userHasPermission($currentUser, `${basePerm}.delete`));
const isError = $derived(!canView || status >= 400 || error);
let search = $state('');
let searchTimeout: ReturnType<typeof setTimeout>;
let observer: IntersectionObserver;
let sentinel: HTMLDivElement;
let scrollContainer: HTMLDivElement;
let sentinel = $state<HTMLDivElement | null>(null);
let scrollContainer = $state<HTMLDivElement | null>(null);
// Infinite scroll state
let hasMore = $state(true);
@@ -62,6 +86,7 @@
if (isLoading) return;
isLoading = true;
error = null;
if (reset) {
currentPage = 1;
@@ -76,25 +101,31 @@
filters.catalog = catalog;
const response = await getTariffFractions(currentPage, pageSize, companyId, filters);
const payload = (response.data || response) as any;
if (response.data) {
const newItems = response.data.items || [];
if (payload?.items) {
const newItems = payload.items || [];
if (reset) {
fractions = newItems;
} else {
fractions = [...fractions, ...newItems];
}
totalFractions = response.data.total;
totalFractions = payload.total || 0;
// Safer end-of-data detection
hasMore = newItems.length === pageSize && fractions.length < totalFractions;
} else {
if (reset) fractions = [];
hasMore = false;
if (response.error) {
error = response.error;
status = (response as any).status || 500;
}
}
} catch (error) {
console.error('Error loading fractions:', error);
toast.error('Error al cargar las fracciones');
} catch (err: any) {
console.error('Error loading fractions:', err);
error = err.message || 'Error al cargar las fracciones';
status = err.status || 500;
hasMore = false;
} finally {
isLoading = false;
@@ -149,20 +180,20 @@
if (!fractionToDelete || !companyStore.activeCompany?.id) return;
try {
// Note: Delete might allow deleting items from source API if allowed,
// or just local overrides. Assuming Service handles logic.
await deleteTariffFraction(fractionToDelete.id, companyStore.activeCompany.id, catalog);
toast.success('Fracción eliminada correctamente');
loadFractions(true);
} catch (error) {
} catch (error: any) {
console.error('Error deleting fraction:', error);
toast.error('Error al eliminar la fracción. Puede que esté en uso.');
const msg = error.status === 403
? 'No tienes permiso para eliminar este registro'
: 'Error al eliminar la fracción. Puede que esté en uso.';
toast.error(msg);
} finally {
showDeleteConfirm = false;
fractionToDelete = null;
}
}
// Removed onMount as we use $effect for company changes which covers initial load
// Reload when company changes
$effect(() => {
@@ -182,117 +213,142 @@
</script>
<div class="flex flex-col gap-6 p-6 h-[calc(100svh-4rem)] group-has-data-[collapsible=icon]/sidebar-wrapper:h-[calc(100svh-3rem)] overflow-hidden">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
{#if !readOnly}
<Button class="h-9" onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
<div class="flex items-center justify-between gap-4">
<div class="space-y-1">
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
<p class="text-sm text-muted-foreground">
Gestiona las fracciones arancelarias de la tarifa.
</p>
</div>
<div class="flex items-center gap-3">
{#if !isError && !readOnly && canCreate}
<Button class="h-9" onclick={openCreateDialog}>
<Plus class="mr-2 h-4 w-4" />
Nueva Fracción
</Button>
{/if}
</div>
</div>
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col">
<Card.Header>
<div class="flex flex-wrap items-center justify-between gap-3">
<Card.Title>Listado de Fracciones</Card.Title>
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="h-9 w-40 bg-card pl-8 lg:w-56" bind:value={search} oninput={handleSearchInput} />
{#if isError}
<ErrorState
status={!canView ? 403 : status}
error={!canView ? 'No tienes permiso para ver este catálogo' : (error || '')}
onRetry={() => loadFractions(true)}
/>
{:else}
{#if error}
<div class="flex-none rounded-md border border-destructive px-4 py-3 text-sm text-destructive">
{error}
</div>
{/if}
<Card.Root class="border bg-background flex min-h-0 flex-1 flex-col overflow-hidden">
<Card.Header>
<div class="flex flex-wrap items-center justify-end gap-3">
<div class="flex flex-wrap items-center gap-2">
<div class="relative">
<Search class="absolute top-2.5 left-2 h-4 w-4 text-muted-foreground" />
<Input placeholder="Buscar..." class="h-9 w-40 bg-card pl-8 lg:w-56" bind:value={search} oninput={handleSearchInput} />
</div>
</div>
</div>
</div>
</Card.Header>
<Card.Content class="min-h-0 p-0">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden">
<div class="min-h-[360px] max-h-[calc(100svh-340px)] flex-1 overflow-auto" bind:this={scrollContainer}>
<Table>
<TableHeader class="catalog-table-header">
<TableRow>
<TableHead class="catalog-table-head-cell">Clave</TableHead>
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
{#if catalog === 'mex'}
<TableHead class="catalog-table-head-cell">NICO</TableHead>
<TableHead class="catalog-table-head-cell">U.M.T</TableHead>
{:else}
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
{/if}
<TableHead class="catalog-table-head-cell">Adv. Impo</TableHead>
<TableHead class="catalog-table-head-cell">Adv. Expo</TableHead>
{#if !readOnly}
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
</Card.Header>
<Card.Content class="min-h-0 p-0 overflow-hidden flex-1 flex flex-col">
<div class="catalog-table-shell flex min-h-0 flex-1 flex-col overflow-hidden border-none">
<div class="flex-1 overflow-auto" bind:this={scrollContainer}>
<Table>
<TableHeader class="catalog-table-header sticky top-0 bg-background z-10">
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
<TableHead class="catalog-table-head-cell">Clave</TableHead>
<TableHead class="catalog-table-head-cell">Fracción</TableHead>
<TableHead class="catalog-table-head-cell">Descripción</TableHead>
{#if catalog === 'mex'}
<TableHead class="catalog-table-head-cell">NICO</TableHead>
<TableHead class="catalog-table-head-cell">U.M.T</TableHead>
{:else if catalog === 'usa' || catalog === 'american' || catalog === 'canadian'}
<TableHead class="catalog-table-head-cell">Unidad</TableHead>
{/if}
<TableHead class="catalog-table-head-cell">Adv. Impo</TableHead>
<TableHead class="catalog-table-head-cell">Adv. Expo</TableHead>
{#if !readOnly && (canEdit || canDelete)}
<TableHead class="catalog-table-head-cell w-[100px]">Acciones</TableHead>
{/if}
</TableRow>
{:else}
{#each fractions as fraction}
<TableRow class="catalog-table-row">
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableHeader>
<TableBody>
{#if fractions.length === 0 && !isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
No se encontraron resultados
</TableCell>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
{:else}
<TableCell>{fraction.umt || '-'}</TableCell>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
{#if !readOnly}
<TableCell>
<div class="flex items-center gap-2">
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
<div bind:this={sentinel} class="h-4 w-full"></div>
{:else}
{#each fractions as fraction (fraction.id)}
<TableRow class="catalog-table-row">
<TableCell class="font-mono">{fraction.um_code || fraction.code}</TableCell>
<TableCell class="font-medium">{fraction.fraction}</TableCell>
<TableCell class="max-w-md truncate" title={fraction.description}>
{fraction.description}
</TableCell>
{#if catalog === 'mex'}
<TableCell>{fraction.nico || '-'}</TableCell>
<TableCell>{fraction.umt || '-'}</TableCell>
{:else if catalog === 'usa' || catalog === 'american' || catalog === 'canadian'}
<TableCell>{fraction.umt || '-'}</TableCell>
{/if}
<TableCell>{fraction.adv_impo || '-'}</TableCell>
<TableCell>{fraction.adv_expo || '-'}</TableCell>
{#if (!readOnly && (canEdit || canDelete))}
<TableCell>
<div class="flex items-center gap-2">
{#if canEdit}
<Button variant="ghost" size="icon" onclick={() => openEditDialog(fraction)}>
<Edit class="h-4 w-4" />
</Button>
{/if}
{#if canDelete}
<Button
variant="ghost"
size="icon"
class="text-destructive hover:text-destructive"
onclick={() => confirmDelete(fraction)}
>
<Trash2 class="h-4 w-4" />
</Button>
{/if}
</div>
</TableCell>
{/if}
</TableRow>
{/each}
{/if}
{#if isLoading}
<TableRow>
<TableCell
colspan={catalog === 'mex' ? (readOnly ? 7 : 8) : readOnly ? 6 : 7}
class="h-24 text-center"
>
<div class="flex justify-center">
<Loader2 class="h-6 w-6 animate-spin" />
</div>
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
<div bind:this={sentinel} class="h-4 w-full"></div>
</div>
</div>
</div>
</Card.Content>
</Card.Root>
</Card.Content>
</Card.Root>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
<div class="flex-none text-sm text-muted-foreground">Mostrando {fractions.length} de {totalFractions} registros</div>
{/if}
</div>
<AlertDialog.Root bind:open={showDeleteConfirm}>

View File

@@ -5,6 +5,7 @@
import * as Table from "$lib/components/ui/table";
import { Search, Loader2, Globe } from "lucide-svelte";
import { countriesApi, type Country } from "$lib/api/dashboard/reference_data/countries";
import { companyStore } from "$lib/stores/company.svelte";
import { toast } from "svelte-sonner";
// --- PROPS ---
@@ -96,7 +97,15 @@
}
try {
const response = await countriesApi.list(page, pageSize, searchTerm);
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
toast.error("No hay una empresa activa seleccionada");
loading = false;
loadingMore = false;
return;
}
const response = await countriesApi.list(companyId, page, pageSize, searchTerm);
if (response.error) {
toast.error(`Error: ${response.error}`);

View File

@@ -5,6 +5,7 @@
import { Search, Loader2, Layers, Tag, Box } from 'lucide-svelte';
// Importamos la interfaz corregida
import { materialTypesApi, type MaterialType } from '$lib/api/dashboard/a76/material-types';
import { companyStore } from '$lib/stores/company.svelte';
// --- PROPS ---
let {
@@ -34,9 +35,12 @@
});
async function loadMaterials() {
const companyId = companyStore.activeCompany?.id;
if (!companyId) return;
loading = true;
try {
const res = await materialTypesApi.list(1, 100);
const res = await materialTypesApi.list(companyId, 1, 100);
const responseData = (res as any).data || res;

View File

@@ -3,6 +3,9 @@
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Loader2, Search } from 'lucide-svelte';
import { countriesApi } from '$lib/api/dashboard/reference_data/countries';
import { companyStore } from '$lib/stores/company.svelte';
import { toast } from 'svelte-sonner';
let {
open = $bindable(),
@@ -22,27 +25,27 @@
loading = true;
error = '';
try {
const response = await fetch('/api-sveltekit/countries', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
if (Array.isArray(data)) {
countries = data;
} else if (data.items && Array.isArray(data.items)) {
countries = data.items;
} else {
console.error('Unexpected data format:', data);
countries = [];
}
filteredCountries = countries;
} else {
error = `Error: ${response.status} - ${response.statusText}`;
console.error('Error response:', await response.text());
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
error = 'No hay una empresa activa seleccionada';
return;
}
// Para este diálogo, cargamos una cantidad grande o implementamos paginación si fuera necesario
// Por ahora seguimos el patrón original de cargar "todos" (limite 100 en backend)
const response = await countriesApi.list(companyId, 1, 100);
if (response.data) {
countries = response.data.items || [];
filteredCountries = countries;
} else if (response.error) {
error = `Error: ${response.error}`;
toast.error(error);
}
} catch (err) {
error = 'Error loading countries';
console.error('Error loading countries:', err);
toast.error(error);
} finally {
loading = false;
}

View File

@@ -6,7 +6,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Package>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Package>[] {
return [
{
accessorKey: 'key',
@@ -63,7 +66,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Package>[] {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Package;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let loading = $state(false);
@@ -79,7 +83,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -90,24 +94,30 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={handleEdit}>
<Pencil size={16} class="mr-2" />
Editar
</DropdownMenu.Item>
{/if}
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{#if canDelete}
<DropdownMenu.Item onclick={handleDelete} class="text-destructive" disabled={loading}>
{#if loading}
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<Trash2 size={16} class="mr-2" />
{/if}
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={dialogOpen}
item={selectedItem}
onSuccess={handleDialogSuccess}
/>
{#if canEdit}
<CreateEditDialog
bind:open={dialogOpen}
item={selectedItem}
onSuccess={handleDialogSuccess}
/>
{/if}

View File

@@ -3,7 +3,10 @@ import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent } from '$lib/components/ui/data-table';
import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<Port>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Port>[] {
return [
{
accessorKey: 'port_code',
@@ -40,7 +43,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Port>[] {
cell: ({ row }) => {
return renderComponent(DataTableActions, {
item: row.original,
onSuccess
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}

View File

@@ -8,10 +8,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Port;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let editDialogOpen = $state(false);
@@ -19,7 +23,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -29,26 +33,34 @@
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={() => editDialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => deleteDialogOpen = true} class="text-destructive">
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
{#if canEdit}
<DropdownMenu.Item onclick={() => editDialogOpen = true}>
<Pencil class="mr-2 h-4 w-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => deleteDialogOpen = true} class="text-destructive">
<Trash2 class="mr-2 h-4 w-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<CreateEditDialog
bind:open={editDialogOpen}
mode="edit"
{item}
{onSuccess}
/>
{#if canEdit}
<CreateEditDialog
bind:open={editDialogOpen}
mode="edit"
{item}
{onSuccess}
/>
{/if}
<DeleteDialog
bind:open={deleteDialogOpen}
{item}
{onSuccess}
/>
{#if canDelete}
<DeleteDialog
bind:open={deleteDialogOpen}
{item}
{onSuccess}
/>
{/if}

View File

@@ -10,7 +10,10 @@ export type CodePedimentoRegimen = {
type_code: string | null;
};
export function createColumns(onSuccess?: () => void): ColumnDef<CodePedimentoRegimen>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<CodePedimentoRegimen>[] {
return [
{
accessorKey: "pedimento_code",
@@ -67,7 +70,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CodePedimentoRe
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,16 +1,24 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CodePedimentoRegimen } from "./columns.js";
import DetailsDialog from "./details-dialog.svelte";
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import Copy from '@lucide/svelte/icons/copy';
import Eye from '@lucide/svelte/icons/eye';
import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import type { CodePedimentoRegimen } from './columns.js';
import DetailsDialog from './details-dialog.svelte';
let {
let {
item,
onSuccess
}: {
onSuccess,
canEdit = true,
canDelete = true
}: {
item: CodePedimentoRegimen;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -37,13 +45,38 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar ID
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleViewDetails}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item
onclick={() => alert('Módulo de edición no disponible para Catálogos Públicos')}
>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item
onclick={() => alert('Módulo de eliminación no disponible para Catálogos Públicos')}
class="text-destructive"
>
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />

View File

@@ -8,7 +8,10 @@ export type Container = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<Container>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Container>[] {
return [
{
accessorKey: "key",
@@ -40,11 +43,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Container>[] {
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,16 +1,24 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Container } from "./columns.js";
import DetailsDialog from "./details-dialog.svelte";
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import Copy from '@lucide/svelte/icons/copy';
import Eye from '@lucide/svelte/icons/eye';
import Pencil from '@lucide/svelte/icons/pencil';
import Trash2 from '@lucide/svelte/icons/trash-2';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import type { Container } from './columns.js';
import DetailsDialog from './details-dialog.svelte';
let {
let {
item,
onSuccess
}: {
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Container;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,10 +26,6 @@
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
@@ -37,13 +41,38 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
Copiar ID
<Copy class="mr-2 size-4" />
Copiar Código
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item
onclick={() => alert('Módulo de edición no disponible para Catálogos Públicos')}
>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item
onclick={() => alert('Módulo de eliminación no disponible para Catálogos Públicos')}
class="text-destructive"
>
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />

View File

@@ -11,7 +11,10 @@ export type Country = {
description_en: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<Country>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Country>[] {
return [
{
accessorKey: "m3_key",
@@ -84,11 +87,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Country>[] {
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Country } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Country;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.m3_key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
Copiar ID
<Copy class="mr-2 size-4" />
Copiar Clave M3
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -9,7 +9,10 @@ export type CurrencyType = {
country_description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<CurrencyType>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<CurrencyType>[] {
return [
{
accessorKey: "code",
@@ -54,11 +57,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CurrencyType>[]
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CurrencyType } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: CurrencyType;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Código
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,7 +8,10 @@ export type CustomsSection = {
section_name: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsSection>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<CustomsSection>[] {
return [
{
accessorKey: "customs_code",
@@ -40,11 +43,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsSection>
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,31 +1,39 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import Eye from "@lucide/svelte/icons/eye";
import Copy from "@lucide/svelte/icons/copy";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CustomsSection } from "./columns.js";
import DetailsDialog from "./details-dialog.svelte";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: CustomsSection;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.customs_code.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,13 +45,42 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Código
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (showEditDialog = true)}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (showDeleteDialog = true)} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
{#if canEdit}
<CreateEditDialog bind:open={showEditDialog} {item} {onSuccess} />
{/if}
{#if canDelete}
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />
{/if}

View File

@@ -9,7 +9,10 @@ export type CustomsWarehouse = {
fiscalized_warehouse: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsWarehouse>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<CustomsWarehouse>[] {
return [
{
accessorKey: "key",
@@ -54,7 +57,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsWarehous
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit,
canDelete
});
}
}
];

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { CustomsWarehouse } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: CustomsWarehouse;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -25,7 +33,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +45,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleViewDetails}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -9,7 +9,10 @@ export type Incoterm = {
description_en: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<Incoterm>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<Incoterm>[] {
return [
{
accessorKey: "code",
@@ -54,7 +57,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Incoterm>[] {
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit,
canDelete
});
}
}
];

View File

@@ -1,5 +1,8 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Incoterm } from "./columns.js";
@@ -7,16 +10,20 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Incoterm;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
navigator.clipboard.writeText(item.code);
}
function handleViewDetails() {
@@ -25,7 +32,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +44,31 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
Copiar Código
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleViewDetails}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -10,7 +10,10 @@ export type InvoiceType = {
type?: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<InvoiceType>[] {
export function createColumns(
onSuccess?: () => void,
{ canEdit = true, canDelete = true }: { canEdit?: boolean; canDelete?: boolean } = {}
): ColumnDef<InvoiceType>[] {
return [
{
accessorKey: "key",
@@ -68,7 +71,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<InvoiceType>[]
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit,
canDelete
});
}
}
];

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { InvoiceType } from "./columns.js";
@@ -7,16 +11,20 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: InvoiceType;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
navigator.clipboard.writeText(item.key);
}
function handleViewDetails() {
@@ -25,7 +33,7 @@
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +45,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleViewDetails}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -9,7 +9,10 @@ export type MaterialType = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<MaterialType>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<MaterialType>[] {
return [
{
accessorKey: "key",
@@ -54,7 +57,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<MaterialType>[]
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { MaterialType } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: MaterialType;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,7 +8,10 @@ export type PaymentMethod = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<PaymentMethod>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<PaymentMethod>[] {
return [
{
accessorKey: "key",
@@ -40,7 +43,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<PaymentMethod>[
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PaymentMethod } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: PaymentMethod;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,7 +8,10 @@ export type PedimentoCode = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<PedimentoCode>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<PedimentoCode>[] {
return [
{
accessorKey: "code",
@@ -40,7 +43,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<PedimentoCode>[
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PedimentoCode } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: PedimentoCode;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Código
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,7 +8,10 @@ export type PedimentoRegimen = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<PedimentoRegimen>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<PedimentoRegimen>[] {
return [
{
accessorKey: "code",
@@ -40,11 +43,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<PedimentoRegime
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { PedimentoRegimen } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: PedimentoRegimen;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.code.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Código
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -12,7 +12,10 @@ export type Sector = {
tenant_id: number;
};
export function createColumns(onSuccess?: () => void): ColumnDef<Sector>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<Sector>[] {
return [
{
accessorKey: "key",
@@ -60,7 +63,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Sector>[] {
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,43 +1,35 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { Sector } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: Sector;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -49,18 +41,34 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -10,7 +10,10 @@ export type State = {
ame_key?: string | null;
};
export function createColumns(onSuccess?: () => void): ColumnDef<State>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<State>[] {
return [
{
accessorKey: "m3_key",
@@ -74,7 +77,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<State>[] {
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

View File

@@ -1,43 +1,35 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { State } from "./columns.js";
import CreateEditDialog from "./create-edit-dialog.svelte";
import DetailsDialog from "./details-dialog.svelte";
import DeleteDialog from "./delete-dialog.svelte";
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: State;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
function handleCopyId() {
navigator.clipboard.writeText(item.m3_key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
function handleEdit() {
showEditDialog = true;
}
function handleDelete() {
showDeleteDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -49,18 +41,34 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
Copiar Clave M3
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>Editar</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item class="text-destructive" onclick={handleDelete}>Eliminar</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => (alert('Módulo de edición no disponible para Catálogos Públicos'))}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => (alert('Módulo de eliminación no disponible para Catálogos Públicos'))} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
<!-- Dialogs -->
<DetailsDialog bind:open={showDetailsDialog} {item} />
<CreateEditDialog bind:open={showEditDialog} item={item} {onSuccess} />
<DeleteDialog bind:open={showDeleteDialog} {item} {onSuccess} />

View File

@@ -8,7 +8,10 @@ export type TransportMode = {
name: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<TransportMode>[] {
export function createColumns(
onSuccess: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<TransportMode>[] {
return [
{
accessorKey: "key",
@@ -40,11 +43,15 @@ export function createColumns(onSuccess?: () => void): ColumnDef<TransportMode>[
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];
}
// Mantener compatibilidad hacia atrás
export const columns = createColumns();
// Legacy export removed

View File

@@ -1,5 +1,9 @@
<script lang="ts">
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
import Copy from "@lucide/svelte/icons/copy";
import Eye from "@lucide/svelte/icons/eye";
import Pencil from "@lucide/svelte/icons/pencil";
import Trash2 from "@lucide/svelte/icons/trash-2";
import { Button } from "$lib/components/ui/button/index.js";
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
import type { TransportMode } from "./columns.js";
@@ -7,10 +11,14 @@
let {
item,
onSuccess
onSuccess,
canEdit = true,
canDelete = true
}: {
item: TransportMode;
onSuccess?: () => void;
canEdit?: boolean;
canDelete?: boolean;
} = $props();
let showDetailsDialog = $state(false);
@@ -18,14 +26,10 @@
function handleCopyId() {
navigator.clipboard.writeText(item.key.toString());
}
function handleViewDetails() {
showDetailsDialog = true;
}
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger disabled={!canEdit && !canDelete}>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="icon" class="relative size-8 p-0">
<span class="sr-only">Abrir menú</span>
@@ -37,11 +41,32 @@
<DropdownMenu.Group>
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Item onclick={handleCopyId}>
<Copy class="mr-2 size-4" />
Copiar Clave
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => (showDetailsDialog = true)}>
<Eye class="mr-2 size-4" />
Ver detalles
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleViewDetails}>Ver detalles</DropdownMenu.Item>
{#if canEdit || canDelete}
<DropdownMenu.Separator />
<DropdownMenu.Group>
{#if canEdit}
<DropdownMenu.Item onclick={() => {/* Edición no disponible por ahora para catálogos públicos en UI */ alert('Módulo de edición no disponible para Catálogos Públicos')}}>
<Pencil class="mr-2 size-4" />
Editar
</DropdownMenu.Item>
{/if}
{#if canDelete}
<DropdownMenu.Item onclick={() => {/* Eliminación no disponible por ahora para catálogos públicos en UI */ alert('Módulo de eliminación no disponible para Catálogos Públicos')}} class="text-destructive">
<Trash2 class="mr-2 size-4" />
Eliminar
</DropdownMenu.Item>
{/if}
</DropdownMenu.Group>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

@@ -8,7 +8,10 @@ export type TransportType = {
description: string;
};
export function createColumns(onSuccess?: () => void): ColumnDef<TransportType>[] {
export function createColumns(
onSuccess?: () => void,
permissions: { canEdit: boolean; canDelete: boolean } = { canEdit: true, canDelete: true }
): ColumnDef<TransportType>[] {
return [
{
accessorKey: "transport_code",
@@ -40,7 +43,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<TransportType>[
{
id: "actions",
cell: ({ row }) => {
return renderComponent(DataTableActions, { item: row.original, onSuccess });
return renderComponent(DataTableActions, {
item: row.original,
onSuccess,
canEdit: permissions.canEdit,
canDelete: permissions.canDelete
});
}
}
];

Some files were not shown because too many files have changed in this diff Show More