From c269f17febe7bdbffa438a161999f98d402e1e40 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Wed, 25 Mar 2026 12:52:32 -0600 Subject: [PATCH 1/2] feat(document-concepts): add CRUD functionality for document concepts and models --- src/components/layout/Sidebar.vue | 6 + .../DocumentConceptsForm.vue | 127 +++++++++++++++ .../DocumentConceptsTable.vue | 86 ++++++++++ .../DocumentConceptsView.vue | 150 ++++++++++++++++++ .../document-concepts.interface.ts | 64 ++++++++ .../document-concepts.services.ts | 79 +++++++++ .../document-models.interface.ts | 48 ++++++ .../document-models.services.ts | 41 +++++ src/router/index.ts | 10 ++ 9 files changed, 611 insertions(+) create mode 100644 src/modules/catalog/components/document-concepts/DocumentConceptsForm.vue create mode 100644 src/modules/catalog/components/document-concepts/DocumentConceptsTable.vue create mode 100644 src/modules/catalog/components/document-concepts/DocumentConceptsView.vue create mode 100644 src/modules/catalog/components/document-concepts/document-concepts.interface.ts create mode 100644 src/modules/catalog/components/document-concepts/document-concepts.services.ts create mode 100644 src/modules/catalog/components/document-concepts/document-models.interface.ts create mode 100644 src/modules/catalog/components/document-concepts/document-models.services.ts diff --git a/src/components/layout/Sidebar.vue b/src/components/layout/Sidebar.vue index 1ee8f5d..272a739 100644 --- a/src/components/layout/Sidebar.vue +++ b/src/components/layout/Sidebar.vue @@ -62,6 +62,12 @@ const menuItems = ref([ 'companies.update', 'companies.destroy', ], + }, + { + label: 'Conceptos de Documentos', + icon: 'pi pi-book', + to: '/catalog/document-concepts', + permission: 'document_concepts.index' } ] }, diff --git a/src/modules/catalog/components/document-concepts/DocumentConceptsForm.vue b/src/modules/catalog/components/document-concepts/DocumentConceptsForm.vue new file mode 100644 index 0000000..0baf5ea --- /dev/null +++ b/src/modules/catalog/components/document-concepts/DocumentConceptsForm.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/modules/catalog/components/document-concepts/DocumentConceptsTable.vue b/src/modules/catalog/components/document-concepts/DocumentConceptsTable.vue new file mode 100644 index 0000000..80c569f --- /dev/null +++ b/src/modules/catalog/components/document-concepts/DocumentConceptsTable.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue b/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue new file mode 100644 index 0000000..51a7f07 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue @@ -0,0 +1,150 @@ + + + + diff --git a/src/modules/catalog/components/document-concepts/document-concepts.interface.ts b/src/modules/catalog/components/document-concepts/document-concepts.interface.ts new file mode 100644 index 0000000..18bf293 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-concepts.interface.ts @@ -0,0 +1,64 @@ +// document-concepts.interface.ts + +/** + * Entity: DocumentConcept + * Representa un concepto documental gestionado en el sistema. + */ +export interface DocumentConcept { + id: number; + tenant_id: number; + document_model_id: number; + name: string; + folder_path: string; // Generado automáticamente por el backend + serie?: string | null; // Opcional + initial_number: number; + created_at: string; // ISO string + updated_at: string; // ISO string + deleted_at: string | null; // Puede ser null (no borrado) +} + +/** + * DTO para creación de un concepto + */ +export interface CreateDocumentConceptDTO { + document_model_id: number; + name: string; + serie?: string | null; + initial_number: number; +} + +/** + * DTO para actualización parcial de un concepto + * Todos los campos son opcionales para permitir updates parciales. + */ +export interface UpdateDocumentConceptDTO { + document_model_id?: number; + name?: string; + serie?: string | null; + initial_number?: number; +} + +/** + * Respuesta de la API para listado de conceptos + */ +export interface ResponseDocumentConceptDTO { + data: DocumentConcept[]; + message?: string; + success?: boolean; +} + +/** + * Respuesta de la API para una sola entidad de concepto (por id o creación) + */ +export interface SingleDocumentConceptResponseDTO { + id: number; + tenant_id: number; + document_model_id: number; + name: string; + folder_path: string; + serie?: string | null; + initial_number: number; + created_at: string; + updated_at: string; + deleted_at: string | null; +} diff --git a/src/modules/catalog/components/document-concepts/document-concepts.services.ts b/src/modules/catalog/components/document-concepts/document-concepts.services.ts new file mode 100644 index 0000000..44157fe --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-concepts.services.ts @@ -0,0 +1,79 @@ +// document-concepts.services.ts +import api from '@/services/api'; +import type { + CreateDocumentConceptDTO, + ResponseDocumentConceptDTO, + SingleDocumentConceptResponseDTO +} from './document-concepts.interface'; + +/** + * Servicio para gestión de conceptos documentales. + */ +export class DocumentConceptsService { + /** + * Obtiene la lista de conceptos documentales, con filtros opcionales. + * @param filters name (string, opcional), document_model_id (number, opcional) + */ + public async getDocumentConcepts(filters?: { name?: string; document_model_id?: number }): Promise { + try { + const response = await api.get('/api/document-concepts', { params: filters }); + return response.data; + } catch (error) { + console.error('Error fetching document concepts:', error); + throw error; + } + } + + /** + * Crea un nuevo concepto documental. + * @param data Payload con los valores requeridos. + */ + public async createDocumentConcept(data: CreateDocumentConceptDTO): Promise { + try { + const response = await api.post('/api/document-concepts', data); + return response.data; + } catch (error) { + console.error('Error creating document concept:', error); + throw error; + } + } + + /** + * Consulta un concepto documental por su id. + * @param id Identificador único del concepto + */ + public async getDocumentConceptById(id: number): Promise { + try { + const response = await api.get(`/api/document-concepts/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching document concept by id:', error); + throw error; + } + } + + /** + * Actualiza parcialmente un concepto documental (PATCH /document-concepts/:id) + */ + public async updateDocumentConcept(id: number, data: Partial): Promise { + try { + const response = await api.patch(`/api/document-concepts/${id}`, data); + return response.data; + } catch (error) { + console.error('Error updating document concept:', error); + throw error; + } + } + + /** + * Elimina un concepto documental por su id (DELETE /document-concepts/:id) + */ + public async deleteDocumentConcept(id: number): Promise { + try { + await api.delete(`/api/document-concepts/${id}`); + } catch (error) { + console.error('Error deleting document concept:', error); + throw error; + } + } +} diff --git a/src/modules/catalog/components/document-concepts/document-models.interface.ts b/src/modules/catalog/components/document-concepts/document-models.interface.ts new file mode 100644 index 0000000..b61b114 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-models.interface.ts @@ -0,0 +1,48 @@ +// document-models.interface.ts + +export interface DocumentModel { + id: number; + name: string; + module: number; + type: number; + num_folio: number; + created_at: string; // ISO date + updated_at: string; // ISO date + deleted_at: string | null; +} + +export interface CreateDocumentModelDTO { + name: string; + module: number; + type: number; + num_folio?: number; +} + +export interface UpdateDocumentModelDTO { + name?: string; + module?: number; + type?: number; + num_folio?: number; +} + +export interface PaginatedDocumentModelsResponse { + current_page: number; + data: DocumentModel[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + next_page_url: string | null; + path: string; + per_page: number; + prev_page_url: string | null; + to: number; + total: number; + links: PaginationLink[]; +} + +export interface PaginationLink { + url: string | null; + label: string; + active: boolean; +} diff --git a/src/modules/catalog/components/document-concepts/document-models.services.ts b/src/modules/catalog/components/document-concepts/document-models.services.ts new file mode 100644 index 0000000..b7cf4a5 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-models.services.ts @@ -0,0 +1,41 @@ +import api from "@/services/api"; +import type { + DocumentModel, + PaginatedDocumentModelsResponse +} from "./document-models.interface"; + +export class DocumentModelsService { + /** + * Obtiene modelos de documento, con paginación o sin ella. + * @param paginate default: true. Si es false, devuelve DocumentModel[] directamente; + * @param per_paginate default: 15. Si se envía, cambia el page size. + */ + public async getDocumentModels(opts?: { + paginate?: boolean; + per_paginate?: number; + search?: string; + [key: string]: any; + }): Promise { + try { + // Construye params siempre + const params: Record = { + paginate: opts?.paginate, + per_paginate: opts?.per_paginate, + ...opts + }; + // Evita undefined + if (params.paginate === undefined) delete params.paginate; + if (params.per_paginate === undefined) delete params.per_paginate; + const response = await api.get("/api/catalogs/document-models", { params }); + if (opts?.paginate === false) { + return response.data.data; // Solo el array de modelos + } else { + return response.data as PaginatedDocumentModelsResponse; + } + } catch (error) { + console.error("Error fetching document models:", error); + throw error; + } + } +} + diff --git a/src/router/index.ts b/src/router/index.ts index 3ae6d0c..7ecc2e2 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -204,6 +204,16 @@ const routes: RouteRecordRaw[] = [ requiresAuth: true } }, + { + path: 'document-concepts', + name: 'DocumentConcepts', + component: () => import('@/modules/catalog/components/document-concepts/DocumentConceptsView.vue'), + meta: { + title: 'Conceptos de Documentos', + requiresAuth: true, + permission: 'document_concepts.index' + } + }, companiesRouter ] }, From bf8c55c3547ecfc9251781aa73d03dbc881c5fd7 Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Wed, 25 Mar 2026 15:22:10 -0600 Subject: [PATCH 2/2] feat(document-concepts): implement CRUD functionality for convertible document concepts --- src/main.ts | 2 + .../DocumentConceptsView.vue | 156 +++++++++++++++++- ...document-concept-convertibles.interface.ts | 52 ++++++ .../document-concept-convertibles.services.ts | 38 +++++ 4 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 src/modules/catalog/components/document-concepts/document-concept-convertibles.interface.ts create mode 100644 src/modules/catalog/components/document-concepts/document-concept-convertibles.services.ts diff --git a/src/main.ts b/src/main.ts index 1b0756f..79fc326 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import PrimeVue from "primevue/config"; import ConfirmationService from 'primevue/confirmationservice'; import ToastService from 'primevue/toastservice'; import StyleClass from "primevue/styleclass"; +import Tooltip from "primevue/tooltip"; import { createApp } from "vue"; import { createPinia } from "pinia"; import App from "./App.vue"; @@ -48,6 +49,7 @@ app.use(PrimeVue, { }); app.directive("styleclass", StyleClass); +app.directive("tooltip", Tooltip); const bootstrap = async () => { const { initAuth } = useAuth(); diff --git a/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue b/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue index 51a7f07..dd80b98 100644 --- a/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue +++ b/src/modules/catalog/components/document-concepts/DocumentConceptsView.vue @@ -6,6 +6,15 @@ Ver convenciones en rules.md, AGENTS.md y SKILL. import { ref, onMounted, computed } from 'vue'; import { useAuth } from '@/modules/auth/composables/useAuth'; import { useToast } from 'primevue/usetoast'; +import TabView from 'primevue/tabview'; +import TabPanel from 'primevue/tabpanel'; +import DataTable from 'primevue/datatable'; +import Column from 'primevue/column'; +import Dialog from 'primevue/dialog'; +import Select from 'primevue/select'; +import ProgressSpinner from 'primevue/progressspinner'; +import Button from 'primevue/button'; +import { DocumentConceptConvertiblesService } from './document-concept-convertibles.services'; import type { DocumentConcept, CreateDocumentConceptDTO, UpdateDocumentConceptDTO } from './document-concepts.interface'; import { DocumentConceptsService } from './document-concepts.services'; import DocumentConceptsTable from './DocumentConceptsTable.vue'; @@ -24,6 +33,81 @@ const items = ref([]); const loading = ref(false); const showDialog = ref(false); const selected = ref(null); + +// --- Convertibles Logic --- +const conceptCandidates = ref([]); // todas menos la seleccionada +const selectedConvertibleIds = ref([]); // ids actualmente asignados +const convertiblesLoading = ref(false); +const convertiblesSaving = ref(false); +const convertiblesService = new DocumentConceptConvertiblesService(); + +// CRUD para tabla +const convertibleConcepts = computed(() => + conceptCandidates.value.filter(c => selectedConvertibleIds.value.includes(c.id)) +); + +const showAddConvertible = ref(false); +const newConvertibleId = ref(null); +const availableForAdd = computed(() => + conceptCandidates.value.filter(c => !selectedConvertibleIds.value.includes(c.id)) +); + +function resetAddConvertible() { + newConvertibleId.value = null; + showAddConvertible.value = false; +} + +async function addConvertible() { + if (!selected.value?.id || !newConvertibleId.value) return; + convertiblesSaving.value = true; + try { + const nuevos = [...selectedConvertibleIds.value, newConvertibleId.value]; + await convertiblesService.assignConvertibles(selected.value.id, { target_ids: nuevos }); + selectedConvertibleIds.value = nuevos; + toast.add({ severity: 'success', summary: 'Convertibles actualizados', detail: 'Se agregó el convertible.', life: 3000 }); + resetAddConvertible(); + } catch (err) { + toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo agregar el convertible.', life: 4000 }); + } finally { + convertiblesSaving.value = false; + } +} + +async function removeConvertible(id: number) { + if (!selected.value?.id) return; + convertiblesSaving.value = true; + try { + const nuevos = selectedConvertibleIds.value.filter(cid => cid !== id); + await convertiblesService.assignConvertibles(selected.value.id, { target_ids: nuevos }); + selectedConvertibleIds.value = nuevos; + toast.add({ severity: 'success', summary: 'Convertibles actualizados', detail: 'Convertible eliminado.', life: 3000 }); + } catch (err) { + toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo eliminar el convertible.', life: 4000 }); + } finally { + convertiblesSaving.value = false; + } +} + +async function fetchConvertiblesInfo() { + if (!selected.value?.id) return; + convertiblesLoading.value = true; + try { + // Fetch all document concepts (candidates for convertibles) + // Exclude the selected concept itself + const response = await conceptsService.getDocumentConcepts(); + conceptCandidates.value = (response.data || []).filter(x => x.id !== selected.value!.id); + + // Fetch assigned convertibles for this concept + const convertiblesResp = await convertiblesService.getConvertibles(selected.value.id); + selectedConvertibleIds.value = (convertiblesResp.data || []).map(x => x.id); + } catch (err) { + toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los convertibles.', life: 4000 }); + } finally { + convertiblesLoading.value = false; + } +} + + const filterName = ref(''); const filterModelId = ref(null); @@ -58,6 +142,8 @@ function openEdit(item: DocumentConcept) { } selected.value = { ...item }; showDialog.value = true; + // When editing is triggered, fetch convertibles information + fetchConvertiblesInfo(); } async function handleDelete(id: number) { if (!canDelete.value) { @@ -137,14 +223,68 @@ async function handleSubmit(data: CreateDocumentConceptDTO | UpdateDocumentConce - + + diff --git a/src/modules/catalog/components/document-concepts/document-concept-convertibles.interface.ts b/src/modules/catalog/components/document-concepts/document-concept-convertibles.interface.ts new file mode 100644 index 0000000..402e3e0 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-concept-convertibles.interface.ts @@ -0,0 +1,52 @@ +// document-concept-convertibles.interface.ts + +/** + * Convertible Document Concept (response item for GET/POST convertibles) + */ +export interface ConvertibleDocumentConcept { + id: number; + tenant_id: number; + document_model_id: number; + name: string; + folder_path: string; + serie: string; + initial_number: number; + created_at: string; // ISO 8601 + updated_at: string; // ISO 8601 + deleted_at: string | null; + pivot: ConvertiblePivot; +} + +export interface ConvertiblePivot { + concept_id: number; + target_concept_id: number; + tenant_id: number; +} + +/** + * GET/POST response for convertibles + */ +export interface ConvertibleDocumentConceptsResponse { + data: ConvertibleDocumentConcept[]; +} + +/** + * POST body for assigning convertibles + */ +export interface AssignConvertiblesBody { + target_ids: number[]; +} + +/** + * Documentation + */ +/** + * GET /api/document-concepts/{concept_id}/convertibles + * Busca todos los conceptos de documento convertibles para el concepto dado + * Response: { data: ConvertibleDocumentConcept[] } + * + * POST /api/document-concepts/{concept_id}/convertibles + * Asigna los destinos convertibles para el concepto dado + * Body: { target_ids: number[] } + * Response: { data: ConvertibleDocumentConcept[] } + */ diff --git a/src/modules/catalog/components/document-concepts/document-concept-convertibles.services.ts b/src/modules/catalog/components/document-concepts/document-concept-convertibles.services.ts new file mode 100644 index 0000000..487a688 --- /dev/null +++ b/src/modules/catalog/components/document-concepts/document-concept-convertibles.services.ts @@ -0,0 +1,38 @@ +import api from "@/services/api"; +import type { + ConvertibleDocumentConceptsResponse, + AssignConvertiblesBody +} from "./document-concept-convertibles.interface"; + +export class DocumentConceptConvertiblesService { + /** + * Obtiene los conceptos de documento convertibles para el concepto dado + * @param conceptId (ID del concepto base) + * @returns ConvertibleDocumentConceptsResponse + */ + public async getConvertibles(conceptId: number): Promise { + try { + const response = await api.get(`/api/document-concepts/${conceptId}/convertibles`); + return response.data; + } catch (error) { + console.error("Error fetching convertibles:", error); + throw error; + } + } + + /** + * Asigna / sobreescribe los destinos convertibles + * @param conceptId (ID del concepto base) + * @param body { target_ids: number[] } + * @returns ConvertibleDocumentConceptsResponse + */ + public async assignConvertibles(conceptId: number, body: AssignConvertiblesBody): Promise { + try { + const response = await api.post(`/api/document-concepts/${conceptId}/convertibles`, body); + return response.data; + } catch (error) { + console.error("Error assigning convertibles:", error); + throw error; + } + } +}