CTL-51-DEVELOP #21
@ -62,6 +62,12 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
'companies.update',
|
'companies.update',
|
||||||
'companies.destroy',
|
'companies.destroy',
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Conceptos de Documentos',
|
||||||
|
icon: 'pi pi-book',
|
||||||
|
to: '/catalog/document-concepts',
|
||||||
|
permission: 'document_concepts.index'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, watch, ref } from 'vue';
|
||||||
|
import type { DocumentConcept, CreateDocumentConceptDTO } from './document-concepts.interface';
|
||||||
|
import type { DocumentModel } from './document-models.interface';
|
||||||
|
import { DocumentModelsService } from './document-models.services';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: DocumentConcept | null;
|
||||||
|
canEdit?: boolean;
|
||||||
|
canCreate?: boolean;
|
||||||
|
canUpdate?: boolean;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), { modelValue: null, canEdit: true, canCreate: true, canUpdate: true });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'submit', value: CreateDocumentConceptDTO): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const documentModels = ref<DocumentModel[]>([]);
|
||||||
|
const loadingModels = ref(false);
|
||||||
|
|
||||||
|
const form = reactive<CreateDocumentConceptDTO>({
|
||||||
|
document_model_id: 0,
|
||||||
|
name: '',
|
||||||
|
serie: '',
|
||||||
|
initial_number: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = reactive<{ [K in keyof CreateDocumentConceptDTO]?: string }>({});
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
if (val) {
|
||||||
|
form.document_model_id = val.document_model_id;
|
||||||
|
form.name = val.name;
|
||||||
|
form.serie = val.serie || '';
|
||||||
|
form.initial_number = val.initial_number;
|
||||||
|
} else {
|
||||||
|
form.document_model_id = 0;
|
||||||
|
form.name = '';
|
||||||
|
form.serie = '';
|
||||||
|
form.initial_number = 1;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const documentModelsService = new DocumentModelsService();
|
||||||
|
|
||||||
|
async function fetchDocumentModels(searchTerm?: string) {
|
||||||
|
loadingModels.value = true;
|
||||||
|
try {
|
||||||
|
const data = await documentModelsService.getDocumentModels({ paginate: false, ...(searchTerm ? { search: searchTerm } : {}) });
|
||||||
|
documentModels.value = Array.isArray(data) ? data : [];
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron obtener los modelos de documento', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
loadingModels.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cargar todos los modelos al montar
|
||||||
|
fetchDocumentModels();
|
||||||
|
|
||||||
|
function onDropdownSearch(event: { value: string }) {
|
||||||
|
fetchDocumentModels(event.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(): boolean {
|
||||||
|
let valid = true;
|
||||||
|
errors.document_model_id = form.document_model_id ? '' : 'Modelo es requerido';
|
||||||
|
errors.name = form.name ? '' : 'Nombre es requerido';
|
||||||
|
errors.initial_number = form.initial_number >= 0 ? '' : 'Número inicial inválido';
|
||||||
|
// 'serie' is optional
|
||||||
|
valid = !errors.document_model_id && !errors.name && !errors.initial_number;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
// Bloquear si no hay permiso de edición (crear o actualizar)
|
||||||
|
if (!props.canEdit) return;
|
||||||
|
if (!validate()) return;
|
||||||
|
emit('submit', { ...form });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit" novalidate>
|
||||||
|
<div class="field mb-4">
|
||||||
|
<label class="block text-sm font-semibold mb-1">Modelo</label>
|
||||||
|
<Dropdown v-model="form.document_model_id"
|
||||||
|
:options="documentModels"
|
||||||
|
optionLabel="name"
|
||||||
|
optionValue="id"
|
||||||
|
filter
|
||||||
|
:placeholder="'Selecciona documento modelo'"
|
||||||
|
:loading="loadingModels"
|
||||||
|
:showClear="true"
|
||||||
|
@filter="onDropdownSearch"
|
||||||
|
class="w-full" />
|
||||||
|
<small v-if="errors.document_model_id" class="p-error">{{ errors.document_model_id }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="field mb-4">
|
||||||
|
<label class="block text-sm font-semibold mb-1">Nombre</label>
|
||||||
|
<InputText v-model="form.name" inputId="name" class="w-full" />
|
||||||
|
<small v-if="errors.name" class="p-error">{{ errors.name }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="field mb-4">
|
||||||
|
<label class="block text-sm font-semibold mb-1">Serie <span class="text-xs text-slate-400">(opcional)</span></label>
|
||||||
|
<InputText v-model="form.serie" inputId="serie" class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="field mb-4">
|
||||||
|
<label class="block text-sm font-semibold mb-1">Número Inicial</label>
|
||||||
|
<InputNumber v-model="form.initial_number" inputId="initial-number" class="w-full" :min="0" />
|
||||||
|
<small v-if="errors.initial_number" class="p-error">{{ errors.initial_number }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions flex justify-end gap-2 mt-5">
|
||||||
|
<Button type="button" label="Cancelar" outlined @click="emit('cancel')" />
|
||||||
|
<Button type="submit" label="Guardar" :disabled="!props.canEdit" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.field { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||||
|
.p-error { color: #ef4444; font-size: 12px; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DocumentConcept } from './document-concepts.interface';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: DocumentConcept[];
|
||||||
|
loading?: boolean;
|
||||||
|
canUpdate?: boolean;
|
||||||
|
canDelete?: boolean;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), { loading: false, canUpdate: false, canDelete: false });
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'edit', item: DocumentConcept): void;
|
||||||
|
(e: 'delete', id: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onEdit(item: DocumentConcept) {
|
||||||
|
emit('edit', item);
|
||||||
|
}
|
||||||
|
function onDelete(id: number) {
|
||||||
|
emit('delete', id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DataTable :value="props.items" :loading="props.loading" striped-rows responsive-layout="scroll" class="min-w-full">
|
||||||
|
<Column field="id" header="ID" :sortable="true">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-xs font-mono font-bold text-slate-400">{{ data.id }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="name" header="Concept Name" :sortable="true">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded bg-blue-50 flex items-center justify-center">
|
||||||
|
<span class="pi pi-book text-blue-600 text-lg"></span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-semibold text-on-surface">{{ data.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="document_model_id" header="Model ID" :sortable="true">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded bg-slate-100 text-slate-600 text-[10px] font-bold">
|
||||||
|
MOD-{{ data.document_model_id }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="serie" header="Serie">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-sm font-medium text-primary">{{ data.serie || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="initial_number" header="Initial No.">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span class="text-sm font-mono text-on-surface-variant">{{ data.initial_number }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="Status">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-secondary-container text-on-secondary-container text-[11px] font-bold"
|
||||||
|
:class="{
|
||||||
|
'bg-secondary-container text-on-secondary-container': !data.deleted_at,
|
||||||
|
'bg-slate-200 text-slate-500': data.deleted_at,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full" :class="data.deleted_at ? 'bg-slate-400' : 'bg-blue-600 animate-pulse'"></span>
|
||||||
|
{{ data.deleted_at ? 'INACTIVE' : 'ACTIVE' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="Actions" body-class="text-right">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
|
<Button icon="pi pi-pencil" text @click="props.canUpdate ? onEdit(data) : null" :disabled="!props.canUpdate" title="Editar" />
|
||||||
|
<Button icon="pi pi-trash" text severity="danger" @click="props.canDelete ? onDelete(data.id) : null" :disabled="!props.canDelete" title="Eliminar" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.min-w-full { min-width: 100%; }
|
||||||
|
</style>
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
<!--
|
||||||
|
Auto-generado desde el HTML fuente y adaptado con la skill opencode-vue-component
|
||||||
|
Ver convenciones en rules.md, AGENTS.md y SKILL.
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { useAuth } from '@/modules/auth/composables/useAuth';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import type { DocumentConcept, CreateDocumentConceptDTO, UpdateDocumentConceptDTO } from './document-concepts.interface';
|
||||||
|
import { DocumentConceptsService } from './document-concepts.services';
|
||||||
|
import DocumentConceptsTable from './DocumentConceptsTable.vue';
|
||||||
|
import DocumentConceptsForm from './DocumentConceptsForm.vue';
|
||||||
|
|
||||||
|
const conceptsService = new DocumentConceptsService();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
// Permisos de usuario
|
||||||
|
const { hasPermission } = useAuth();
|
||||||
|
const canCreate = computed(() => hasPermission('document_concepts.store'));
|
||||||
|
const canUpdate = computed(() => hasPermission('document_concepts.update'));
|
||||||
|
const canDelete = computed(() => hasPermission('document_concepts.destroy'));
|
||||||
|
|
||||||
|
const items = ref<DocumentConcept[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const showDialog = ref(false);
|
||||||
|
const selected = ref<DocumentConcept | null>(null);
|
||||||
|
const filterName = ref<string>('');
|
||||||
|
const filterModelId = ref<number|null>(null);
|
||||||
|
|
||||||
|
async function fetchItems() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await conceptsService.getDocumentConcepts({
|
||||||
|
name: filterName.value || undefined,
|
||||||
|
document_model_id: filterModelId.value || undefined,
|
||||||
|
});
|
||||||
|
items.value = response.data;
|
||||||
|
} catch {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudieron cargar los conceptos', life: 3000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(fetchItems);
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
if (!canCreate.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permiso', detail: 'No tienes permiso para crear conceptos', life: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selected.value = null;
|
||||||
|
showDialog.value = true;
|
||||||
|
}
|
||||||
|
function openEdit(item: DocumentConcept) {
|
||||||
|
if (!canUpdate.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permiso', detail: 'No tienes permiso para editar conceptos', life: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selected.value = { ...item };
|
||||||
|
showDialog.value = true;
|
||||||
|
}
|
||||||
|
async function handleDelete(id: number) {
|
||||||
|
if (!canDelete.value) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permiso', detail: 'No tienes permiso para eliminar conceptos', life: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await conceptsService.deleteDocumentConcept(id);
|
||||||
|
toast.add({ severity: 'success', summary: 'Eliminado', detail: 'Concepto eliminado', life: 3000 });
|
||||||
|
await fetchItems();
|
||||||
|
} catch {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo eliminar', life: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleSubmit(data: CreateDocumentConceptDTO | UpdateDocumentConceptDTO) {
|
||||||
|
// Permiso requerido según si es update o store
|
||||||
|
const needsUpdate = !!selected.value?.id;
|
||||||
|
if ((needsUpdate && !canUpdate.value) || (!needsUpdate && !canCreate.value)) {
|
||||||
|
toast.add({ severity: 'warn', summary: 'Sin permiso', detail: 'No tienes permiso para guardar conceptos', life: 2500 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (selected.value?.id) {
|
||||||
|
await conceptsService.updateDocumentConcept(selected.value.id, data as UpdateDocumentConceptDTO);
|
||||||
|
toast.add({ severity: 'success', summary: 'Actualizado', detail: 'Concepto actualizado', life: 3000 });
|
||||||
|
} else {
|
||||||
|
await conceptsService.createDocumentConcept(data as CreateDocumentConceptDTO);
|
||||||
|
toast.add({ severity: 'success', summary: 'Creado', detail: 'Concepto creado', life: 3000 });
|
||||||
|
}
|
||||||
|
showDialog.value = false;
|
||||||
|
await fetchItems();
|
||||||
|
} catch {
|
||||||
|
toast.add({ severity: 'error', summary: 'Error', detail: 'No se pudo guardar', life: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="document-concepts-view p-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-10">
|
||||||
|
<div>
|
||||||
|
<nav class="flex items-center gap-2 text-xs font-medium text-slate-400 mb-2">
|
||||||
|
<span>Admin</span>
|
||||||
|
<span class="pi pi-chevron-right"></span>
|
||||||
|
<span>Documentación</span>
|
||||||
|
<span class="pi pi-chevron-right"></span>
|
||||||
|
<span class="text-primary-container">Conceptos</span>
|
||||||
|
</nav>
|
||||||
|
<h2 class="text-3xl font-extrabold text-on-surface tracking-tight">Gestión de Conceptos</h2>
|
||||||
|
<p class="text-on-surface-variant mt-1 text-sm font-medium">
|
||||||
|
Configure and organize document types for precise ERP automation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button label="Nuevo Concepto" icon="pi pi-plus" @click="openCreate" :disabled="!canCreate" class="px-6 py-3 font-bold rounded-lg shadow-lg text-sm" v-if="canCreate" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter/Search Bar -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 mb-6">
|
||||||
|
<div class="lg:col-span-8 flex flex-wrap gap-3">
|
||||||
|
<span class="pi pi-filter absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></span>
|
||||||
|
<InputText v-model="filterName" placeholder="Filter by Name or Serie (e.g. E-2026)..." class="w-full pl-12 pr-4 py-3 bg-surface-container-lowest border-b text-sm rounded-t-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<div class="bg-surface-container-lowest rounded-xl overflow-hidden shadow-sm">
|
||||||
|
<DocumentConceptsTable
|
||||||
|
:items="items"
|
||||||
|
:loading="loading"
|
||||||
|
@edit="openEdit"
|
||||||
|
@delete="handleDelete"
|
||||||
|
:can-update="canUpdate"
|
||||||
|
:can-delete="canDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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" >
|
||||||
|
<DocumentConceptsForm
|
||||||
|
:model-value="selected"
|
||||||
|
:can-edit="selected ? canUpdate : canCreate"
|
||||||
|
:can-create="canCreate"
|
||||||
|
:can-update="canUpdate"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="() => showDialog = false"
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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<ResponseDocumentConceptDTO> {
|
||||||
|
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<SingleDocumentConceptResponseDTO> {
|
||||||
|
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<SingleDocumentConceptResponseDTO> {
|
||||||
|
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<CreateDocumentConceptDTO>): Promise<SingleDocumentConceptResponseDTO> {
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/document-concepts/${id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting document concept:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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<PaginatedDocumentModelsResponse | DocumentModel[]> {
|
||||||
|
try {
|
||||||
|
// Construye params siempre
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -204,6 +204,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
requiresAuth: true
|
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
|
companiesRouter
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user