Add: Administración de clasificaciones comerciales
This commit is contained in:
parent
fa1bb4b20b
commit
2bd5d00827
@ -94,6 +94,14 @@ onMounted(() => {
|
|||||||
to="admin.units-measure.index"
|
to="admin.units-measure.index"
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section name="Comercial">
|
||||||
|
<Link
|
||||||
|
icon="bookmarks"
|
||||||
|
name="Clasificaciones Comerciales"
|
||||||
|
to="admin.comercial-classifications.index"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section name="Capacitaciones">
|
<Section name="Capacitaciones">
|
||||||
<DropDown
|
<DropDown
|
||||||
icon="grid_view"
|
icon="grid_view"
|
||||||
|
|||||||
89
src/pages/ComercialClassifications/Create.vue
Normal file
89
src/pages/ComercialClassifications/Create.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, useRoute, RouterLink } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { apiTo, transl, viewTo } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Form from './Form.vue'
|
||||||
|
|
||||||
|
/** Definidores */
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
parent_id: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentInfo = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.transform(data => ({
|
||||||
|
...data,
|
||||||
|
parent_id: data.parent_id // Usar el parent_id del formulario
|
||||||
|
})).post(apiTo('store'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.create.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
// Verificar si se están pasando parámetros para crear subcategoría
|
||||||
|
if (route.query.parent_id) {
|
||||||
|
parentInfo.value = {
|
||||||
|
id: parseInt(route.query.parent_id),
|
||||||
|
name: route.query.parent_name,
|
||||||
|
code: route.query.parent_code
|
||||||
|
};
|
||||||
|
// Pre-llenar el parent_id en el formulario
|
||||||
|
form.parent_id = parseInt(route.query.parent_id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader
|
||||||
|
:title="parentInfo ? `${transl('create.title')} - Subcategoría` : transl('create.title')"
|
||||||
|
>
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<!-- Mostrar información del padre si se está creando una subcategoría -->
|
||||||
|
<div v-if="parentInfo" class="mb-4 p-4 bg-blue-50 dark:bg-blue-900 rounded-lg border border-blue-200 dark:border-blue-700">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<GoogleIcon class="text-blue-600 text-xl mr-2" name="account_tree" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||||
|
Creando subcategoría para:
|
||||||
|
</p>
|
||||||
|
<p class="text-lg font-semibold text-blue-900 dark:text-blue-100">
|
||||||
|
<code class="bg-blue-100 dark:bg-blue-800 px-2 py-1 rounded text-sm mr-2">{{ parentInfo.code }}</code>
|
||||||
|
{{ parentInfo.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
action="create"
|
||||||
|
:form="form"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
139
src/pages/ComercialClassifications/Edit.vue
Normal file
139
src/pages/ComercialClassifications/Edit.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { viewTo, apiTo, transl } from './Module';
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Form from './Form.vue'
|
||||||
|
|
||||||
|
/** Definiciones */
|
||||||
|
const vroute = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
id: null,
|
||||||
|
code: '',
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentOptions = ref([]);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
form.transform(data => ({
|
||||||
|
...data,
|
||||||
|
})).put(apiTo('update', { comercial_classification: form.id }), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success(Lang('register.edit.onSuccess'))
|
||||||
|
router.push(viewTo({ name: 'index' }));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para obtener clasificaciones padre (excluyendo la actual y sus hijos) */
|
||||||
|
function loadParentOptions() {
|
||||||
|
api.get(apiTo('index'), {
|
||||||
|
onSuccess: (r) => {
|
||||||
|
// Función para excluir la clasificación actual y sus descendientes
|
||||||
|
const excludeCurrentAndChildren = (items, excludeId) => {
|
||||||
|
return items.filter(item => {
|
||||||
|
if (item.id === excludeId) return false;
|
||||||
|
if (hasDescendant(item, excludeId)) return false;
|
||||||
|
return true;
|
||||||
|
}).map(item => ({
|
||||||
|
...item,
|
||||||
|
children: excludeCurrentAndChildren(item.children || [], excludeId)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Función para verificar si un item tiene como descendiente la clasificación actual
|
||||||
|
const hasDescendant = (item, targetId) => {
|
||||||
|
if (!item.children) return false;
|
||||||
|
return item.children.some(child =>
|
||||||
|
child.id === targetId || hasDescendant(child, targetId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aplanar la estructura jerárquica para el selector
|
||||||
|
const flattenOptions = (items, level = 0) => {
|
||||||
|
let options = [];
|
||||||
|
items.forEach(item => {
|
||||||
|
options.push({
|
||||||
|
...item,
|
||||||
|
name: ' '.repeat(level) + item.name, // Indentación visual
|
||||||
|
level
|
||||||
|
});
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
options = options.concat(flattenOptions(item.children, level + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataSource = r.data?.comercial_classifications?.data || r.comercial_classifications || [];
|
||||||
|
const filteredData = excludeCurrentAndChildren(dataSource, form.id);
|
||||||
|
parentOptions.value = flattenOptions(filteredData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
api.get(apiTo('show', { comercial_classification: vroute.params.id }), {
|
||||||
|
onSuccess: (r) => {
|
||||||
|
const classification = r.data || r.comercial_classification || r;
|
||||||
|
form.fill({
|
||||||
|
id: classification.id,
|
||||||
|
code: classification.code,
|
||||||
|
name: classification.name,
|
||||||
|
description: classification.description,
|
||||||
|
is_active: classification.is_active,
|
||||||
|
parent: classification.parent || null
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cargar opciones padre después de obtener los datos actuales
|
||||||
|
loadParentOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageHeader :title="`${transl('edit.title')}${form.parent ? ' - Subcategoría' : ''}`">
|
||||||
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
|
<IconButton
|
||||||
|
class="text-white"
|
||||||
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<!-- Mostrar información del padre si es una subcategoría -->
|
||||||
|
<div v-if="form.parent" class="mb-4 p-4 bg-yellow-50 dark:bg-yellow-900 rounded-lg border border-yellow-200 dark:border-yellow-700">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<GoogleIcon class="text-yellow-600 text-xl mr-2" name="account_tree" />
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||||
|
Editando subcategoría de:
|
||||||
|
</p>
|
||||||
|
<p class="text-lg font-semibold text-yellow-900 dark:text-yellow-100">
|
||||||
|
<code class="bg-yellow-100 dark:bg-yellow-800 px-2 py-1 rounded text-sm mr-2">{{ form.parent.code }}</code>
|
||||||
|
{{ form.parent.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
action="update"
|
||||||
|
:form="form"
|
||||||
|
:parent-options="parentOptions"
|
||||||
|
@submit="submit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
68
src/pages/ComercialClassifications/Form.vue
Normal file
68
src/pages/ComercialClassifications/Form.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
import { transl } from './Module';
|
||||||
|
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import Selectable from '@Holos/Form/Selectable.vue';
|
||||||
|
import Checkbox from '@Holos/Checkbox.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'submit'
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
defineProps({
|
||||||
|
action: {
|
||||||
|
default: 'create',
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
form: Object,
|
||||||
|
parentOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function submit() {
|
||||||
|
emit('submit')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm" v-text="transl(`${action}.description`)" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<form @submit.prevent="submit" class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
<Input
|
||||||
|
v-model="form.code"
|
||||||
|
id="code"
|
||||||
|
:onError="form.errors.code"
|
||||||
|
autofocus
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.name"
|
||||||
|
id="name"
|
||||||
|
:onError="form.errors.name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="form.description"
|
||||||
|
id="description"
|
||||||
|
:onError="form.errors.description"
|
||||||
|
class="md:col-span-2"
|
||||||
|
/>
|
||||||
|
<slot />
|
||||||
|
<div class="col-span-1 md:col-span-2 lg:col-span-3 xl:col-span-4 flex flex-col items-center justify-end space-y-4 mt-4">
|
||||||
|
<PrimaryButton
|
||||||
|
v-text="$t(action)"
|
||||||
|
:class="{ 'opacity-25': form.processing }"
|
||||||
|
:disabled="form.processing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
204
src/pages/ComercialClassifications/Index.vue
Normal file
204
src/pages/ComercialClassifications/Index.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter, RouterLink } from 'vue-router';
|
||||||
|
import { useSearcher } from '@Services/Api';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
import { can, apiTo, viewTo, transl } from './Module'
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
|
import SearcherHead from '@Holos/Searcher.vue';
|
||||||
|
import Table from '@Holos/Table.vue';
|
||||||
|
import ShowView from './Modals/Show.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Button from '@Holos/Button/Button.vue';
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const models = ref([]);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Referencias */
|
||||||
|
const showModal = ref(null);
|
||||||
|
const destroyModal = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiTo('index'),
|
||||||
|
onSuccess: (r) => {
|
||||||
|
console.log('Datos recibidos:', r);
|
||||||
|
// La API retorna los datos en r.data.comercial_classifications.data
|
||||||
|
const classificationsData = r.data?.comercial_classifications?.data || r.comercial_classifications || [];
|
||||||
|
models.value = classificationsData;
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
models.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Función para renderizar estructura jerárquica */
|
||||||
|
const renderHierarchicalRows = (items, level = 0) => {
|
||||||
|
const rows = [];
|
||||||
|
items.forEach(item => {
|
||||||
|
rows.push({ ...item, level });
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
rows.push(...renderHierarchicalRows(item.children, level + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Función para crear subcategoría */
|
||||||
|
const createSubcategory = (parentCategory) => {
|
||||||
|
// Redirigir a la vista de creación con el parent_id en query params
|
||||||
|
router.push(viewTo({
|
||||||
|
name: 'create',
|
||||||
|
query: {
|
||||||
|
parent_id: parentCategory.id,
|
||||||
|
parent_name: parentCategory.name,
|
||||||
|
parent_code: parentCategory.code
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Función para eliminar con información contextual */
|
||||||
|
const deleteItem = (item) => {
|
||||||
|
// Agregar información contextual al modal
|
||||||
|
const itemWithContext = {
|
||||||
|
...item,
|
||||||
|
contextInfo: item.parent ? `Subcategoría de: ${item.parent.code} - ${item.parent.name}` : 'Categoría principal'
|
||||||
|
};
|
||||||
|
destroyModal.value.open(itemWithContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Función para mostrar detalles */
|
||||||
|
const showItem = (item) => {
|
||||||
|
showModal.value.open(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
searcher.search();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-foreground">Clasificaciones Comerciales</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Gestión de categorías y subcategorías de productos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<RouterLink :to="viewTo({ name: 'create' })">
|
||||||
|
<Button color="info">Nueva Clasificación</Button>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<!-- <SearcherHead :title="transl('name')" @search="(x) => searcher.search(x)">
|
||||||
|
<RouterLink :to="viewTo({ name: 'create' })">
|
||||||
|
|
||||||
|
<IconButton class="text-white" icon="add" :title="$t('crud.create')" filled />
|
||||||
|
</RouterLink>
|
||||||
|
<IconButton icon="refresh" :title="$t('refresh')" @click="searcher.search()" />
|
||||||
|
</SearcherHead> -->
|
||||||
|
<div class="pt-2 w-full">
|
||||||
|
<Table :items="models" :processing="searcher.processing"
|
||||||
|
@send-pagination="(page) => searcher.pagination(page)">
|
||||||
|
<template #head>
|
||||||
|
<th v-text="$t('code')" />
|
||||||
|
<th v-text="$t('name')" />
|
||||||
|
<th v-text="$t('description')" />
|
||||||
|
<th v-text="$t('status')" class="w-20 text-center" />
|
||||||
|
<th v-text="$t('actions')" class="w-32 text-center" />
|
||||||
|
</template>
|
||||||
|
<template #body="{ items }">
|
||||||
|
<tr v-for="model in items" :key="model.id" class="table-row"
|
||||||
|
:class="{ 'bg-gray-50 dark:bg-gray-800': model.level > 0 }">
|
||||||
|
<td class="table-cell">
|
||||||
|
<div :style="{ paddingLeft: `${model.level * 24}px` }" class="flex items-center">
|
||||||
|
<span v-if="model.level > 0" class="text-gray-400 mr-2">└─</span>
|
||||||
|
<code class="font-mono text-sm"
|
||||||
|
:class="model.level > 0 ? 'text-blue-600 dark:text-blue-400' : 'text-gray-900 dark:text-gray-100'">
|
||||||
|
{{ model.code }}
|
||||||
|
</code>
|
||||||
|
<span v-if="model.level > 0" class="ml-2 text-xs bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100 px-2 py-1 rounded-full">
|
||||||
|
Subcategoría
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell">
|
||||||
|
<span class="font-semibold">{{ model.name }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ model.description || '-' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell text-center">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium"
|
||||||
|
:class="model.is_active
|
||||||
|
? 'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100'
|
||||||
|
: 'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100'"
|
||||||
|
>
|
||||||
|
{{ model.is_active ? $t('active') : $t('inactive') }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell">
|
||||||
|
<div class="table-actions">
|
||||||
|
<IconButton
|
||||||
|
icon="add_circle"
|
||||||
|
:title="$t('Agregar subcategoría')"
|
||||||
|
@click="createSubcategory(model)"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon="visibility"
|
||||||
|
:title="$t('crud.show')"
|
||||||
|
@click="showItem(model)"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
<RouterLink
|
||||||
|
class="h-fit"
|
||||||
|
:to="viewTo({ name: 'edit', params: { id: model.id } })"
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
icon="edit"
|
||||||
|
:title="$t('crud.edit')"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
<IconButton
|
||||||
|
icon="delete"
|
||||||
|
:title="$t('crud.destroy')"
|
||||||
|
@click="deleteItem(model)"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<td class="table-cell">
|
||||||
|
<div class="flex items-center text-sm">
|
||||||
|
<GoogleIcon name="folder_open" class="mr-2 text-gray-400" />
|
||||||
|
<p class="font-semibold">
|
||||||
|
{{ $t('registers.empty') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="table-cell">-</td>
|
||||||
|
<td class="table-cell">-</td>
|
||||||
|
<td class="table-cell">-</td>
|
||||||
|
<td class="table-cell">-</td>
|
||||||
|
<td class="table-cell">-</td>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ShowView ref="showModal" @reload="searcher.search()" />
|
||||||
|
|
||||||
|
<DestroyView ref="destroyModal" subtitle="name" :to="(id) => apiTo('destroy', { comercial_classification: id })"
|
||||||
|
@update="searcher.search()" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
362
src/pages/ComercialClassifications/Modals/Show.vue
Normal file
362
src/pages/ComercialClassifications/Modals/Show.vue
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { getDateTime } from '@Controllers/DateController';
|
||||||
|
import { viewTo, apiTo } from '../Module';
|
||||||
|
import ComercialClassificationsService from '../services/ComercialClassificationsService';
|
||||||
|
import Notify from '@Plugins/Notify';
|
||||||
|
|
||||||
|
import Header from '@Holos/Modal/Elements/Header.vue';
|
||||||
|
import ShowModal from '@Holos/Modal/Show.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Button from '@Holos/Button/Button.vue';
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue';
|
||||||
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'reload'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Servicios */
|
||||||
|
const classificationsService = new ComercialClassificationsService();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const model = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const deletingChildId = ref(null); // Para mostrar loading específico en subcategoría que se está eliminando
|
||||||
|
|
||||||
|
/** Referencias */
|
||||||
|
const modalRef = ref(null);
|
||||||
|
const destroyModal = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function close() {
|
||||||
|
model.value = null;
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para actualizar el estado de la clasificación */
|
||||||
|
async function toggleStatus(item) {
|
||||||
|
if (loading.value) return;
|
||||||
|
|
||||||
|
const newStatus = !item.is_active;
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
// Usar el servicio para actualizar el estado
|
||||||
|
await classificationsService.updateStatus(item.id, newStatus);
|
||||||
|
|
||||||
|
// Actualizar el modelo local
|
||||||
|
item.is_active = newStatus;
|
||||||
|
|
||||||
|
// Notificación de éxito
|
||||||
|
const statusText = newStatus ? 'activada' : 'desactivada';
|
||||||
|
Notify.success(
|
||||||
|
`Clasificación "${item.code}" ${statusText} exitosamente`,
|
||||||
|
'Estado actualizado'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Emitir evento para recargar la lista principal si es necesario
|
||||||
|
emit('reload');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error actualizando estado:', error);
|
||||||
|
|
||||||
|
// Manejo específico de errores según la estructura de tu API
|
||||||
|
let errorMessage = 'Error al actualizar el estado de la clasificación';
|
||||||
|
let errorTitle = 'Error';
|
||||||
|
|
||||||
|
if (error?.response?.data) {
|
||||||
|
const errorData = error.response.data;
|
||||||
|
|
||||||
|
// Caso 1: Error con estructura específica de tu API
|
||||||
|
if (errorData.status === 'error') {
|
||||||
|
if (errorData.errors) {
|
||||||
|
// Errores de validación - extraer el primer error
|
||||||
|
const firstField = Object.keys(errorData.errors)[0];
|
||||||
|
const firstError = errorData.errors[firstField];
|
||||||
|
errorMessage = Array.isArray(firstError) ? firstError[0] : firstError;
|
||||||
|
errorTitle = 'Error de validación';
|
||||||
|
} else if (errorData.message) {
|
||||||
|
// Mensaje general del error
|
||||||
|
errorMessage = errorData.message;
|
||||||
|
errorTitle = 'Error del servidor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Caso 2: Otros formatos de error
|
||||||
|
else if (errorData.message) {
|
||||||
|
errorMessage = errorData.message;
|
||||||
|
}
|
||||||
|
} else if (error?.message) {
|
||||||
|
// Error genérico de la petición (red, timeout, etc.)
|
||||||
|
errorMessage = `Error de conexión: ${error.message}`;
|
||||||
|
errorTitle = 'Error de red';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notificación de error
|
||||||
|
Notify.error(errorMessage, errorTitle);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para editar subcategoría */
|
||||||
|
function editSubcategory(child) {
|
||||||
|
// Navegar a la vista de edición de la subcategoría
|
||||||
|
const editUrl = viewTo({ name: 'edit', params: { id: child.id } });
|
||||||
|
router.push(editUrl);
|
||||||
|
// Cerrar el modal actual
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para eliminar subcategoría */
|
||||||
|
function deleteSubcategory(child) {
|
||||||
|
// Marcar cuál subcategoría se va a eliminar para mostrar loading
|
||||||
|
deletingChildId.value = child.id;
|
||||||
|
destroyModal.value.open(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para recargar después de eliminar - Mejorada */
|
||||||
|
async function onSubcategoryDeleted() {
|
||||||
|
try {
|
||||||
|
// Mostrar que se está procesando
|
||||||
|
const deletedId = deletingChildId.value;
|
||||||
|
|
||||||
|
// Recargar los datos de la clasificación actual para obtener subcategorías actualizadas
|
||||||
|
const response = await classificationsService.getById(model.value.id);
|
||||||
|
|
||||||
|
// Actualizar el modelo local con los datos frescos
|
||||||
|
if (response && response.comercial_classification) {
|
||||||
|
model.value = response.comercial_classification;
|
||||||
|
|
||||||
|
// Notificación de éxito con información específica
|
||||||
|
const deletedCount = model.value.children ? model.value.children.length : 0;
|
||||||
|
Notify.success(
|
||||||
|
`Subcategoría eliminada exitosamente. ${deletedCount} subcategorías restantes.`,
|
||||||
|
'Eliminación exitosa'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emitir evento para recargar la lista principal
|
||||||
|
emit('reload');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error recargando datos después de eliminar:', error);
|
||||||
|
|
||||||
|
// En caso de error, cerrar modal y recargar lista principal
|
||||||
|
close();
|
||||||
|
emit('reload');
|
||||||
|
|
||||||
|
// Notificación de error
|
||||||
|
Notify.error(
|
||||||
|
'Error al actualizar la vista. Los datos se han actualizado correctamente.',
|
||||||
|
'Error de actualización'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Limpiar el estado de eliminación
|
||||||
|
deletingChildId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para cancelar eliminación */
|
||||||
|
function onDeleteCancelled() {
|
||||||
|
// Limpiar el estado de eliminación si se cancela
|
||||||
|
deletingChildId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Función para renderizar la jerarquía de hijos */
|
||||||
|
const renderChildren = (children, level = 1) => {
|
||||||
|
if (!children || children.length === 0) return null;
|
||||||
|
return children.map(child => ({
|
||||||
|
...child,
|
||||||
|
level,
|
||||||
|
children: renderChildren(child.children, level + 1)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
open: (data) => {
|
||||||
|
model.value = data;
|
||||||
|
modalRef.value.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ShowModal
|
||||||
|
ref="modalRef"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<div v-if="model">
|
||||||
|
<Header
|
||||||
|
:title="model.code"
|
||||||
|
:subtitle="model.name"
|
||||||
|
>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<div class="flex w-full justify-center items-center">
|
||||||
|
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-white text-3xl"
|
||||||
|
name="category"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
<div class="flex w-full p-4 space-y-6">
|
||||||
|
<!-- Información básica -->
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl text-success mt-1"
|
||||||
|
name="info"
|
||||||
|
/>
|
||||||
|
<div class="pl-3 w-full">
|
||||||
|
<p class="font-bold text-lg leading-none pb-3">
|
||||||
|
{{ $t('details') }}
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('code') }}: </b>
|
||||||
|
<code class="font-mono text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
|
||||||
|
{{ model.code }}
|
||||||
|
</code>
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
<b>{{ $t('name') }}: </b>
|
||||||
|
{{ model.name }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2" v-if="model.description">
|
||||||
|
<b>{{ $t('description') }}: </b>
|
||||||
|
{{ model.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('status') }}: </b>
|
||||||
|
<Button
|
||||||
|
:variant="'smooth'"
|
||||||
|
:color="model.is_active ? 'success' : 'danger'"
|
||||||
|
:size="'sm'"
|
||||||
|
:loading="loading"
|
||||||
|
@click="toggleStatus(model)"
|
||||||
|
>
|
||||||
|
{{ model.is_active ? $t('Activo') : $t('Inactivo') }}
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
<b>{{ $t('created_at') }}: </b>
|
||||||
|
{{ getDateTime(model.created_at) }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
<b>{{ $t('updated_at') }}: </b>
|
||||||
|
{{ getDateTime(model.updated_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Clasificaciones hijas -->
|
||||||
|
<div v-if="model.children && model.children.length > 0" class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl text-primary mt-1"
|
||||||
|
name="account_tree"
|
||||||
|
/>
|
||||||
|
<div class="pl-3 w-full">
|
||||||
|
<p class="font-bold text-lg leading-none pb-3">
|
||||||
|
{{ $t('Subclasificaciones') }}
|
||||||
|
</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="child in model.children"
|
||||||
|
:key="child.id"
|
||||||
|
class="flex items-center p-3 bg-gray-50 dark:bg-gray-800 rounded-lg transition-all duration-300"
|
||||||
|
:class="{
|
||||||
|
'opacity-50 pointer-events-none': deletingChildId === child.id,
|
||||||
|
'bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700': deletingChildId === child.id
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<code class="font-mono text-sm bg-white dark:bg-gray-700 px-2 py-1 rounded mr-3">
|
||||||
|
{{ child.code }}
|
||||||
|
</code>
|
||||||
|
<span class="font-semibold">{{ child.name }}</span>
|
||||||
|
<!-- Indicador de eliminación -->
|
||||||
|
<span v-if="deletingChildId === child.id" class="ml-2 text-xs bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100 px-2 py-1 rounded-full animate-pulse">
|
||||||
|
Eliminando...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="child.description" class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{{ child.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botones de acción para subcategorías -->
|
||||||
|
<div class="flex items-center space-x-2 ml-4">
|
||||||
|
<!-- Botón de estado -->
|
||||||
|
<Button
|
||||||
|
:variant="'smooth'"
|
||||||
|
:color="child.is_active ? 'success' : 'danger'"
|
||||||
|
:size="'sm'"
|
||||||
|
:loading="loading && deletingChildId !== child.id"
|
||||||
|
:disabled="deletingChildId === child.id"
|
||||||
|
@click="toggleStatus(child)"
|
||||||
|
>
|
||||||
|
{{ child.is_active ? $t('Activo') : $t('Inactivo') }}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Botón editar -->
|
||||||
|
<IconButton
|
||||||
|
icon="edit"
|
||||||
|
:title="$t('crud.edit')"
|
||||||
|
:disabled="deletingChildId === child.id"
|
||||||
|
@click="editSubcategory(child)"
|
||||||
|
outline
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Botón eliminar -->
|
||||||
|
<IconButton
|
||||||
|
icon="delete"
|
||||||
|
:title="$t('crud.destroy')"
|
||||||
|
:loading="deletingChildId === child.id"
|
||||||
|
:disabled="deletingChildId && deletingChildId !== child.id"
|
||||||
|
@click="deleteSubcategory(child)"
|
||||||
|
outline
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje cuando no hay subcategorías (después de eliminar todas) -->
|
||||||
|
<div v-if="!model.children || model.children.length === 0"
|
||||||
|
class="text-center p-4 text-gray-500 dark:text-gray-400">
|
||||||
|
<GoogleIcon class="text-2xl mb-2" name="folder_open" />
|
||||||
|
<p class="text-sm">No hay subcategorías</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal de eliminación para subcategorías -->
|
||||||
|
<DestroyView
|
||||||
|
ref="destroyModal"
|
||||||
|
subtitle="name"
|
||||||
|
:to="(id) => apiTo('destroy', { comercial_classification: id })"
|
||||||
|
@update="onSubcategoryDeleted"
|
||||||
|
/>
|
||||||
|
</ShowModal>
|
||||||
|
</template>
|
||||||
23
src/pages/ComercialClassifications/Module.js
Normal file
23
src/pages/ComercialClassifications/Module.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { lang } from '@Lang/i18n';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`comercial-classifications.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => view({
|
||||||
|
name: `admin.comercial-classifications.${name}`, params, query
|
||||||
|
})
|
||||||
|
|
||||||
|
// Obtener traducción del componente
|
||||||
|
const transl = (str) => lang(`admin.comercial_classifications.${str}`)
|
||||||
|
|
||||||
|
// Control de permisos
|
||||||
|
const can = (permission) => hasPermission(`admin.comercial-classifications.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo,
|
||||||
|
transl
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Interfaces para Comercial Classifications
|
||||||
|
*
|
||||||
|
* @author Sistema
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ComercialClassification
|
||||||
|
* @property {number} id - ID de la clasificación
|
||||||
|
* @property {string} code - Código de la clasificación
|
||||||
|
* @property {string} name - Nombre de la clasificación
|
||||||
|
* @property {string|null} description - Descripción de la clasificación
|
||||||
|
* @property {boolean} is_active - Estado activo/inactivo
|
||||||
|
* @property {number|null} parent_id - ID del padre
|
||||||
|
* @property {string} created_at - Fecha de creación
|
||||||
|
* @property {string} updated_at - Fecha de actualización
|
||||||
|
* @property {string|null} deleted_at - Fecha de eliminación
|
||||||
|
* @property {ComercialClassification|null} parent - Clasificación padre
|
||||||
|
* @property {ComercialClassification[]} children - Clasificaciones hijas
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ComercialClassificationResponse
|
||||||
|
* @property {string} status - Estado de la respuesta
|
||||||
|
* @property {Object} data - Datos de la respuesta
|
||||||
|
* @property {string} data.message - Mensaje de la respuesta
|
||||||
|
* @property {ComercialClassification} data.comercial_classification - Clasificación comercial
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ComercialClassificationsListResponse
|
||||||
|
* @property {string} status - Estado de la respuesta
|
||||||
|
* @property {Object} data - Datos de la respuesta
|
||||||
|
* @property {ComercialClassification[]} data.comercial_classifications - Lista de clasificaciones
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} CreateComercialClassificationData
|
||||||
|
* @property {string} code - Código de la clasificación
|
||||||
|
* @property {string} name - Nombre de la clasificación
|
||||||
|
* @property {string|null} description - Descripción de la clasificación
|
||||||
|
* @property {boolean} is_active - Estado activo/inactivo
|
||||||
|
* @property {number|null} parent_id - ID del padre
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} UpdateComercialClassificationData
|
||||||
|
* @property {string} [code] - Código de la clasificación
|
||||||
|
* @property {string} [name] - Nombre de la clasificación
|
||||||
|
* @property {string|null} [description] - Descripción de la clasificación
|
||||||
|
* @property {boolean} [is_active] - Estado activo/inactivo
|
||||||
|
* @property {number|null} [parent_id] - ID del padre
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
ComercialClassification,
|
||||||
|
ComercialClassificationResponse,
|
||||||
|
ComercialClassificationsListResponse,
|
||||||
|
CreateComercialClassificationData,
|
||||||
|
UpdateComercialClassificationData
|
||||||
|
};
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Servicio para Comercial Classifications
|
||||||
|
*
|
||||||
|
* @author Sistema
|
||||||
|
* @version 2.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { api, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
export default class ComercialClassificationsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener todas las clasificaciones comerciales
|
||||||
|
* @param {Object} params - Parámetros de la consulta
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async getAll(params = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.get(apiURL('comercial-classifications'), {
|
||||||
|
params,
|
||||||
|
onSuccess: (response) => resolve(response),
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener una clasificación comercial por ID
|
||||||
|
* @param {number} id - ID de la clasificación
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async getById(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.get(apiURL(`comercial-classifications/${id}`), {
|
||||||
|
onSuccess: (response) => resolve(response),
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear una nueva clasificación comercial
|
||||||
|
* @param {Object} data - Datos de la clasificación
|
||||||
|
* @param {string} data.code - Código de la clasificación
|
||||||
|
* @param {string} data.name - Nombre de la clasificación
|
||||||
|
* @param {string} data.description - Descripción de la clasificación
|
||||||
|
* @param {number|null} data.parent_id - ID de la clasificación padre (null para raíz)
|
||||||
|
* @param {boolean} data.is_active - Estado activo/inactivo
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async create(data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.post(apiURL('comercial-classifications'), {
|
||||||
|
data,
|
||||||
|
onSuccess: (response) => resolve(response),
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar una clasificación comercial existente
|
||||||
|
* @param {number} id - ID de la clasificación
|
||||||
|
* @param {Object} data - Datos actualizados
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async update(id, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.put(apiURL(`comercial-classifications/${id}`), {
|
||||||
|
data,
|
||||||
|
onSuccess: (response) => resolve(response),
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar una clasificación comercial
|
||||||
|
* @param {number} id - ID de la clasificación
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async delete(id) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.delete(apiURL(`comercial-classifications/${id}`), {
|
||||||
|
onSuccess: (response) => resolve(response),
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar solo el estado de una clasificación comercial
|
||||||
|
* @param {number} id - ID de la clasificación
|
||||||
|
* @param {boolean} is_active - Nuevo estado
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async updateStatus(id, is_active) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.put(apiURL(`comercial-classifications/${id}`), {
|
||||||
|
data: { is_active },
|
||||||
|
onSuccess: (response) => {
|
||||||
|
resolve(response);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
// Mejorar el manejo de errores
|
||||||
|
const enhancedError = {
|
||||||
|
...error,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
action: 'updateStatus',
|
||||||
|
id: id,
|
||||||
|
is_active: is_active
|
||||||
|
};
|
||||||
|
reject(enhancedError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener clasificaciones padre disponibles (para selects)
|
||||||
|
* @param {number|null} excludeId - ID a excluir de la lista (para evitar loops)
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async getParentOptions(excludeId = null) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
api.get(apiURL('comercial-classifications'), {
|
||||||
|
params: { exclude_children_of: excludeId },
|
||||||
|
onSuccess: (response) => {
|
||||||
|
// Aplanar la estructura jerárquica para el selector
|
||||||
|
const flattenOptions = (items, level = 0) => {
|
||||||
|
let options = [];
|
||||||
|
items.forEach(item => {
|
||||||
|
if (excludeId && (item.id === excludeId || this.hasDescendant(item, excludeId))) {
|
||||||
|
return; // Excluir item actual y sus descendientes
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
...item,
|
||||||
|
name: ' '.repeat(level) + item.name, // Indentación visual
|
||||||
|
level
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
options = options.concat(flattenOptions(item.children, level + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flatOptions = flattenOptions(response.comercial_classifications?.data || response.data || []);
|
||||||
|
resolve(flatOptions);
|
||||||
|
},
|
||||||
|
onError: (error) => reject(error)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Función auxiliar para verificar si un item tiene como descendiente un ID específico
|
||||||
|
* @param {Object} item - Item a verificar
|
||||||
|
* @param {number} targetId - ID objetivo
|
||||||
|
* @returns {boolean} True si es descendiente
|
||||||
|
*/
|
||||||
|
hasDescendant(item, targetId) {
|
||||||
|
if (!item.children) return false;
|
||||||
|
return item.children.some(child =>
|
||||||
|
child.id === targetId || this.hasDescendant(child, targetId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternar el estado de una clasificación
|
||||||
|
* @param {Object} item - Objeto con la clasificación
|
||||||
|
* @returns {Promise} Promesa con la respuesta
|
||||||
|
*/
|
||||||
|
async toggleStatus(item) {
|
||||||
|
const newStatus = !item.is_active;
|
||||||
|
return this.updateStatus(item.id, newStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -494,6 +494,40 @@ const router = createRouter({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'comercial-classifications',
|
||||||
|
name: 'admin.comercial-classifications',
|
||||||
|
meta: {
|
||||||
|
title: 'Clasificaciones Comerciales',
|
||||||
|
icon: 'category',
|
||||||
|
},
|
||||||
|
redirect: '/admin/comercial-classifications',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'admin.comercial-classifications.index',
|
||||||
|
component: () => import('@Pages/ComercialClassifications/Index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'create',
|
||||||
|
name: 'admin.comercial-classifications.create',
|
||||||
|
component: () => import('@Pages/ComercialClassifications/Create.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'Crear',
|
||||||
|
icon: 'add',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit',
|
||||||
|
name: 'admin.comercial-classifications.edit',
|
||||||
|
component: () => import('@Pages/ComercialClassifications/Edit.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'Editar',
|
||||||
|
icon: 'edit',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'roles',
|
path: 'roles',
|
||||||
name: 'admin.roles',
|
name: 'admin.roles',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user