CTL-51-DEVELOP #21
@ -6,6 +6,7 @@ import PrimeVue from "primevue/config";
|
|||||||
import ConfirmationService from 'primevue/confirmationservice';
|
import ConfirmationService from 'primevue/confirmationservice';
|
||||||
import ToastService from 'primevue/toastservice';
|
import ToastService from 'primevue/toastservice';
|
||||||
import StyleClass from "primevue/styleclass";
|
import StyleClass from "primevue/styleclass";
|
||||||
|
import Tooltip from "primevue/tooltip";
|
||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
@ -48,6 +49,7 @@ app.use(PrimeVue, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.directive("styleclass", StyleClass);
|
app.directive("styleclass", StyleClass);
|
||||||
|
app.directive("tooltip", Tooltip);
|
||||||
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
const { initAuth } = useAuth();
|
const { initAuth } = useAuth();
|
||||||
|
|||||||
@ -6,6 +6,15 @@ Ver convenciones en rules.md, AGENTS.md y SKILL.
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useAuth } from '@/modules/auth/composables/useAuth';
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
import { useToast } from 'primevue/usetoast';
|
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 type { DocumentConcept, CreateDocumentConceptDTO, UpdateDocumentConceptDTO } from './document-concepts.interface';
|
||||||
import { DocumentConceptsService } from './document-concepts.services';
|
import { DocumentConceptsService } from './document-concepts.services';
|
||||||
import DocumentConceptsTable from './DocumentConceptsTable.vue';
|
import DocumentConceptsTable from './DocumentConceptsTable.vue';
|
||||||
@ -24,6 +33,81 @@ const items = ref<DocumentConcept[]>([]);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showDialog = ref(false);
|
const showDialog = ref(false);
|
||||||
const selected = ref<DocumentConcept | null>(null);
|
const selected = ref<DocumentConcept | null>(null);
|
||||||
|
|
||||||
|
// --- Convertibles Logic ---
|
||||||
|
const conceptCandidates = ref<DocumentConcept[]>([]); // todas menos la seleccionada
|
||||||
|
const selectedConvertibleIds = ref<number[]>([]); // 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<number|null>(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<string>('');
|
const filterName = ref<string>('');
|
||||||
const filterModelId = ref<number|null>(null);
|
const filterModelId = ref<number|null>(null);
|
||||||
|
|
||||||
@ -58,6 +142,8 @@ function openEdit(item: DocumentConcept) {
|
|||||||
}
|
}
|
||||||
selected.value = { ...item };
|
selected.value = { ...item };
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
|
// When editing is triggered, fetch convertibles information
|
||||||
|
fetchConvertiblesInfo();
|
||||||
}
|
}
|
||||||
async function handleDelete(id: number) {
|
async function handleDelete(id: number) {
|
||||||
if (!canDelete.value) {
|
if (!canDelete.value) {
|
||||||
@ -137,14 +223,68 @@ async function handleSubmit(data: CreateDocumentConceptDTO | UpdateDocumentConce
|
|||||||
|
|
||||||
<!-- Dialog for Create/Edit -->
|
<!-- Dialog for Create/Edit -->
|
||||||
<Dialog v-model:visible="showDialog" :header="selected ? 'Editar Concepto' : 'Nuevo Concepto'" modal :style="{ width: '980px', maxWidth: '98vw' }" dismissableMask :closable="true" :draggable="false" >
|
<Dialog v-model:visible="showDialog" :header="selected ? 'Editar Concepto' : 'Nuevo Concepto'" modal :style="{ width: '980px', maxWidth: '98vw' }" dismissableMask :closable="true" :draggable="false" >
|
||||||
<DocumentConceptsForm
|
<template v-if="selected">
|
||||||
:model-value="selected"
|
<TabView>
|
||||||
:can-edit="selected ? canUpdate : canCreate"
|
<TabPanel header="Formulario" value="form">
|
||||||
:can-create="canCreate"
|
<DocumentConceptsForm
|
||||||
:can-update="canUpdate"
|
:model-value="selected"
|
||||||
@submit="handleSubmit"
|
:can-edit="canUpdate"
|
||||||
@cancel="() => showDialog = false"
|
:can-create="canCreate"
|
||||||
/>
|
:can-update="canUpdate"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="() => showDialog = false"
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel header="Convertibles" value="convertibles">
|
||||||
|
<div class="p-6 flex flex-col gap-6 min-h-[300px]">
|
||||||
|
<template v-if="convertiblesLoading">
|
||||||
|
<ProgressSpinner />
|
||||||
|
<div class="text-on-surface-variant text-center mt-2">Cargando convertibles...</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DataTable :value="convertibleConcepts" :loading="convertiblesLoading" class="w-full" striped-rows size="small" tableStyle="min-width: 40rem">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="font-bold text-lg">Conceptos convertibles actuales</span>
|
||||||
|
<Button label="Agregar convertible" icon="pi pi-plus" @click="showAddConvertible = true" :disabled="!canUpdate || convertiblesSaving"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<Column field="name" header="Concepto convertible" />
|
||||||
|
<Column header="Acciones">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button icon="pi pi-trash" severity="danger" text rounded class="p-button-sm" v-tooltip.top="'Eliminar convertible'" @click="removeConvertible(data.id)" :disabled="!canUpdate || convertiblesSaving"/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<template #empty>
|
||||||
|
<div class="text-center text-on-surface-variant">No hay conceptos convertibles asignados.</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
|
||||||
|
<!-- Dialog para agregar nuevo convertible -->
|
||||||
|
<Dialog v-model:visible="showAddConvertible" header="Agregar convertible" modal :style="{ width: '400px' }" :closable="true">
|
||||||
|
<div class="mb-4">
|
||||||
|
<Select v-model="newConvertibleId" :options="availableForAdd" optionLabel="name" optionValue="id" filter placeholder="Seleccione un concepto..." class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3 justify-end">
|
||||||
|
<Button label="Agregar" icon="pi pi-check" @click="addConvertible" :loading="convertiblesSaving" :disabled="!newConvertibleId || convertiblesSaving || !canUpdate"/>
|
||||||
|
<Button label="Cancelar" icon="pi pi-times" @click="showAddConvertible = false" class="p-button-text"/>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</TabView>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DocumentConceptsForm
|
||||||
|
:model-value="selected"
|
||||||
|
:can-edit="canCreate"
|
||||||
|
:can-create="canCreate"
|
||||||
|
:can-update="canUpdate"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="() => showDialog = false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -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[] }
|
||||||
|
*/
|
||||||
@ -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<ConvertibleDocumentConceptsResponse> {
|
||||||
|
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<ConvertibleDocumentConceptsResponse> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user