fix: solución de bloqueos y estandarización de permisos
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -37,6 +37,9 @@ export interface PortUpdate {
|
||||
export interface PortListResponse {
|
||||
items: Port[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
class PortsApi {
|
||||
|
||||
@@ -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 }>(
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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()}`);
|
||||
}
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`)
|
||||
};
|
||||
@@ -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),
|
||||
|
||||
@@ -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}`)
|
||||
};
|
||||
@@ -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}`)
|
||||
};
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
158
frontend/src/lib/components/dashboard/common/error-state.svelte
Normal file
158
frontend/src/lib/components/dashboard/common/error-state.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user