feat(document-concepts): add CRUD functionality for document concepts and models
This commit is contained in:
parent
97b91013dd
commit
c269f17feb
@ -62,6 +62,12 @@ const menuItems = ref<MenuItem[]>([
|
||||
'companies.update',
|
||||
'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
|
||||
}
|
||||
},
|
||||
{
|
||||
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
|
||||
]
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user