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; + } + } +}