feat: agregar subcategorias y modificar vistas
This commit is contained in:
parent
b2dea0785e
commit
1909ebec68
@ -452,7 +452,7 @@ export default {
|
|||||||
pos: {
|
pos: {
|
||||||
title: 'Punto de Venta',
|
title: 'Punto de Venta',
|
||||||
subtitle: 'Gestión de ventas y caja',
|
subtitle: 'Gestión de ventas y caja',
|
||||||
category: 'Categorías',
|
category: 'Clasificaciones',
|
||||||
bundles: 'Paquetes',
|
bundles: 'Paquetes',
|
||||||
inventory: 'Productos',
|
inventory: 'Productos',
|
||||||
prices: 'Precios',
|
prices: 'Precios',
|
||||||
@ -512,13 +512,13 @@ export default {
|
|||||||
inactive: 'Inactivo',
|
inactive: 'Inactivo',
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
title: 'Gestión De Categorías',
|
title: 'Gestión De Clasificaciones',
|
||||||
description: 'Administra las categorías de productos.',
|
description: 'Administra las clasificaciones de productos.',
|
||||||
create: {
|
create: {
|
||||||
title: 'Nueva Categoría',
|
title: 'Nueva Clasificación',
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
title: 'Editar Categoría',
|
title: 'Editar Clasificación',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
prices: {
|
prices: {
|
||||||
|
|||||||
@ -23,9 +23,9 @@ const form = useForm({
|
|||||||
/** Métodos */
|
/** Métodos */
|
||||||
const createCategory = () => {
|
const createCategory = () => {
|
||||||
form.post(apiURL('categorias'), {
|
form.post(apiURL('categorias'), {
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
Notify.success('Categoría creada exitosamente');
|
Notify.success('Categoría creada exitosamente');
|
||||||
emit('created');
|
emit('created', data?.model);
|
||||||
closeModal();
|
closeModal();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import { useSearcher, apiURL } from '@Services/Api';
|
import { useSearcher, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
import SearcherHead from '@Holos/Searcher.vue';
|
import SearcherHead from '@Holos/Searcher.vue';
|
||||||
@ -9,6 +10,8 @@ import CreateModal from './CreateModal.vue';
|
|||||||
import EditModal from './EditModal.vue';
|
import EditModal from './EditModal.vue';
|
||||||
import DeleteModal from './DeleteModal.vue';
|
import DeleteModal from './DeleteModal.vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const models = ref([]);
|
const models = ref([]);
|
||||||
const showCreateModal = ref(false);
|
const showCreateModal = ref(false);
|
||||||
@ -163,6 +166,13 @@ onMounted(() => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="router.push({ name: 'pos.category.subcategories', params: { id: model.id } })"
|
||||||
|
class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
||||||
|
title="Gestionar subcategorías"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="account_tree" class="text-xl" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openEditModal(model)"
|
@click="openEditModal(model)"
|
||||||
class="text-indigo-600 hover:text-indigo-900 transition-colors"
|
class="text-indigo-600 hover:text-indigo-900 transition-colors"
|
||||||
|
|||||||
131
src/pages/POS/Category/Subcategories/CreateModal.vue
Normal file
131
src/pages/POS/Category/Subcategories/CreateModal.vue
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useForm, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import FormInput from '@Holos/Form/Input.vue';
|
||||||
|
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['close', 'created']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
categoryId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Formulario */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const createSubcategory = () => {
|
||||||
|
form.post(apiURL(`categorias/${props.categoryId}/subcategorias`), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
Notify.success('Subcategoría creada exitosamente');
|
||||||
|
emit('created', data?.model);
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Notify.error('Error al crear la subcategoría');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="closeModal">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Crear Subcategoría
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario -->
|
||||||
|
<form @submit.prevent="createSubcategory" class="space-y-4">
|
||||||
|
<!-- Nombre -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
NOMBRE
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre de la subcategoría"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descripción -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
DESCRIPCIÓN
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="3"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
placeholder="Descripción de la subcategoría"
|
||||||
|
></textarea>
|
||||||
|
<FormError :message="form.errors?.description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
ESTADO
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="form.is_active"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<option :value="true">Activo</option>
|
||||||
|
<option :value="false">Inactivo</option>
|
||||||
|
</select>
|
||||||
|
<FormError :message="form.errors?.is_active" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botones -->
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="form.processing"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<span v-if="form.processing">Guardando...</span>
|
||||||
|
<span v-else>Guardar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
117
src/pages/POS/Category/Subcategories/DeleteModal.vue
Normal file
117
src/pages/POS/Category/Subcategories/DeleteModal.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script setup>
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
subcategory: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Emits */
|
||||||
|
const emit = defineEmits(['close', 'confirm']);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', props.subcategory.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="handleClose">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-red-100 dark:bg-red-900/30">
|
||||||
|
<GoogleIcon name="delete_forever" class="text-2xl text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Eliminar Subcategoría
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="handleClose"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="space-y-5">
|
||||||
|
<p class="text-gray-700 dark:text-gray-300 text-base">
|
||||||
|
¿Estás seguro de que deseas eliminar esta subcategoría?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="subcategory" class="bg-linear-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl p-5 space-y-2">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-indigo-100 dark:bg-indigo-900/30">
|
||||||
|
<GoogleIcon name="category" class="text-xl text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-base font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
{{ subcategory.name }}
|
||||||
|
</p>
|
||||||
|
<p v-if="subcategory.description" class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">
|
||||||
|
{{ subcategory.description }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-sm text-gray-500 dark:text-gray-500 italic mt-0.5">
|
||||||
|
Sin descripción
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="subcategory.is_active !== undefined">
|
||||||
|
<span
|
||||||
|
class="inline-flex px-3 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400': subcategory.is_active,
|
||||||
|
'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400': !subcategory.is_active
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ subcategory.is_active ? 'Activo' : 'Inactivo' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-start gap-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||||
|
<GoogleIcon name="info" class="text-red-600 dark:text-red-400 text-xl shrink-0 mt-0.5" />
|
||||||
|
<p class="text-sm text-red-800 dark:text-red-300 font-medium">
|
||||||
|
Los productos que tenían asignada esta subcategoría quedarán sin subcategoría automáticamente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleClose"
|
||||||
|
class="px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleConfirm"
|
||||||
|
class="flex items-center gap-2 px-5 py-2.5 bg-red-600 hover:bg-red-700 text-white text-sm font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-600 shadow-lg shadow-red-600/30 transition-all"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
Eliminar Subcategoría
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
142
src/pages/POS/Category/Subcategories/EditModal.vue
Normal file
142
src/pages/POS/Category/Subcategories/EditModal.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useForm, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import FormInput from '@Holos/Form/Input.vue';
|
||||||
|
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits(['close', 'updated']);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean,
|
||||||
|
categoryId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
subcategory: Object
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Formulario */
|
||||||
|
const form = useForm({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const updateSubcategory = () => {
|
||||||
|
form.put(apiURL(`categorias/${props.categoryId}/subcategorias/${props.subcategory.id}`), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success('Subcategoría actualizada exitosamente');
|
||||||
|
emit('updated');
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
Notify.error('Error al actualizar la subcategoría');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
form.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Observadores */
|
||||||
|
watch(() => props.subcategory, (newSubcategory) => {
|
||||||
|
if (newSubcategory) {
|
||||||
|
form.name = newSubcategory.name || '';
|
||||||
|
form.description = newSubcategory.description || '';
|
||||||
|
form.is_active = newSubcategory.is_active ?? true;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="md" @close="closeModal">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Editar Subcategoría
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario -->
|
||||||
|
<form @submit.prevent="updateSubcategory" class="space-y-4">
|
||||||
|
<!-- Nombre -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
NOMBRE
|
||||||
|
</label>
|
||||||
|
<FormInput
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nombre de la subcategoría"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<FormError :message="form.errors?.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Descripción -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
DESCRIPCIÓN
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.description"
|
||||||
|
rows="3"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
placeholder="Descripción de la subcategoría"
|
||||||
|
></textarea>
|
||||||
|
<FormError :message="form.errors?.description" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Estado -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
ESTADO
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="form.is_active"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<option :value="true">Activo</option>
|
||||||
|
<option :value="false">Inactivo</option>
|
||||||
|
</select>
|
||||||
|
<FormError :message="form.errors?.is_active" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botones -->
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-6">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-colors"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="form.processing"
|
||||||
|
class="px-4 py-2 text-sm font-semibold text-white bg-gray-900 rounded-lg hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<span v-if="form.processing">Actualizando...</span>
|
||||||
|
<span v-else>Actualizar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
236
src/pages/POS/Category/Subcategories/Index.vue
Normal file
236
src/pages/POS/Category/Subcategories/Index.vue
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useSearcher, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Table from '@Holos/Table.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import CreateModal from './CreateModal.vue';
|
||||||
|
import EditModal from './EditModal.vue';
|
||||||
|
import DeleteModal from './DeleteModal.vue';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const categoryId = route.params.id;
|
||||||
|
|
||||||
|
/** Estado */
|
||||||
|
const models = ref([]);
|
||||||
|
const categoryName = ref('');
|
||||||
|
const showCreateModal = ref(false);
|
||||||
|
const showEditModal = ref(false);
|
||||||
|
const showDeleteModal = ref(false);
|
||||||
|
const editingSubcategory = ref(null);
|
||||||
|
const deletingSubcategory = ref(null);
|
||||||
|
|
||||||
|
/** Cargar nombre de la categoría padre */
|
||||||
|
const loadCategory = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiURL(`categorias/${categoryId}`), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.data?.model) {
|
||||||
|
categoryName.value = result.data.model.name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading category:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiURL(`categorias/${categoryId}/subcategorias`),
|
||||||
|
onSuccess: (r) => {
|
||||||
|
models.value = r.subcategories || { data: [], total: 0 };
|
||||||
|
},
|
||||||
|
onError: () => models.value = { data: [], total: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
showCreateModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateModal = () => {
|
||||||
|
showCreateModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (subcategory) => {
|
||||||
|
editingSubcategory.value = subcategory;
|
||||||
|
showEditModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditModal = () => {
|
||||||
|
showEditModal.value = false;
|
||||||
|
editingSubcategory.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (subcategory) => {
|
||||||
|
deletingSubcategory.value = subcategory;
|
||||||
|
showDeleteModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDeleteModal = () => {
|
||||||
|
showDeleteModal.value = false;
|
||||||
|
deletingSubcategory.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubcategorySaved = () => {
|
||||||
|
searcher.search();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async (id) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiURL(`categorias/${categoryId}/subcategorias/${id}`), {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
Notify.success('Subcategoría eliminada exitosamente');
|
||||||
|
closeDeleteModal();
|
||||||
|
searcher.search();
|
||||||
|
} else {
|
||||||
|
Notify.error('Error al eliminar la subcategoría');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
Notify.error('Error al eliminar la subcategoría');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ciclos */
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategory();
|
||||||
|
searcher.search();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
@click="router.push({ name: 'pos.category.index' })"
|
||||||
|
class="flex items-center justify-center w-8 h-8 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
title="Volver a categorías"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="arrow_back" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">Categoría</p>
|
||||||
|
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
{{ categoryName || 'Subcategorías' }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 px-3 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-lg transition-colors shadow-sm"
|
||||||
|
@click="openCreateModal"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="add" class="text-xl" />
|
||||||
|
Nueva Subcategoría
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modales -->
|
||||||
|
<CreateModal
|
||||||
|
:show="showCreateModal"
|
||||||
|
:category-id="categoryId"
|
||||||
|
@close="closeCreateModal"
|
||||||
|
@created="onSubcategorySaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditModal
|
||||||
|
:show="showEditModal"
|
||||||
|
:category-id="categoryId"
|
||||||
|
:subcategory="editingSubcategory"
|
||||||
|
@close="closeEditModal"
|
||||||
|
@updated="onSubcategorySaved"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteModal
|
||||||
|
:show="showDeleteModal"
|
||||||
|
:subcategory="deletingSubcategory"
|
||||||
|
@close="closeDeleteModal"
|
||||||
|
@confirm="confirmDelete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<div class="pt-2 w-full">
|
||||||
|
<Table
|
||||||
|
:items="models"
|
||||||
|
:processing="searcher.processing"
|
||||||
|
@send-pagination="(page) => searcher.pagination(page)"
|
||||||
|
>
|
||||||
|
<template #head>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">NOMBRE</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">DESCRIPCIÓN</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ESTADO</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACCIONES</th>
|
||||||
|
</template>
|
||||||
|
<template #body="{items}">
|
||||||
|
<tr
|
||||||
|
v-for="model in items"
|
||||||
|
:key="model.id"
|
||||||
|
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ model.name }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300">{{ model.description || '-' }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
class="inline-flex px-3 py-1 text-xs font-semibold rounded-full"
|
||||||
|
:class="{
|
||||||
|
'bg-green-50 text-green-700': model.is_active,
|
||||||
|
'bg-red-50 text-red-700': !model.is_active
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ model.is_active ? 'Activo' : 'Inactivo' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<button
|
||||||
|
@click="openEditModal(model)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-900 transition-colors"
|
||||||
|
title="Editar subcategoría"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="edit" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="openDeleteModal(model)"
|
||||||
|
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors"
|
||||||
|
title="Eliminar subcategoría"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="delete" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template #empty>
|
||||||
|
<td colspan="4" class="table-cell text-center">
|
||||||
|
<div class="flex flex-col items-center justify-center py-8 text-gray-500">
|
||||||
|
<GoogleIcon
|
||||||
|
name="category"
|
||||||
|
class="text-6xl mb-2 opacity-50"
|
||||||
|
/>
|
||||||
|
<p class="font-semibold">No hay subcategorías registradas</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -5,6 +5,9 @@ import { useForm, apiURL } from '@Services/Api';
|
|||||||
import Modal from '@Holos/Modal.vue';
|
import Modal from '@Holos/Modal.vue';
|
||||||
import FormInput from '@Holos/Form/Input.vue';
|
import FormInput from '@Holos/Form/Input.vue';
|
||||||
import FormError from '@Holos/Form/Elements/Error.vue';
|
import FormError from '@Holos/Form/Elements/Error.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import CategoryCreateModal from '@Pages/POS/Category/CreateModal.vue';
|
||||||
|
import SubcategoryCreateModal from '@Pages/POS/Category/Subcategories/CreateModal.vue';
|
||||||
|
|
||||||
/** Eventos */
|
/** Eventos */
|
||||||
const emit = defineEmits(['close', 'created']);
|
const emit = defineEmits(['close', 'created']);
|
||||||
@ -16,7 +19,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const categories = ref([]);
|
const categories = ref([]);
|
||||||
|
const subcategories = ref([]);
|
||||||
const units = ref([]);
|
const units = ref([]);
|
||||||
|
const showCategoryCreate = ref(false);
|
||||||
|
const showSubcategoryCreate = ref(false);
|
||||||
|
|
||||||
/** Formulario */
|
/** Formulario */
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -25,6 +31,7 @@ const form = useForm({
|
|||||||
sku: '',
|
sku: '',
|
||||||
barcode: '',
|
barcode: '',
|
||||||
category_id: '',
|
category_id: '',
|
||||||
|
subcategory_id: '',
|
||||||
unit_of_measure_id: null,
|
unit_of_measure_id: null,
|
||||||
retail_price: 0,
|
retail_price: 0,
|
||||||
tax: 16,
|
tax: 16,
|
||||||
@ -60,6 +67,12 @@ const loadCategories = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCategoryChange = () => {
|
||||||
|
const selected = categories.value.find(c => c.id == form.category_id);
|
||||||
|
subcategories.value = selected?.subcategories ?? [];
|
||||||
|
form.subcategory_id = '';
|
||||||
|
};
|
||||||
|
|
||||||
const loadUnits = async () => {
|
const loadUnits = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiURL('unidades-medida/active'), {
|
const response = await fetch(apiURL('unidades-medida/active'), {
|
||||||
@ -117,8 +130,23 @@ const createProduct = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCategoryCreated = async (newCategory) => {
|
||||||
|
if (newCategory) {
|
||||||
|
form.category_id = newCategory.id;
|
||||||
|
}
|
||||||
|
await loadCategories();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubcategoryCreated = (newSubcategory) => {
|
||||||
|
if (newSubcategory) {
|
||||||
|
subcategories.value.push(newSubcategory);
|
||||||
|
form.subcategory_id = newSubcategory.id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
subcategories.value = [];
|
||||||
emit('close');
|
emit('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -216,28 +244,72 @@ watch(() => form.track_serials, () => {
|
|||||||
<FormError :message="form.errors?.barcode" />
|
<FormError :message="form.errors?.barcode" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Categoría -->
|
<!-- Clasificación -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
CATEGORÍA
|
CLASIFICACIÓN
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div class="flex gap-2">
|
||||||
v-model="form.category_id"
|
<select
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
v-model="form.category_id"
|
||||||
required
|
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
>
|
required
|
||||||
<option value="">Seleccionar categoría</option>
|
@change="onCategoryChange"
|
||||||
<option
|
|
||||||
v-for="category in categories"
|
|
||||||
:key="category.id"
|
|
||||||
:value="category.id"
|
|
||||||
>
|
>
|
||||||
{{ category.name }}
|
<option value="">Seleccionar clasificación</option>
|
||||||
</option>
|
<option
|
||||||
</select>
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:value="category.id"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showCategoryCreate = true"
|
||||||
|
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0"
|
||||||
|
title="Crear nueva clasificación"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="add" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<FormError :message="form.errors?.category_id" />
|
<FormError :message="form.errors?.category_id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Subclasificación -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
SUBCLASIFICACIÓN
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<select
|
||||||
|
v-model="form.subcategory_id"
|
||||||
|
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="!subcategories.length"
|
||||||
|
>
|
||||||
|
<option value="">{{ subcategories.length ? 'Sin subclasificación' : 'Selecciona una clasificación primero' }}</option>
|
||||||
|
<option
|
||||||
|
v-for="subcategory in subcategories"
|
||||||
|
:key="subcategory.id"
|
||||||
|
:value="subcategory.id"
|
||||||
|
>
|
||||||
|
{{ subcategory.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="showSubcategoryCreate = true"
|
||||||
|
:disabled="!form.category_id"
|
||||||
|
class="flex items-center justify-center w-9 h-9 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-600 border border-indigo-200 dark:bg-indigo-900/20 dark:hover:bg-indigo-900/40 dark:text-indigo-400 dark:border-indigo-800 transition-colors shrink-0 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
title="Crear nueva subclasificación"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="add" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<FormError :message="form.errors?.subcategory_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Unidad de Medida -->
|
<!-- Unidad de Medida -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
@ -319,4 +391,17 @@ watch(() => form.track_serials, () => {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<CategoryCreateModal
|
||||||
|
:show="showCategoryCreate"
|
||||||
|
@close="showCategoryCreate = false"
|
||||||
|
@created="onCategoryCreated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SubcategoryCreateModal
|
||||||
|
:show="showSubcategoryCreate"
|
||||||
|
:category-id="form.category_id"
|
||||||
|
@close="showSubcategoryCreate = false"
|
||||||
|
@created="onSubcategoryCreated"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
/** Estado */
|
/** Estado */
|
||||||
const categories = ref([]);
|
const categories = ref([]);
|
||||||
|
const subcategories = ref([]);
|
||||||
const units = ref([]);
|
const units = ref([]);
|
||||||
const activeTab = ref('general');
|
const activeTab = ref('general');
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ const form = useForm({
|
|||||||
sku: '',
|
sku: '',
|
||||||
barcode: '',
|
barcode: '',
|
||||||
category_id: '',
|
category_id: '',
|
||||||
|
subcategory_id: '',
|
||||||
unit_of_measure_id: null,
|
unit_of_measure_id: null,
|
||||||
retail_price: 0,
|
retail_price: 0,
|
||||||
tax: 16,
|
tax: 16,
|
||||||
@ -92,12 +94,23 @@ const loadCategories = async () => {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.data && result.data.categories && result.data.categories.data) {
|
if (result.data && result.data.categories && result.data.categories.data) {
|
||||||
categories.value = result.data.categories.data;
|
categories.value = result.data.categories.data;
|
||||||
|
// Actualizar subcategorías si ya hay una categoría seleccionada
|
||||||
|
if (form.category_id) {
|
||||||
|
const selected = categories.value.find(c => c.id == form.category_id);
|
||||||
|
subcategories.value = selected?.subcategories ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading categories:', error);
|
console.error('Error loading categories:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCategoryChange = () => {
|
||||||
|
const selected = categories.value.find(c => c.id == form.category_id);
|
||||||
|
subcategories.value = selected?.subcategories ?? [];
|
||||||
|
form.subcategory_id = '';
|
||||||
|
};
|
||||||
|
|
||||||
const loadUnits = async () => {
|
const loadUnits = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiURL('unidades-medida/active'), {
|
const response = await fetch(apiURL('unidades-medida/active'), {
|
||||||
@ -275,6 +288,7 @@ watch(() => props.product, (newProduct) => {
|
|||||||
form.sku = newProduct.sku || '';
|
form.sku = newProduct.sku || '';
|
||||||
form.barcode = newProduct.barcode || '';
|
form.barcode = newProduct.barcode || '';
|
||||||
form.category_id = newProduct.category_id || '';
|
form.category_id = newProduct.category_id || '';
|
||||||
|
form.subcategory_id = newProduct.subcategory_id || '';
|
||||||
form.unit_of_measure_id = newProduct.unit_of_measure_id || null;
|
form.unit_of_measure_id = newProduct.unit_of_measure_id || null;
|
||||||
form.cost = parseFloat(newProduct.price?.cost || 0);
|
form.cost = parseFloat(newProduct.price?.cost || 0);
|
||||||
form.retail_price = parseFloat(newProduct.price?.retail_price || 0);
|
form.retail_price = parseFloat(newProduct.price?.retail_price || 0);
|
||||||
@ -285,11 +299,11 @@ watch(() => props.product, (newProduct) => {
|
|||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
watch(() => props.show, (newValue) => {
|
watch(() => props.show, async (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
loadCategories();
|
|
||||||
loadUnits();
|
loadUnits();
|
||||||
activeTab.value = 'general';
|
activeTab.value = 'general';
|
||||||
|
await loadCategories();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -424,14 +438,15 @@ watch(activeTab, (tab) => {
|
|||||||
<!-- Categoría -->
|
<!-- Categoría -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
CATEGORÍA
|
CLASIFICACIÓN
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
v-model="form.category_id"
|
v-model="form.category_id"
|
||||||
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100"
|
||||||
required
|
required
|
||||||
|
@change="onCategoryChange"
|
||||||
>
|
>
|
||||||
<option value="">Seleccionar categoría</option>
|
<option value="">Seleccionar clasificación</option>
|
||||||
<option
|
<option
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
@ -443,6 +458,28 @@ watch(activeTab, (tab) => {
|
|||||||
<FormError :message="form.errors?.category_id" />
|
<FormError :message="form.errors?.category_id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Subclasificación -->
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
SUBCLASIFICACIÓN
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
v-model="form.subcategory_id"
|
||||||
|
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="!subcategories.length"
|
||||||
|
>
|
||||||
|
<option value="">{{ subcategories.length ? 'Sin subclasificación' : 'Selecciona una clasificación primero' }}</option>
|
||||||
|
<option
|
||||||
|
v-for="subcategory in subcategories"
|
||||||
|
:key="subcategory.id"
|
||||||
|
:value="subcategory.id"
|
||||||
|
>
|
||||||
|
{{ subcategory.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<FormError :message="form.errors?.subcategory_id" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Unidad de Medida -->
|
<!-- Unidad de Medida -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||||
|
|||||||
@ -62,6 +62,23 @@ const loadCategories = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadSubcategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiURL('subcategorias'), {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.data && result.data.subcategories && result.data.subcategories.data) {
|
||||||
|
categories.value = result.data.subcategories.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar subcategorías:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
showCreateModal.value = true;
|
showCreateModal.value = true;
|
||||||
};
|
};
|
||||||
@ -199,6 +216,7 @@ const exportReport = async () => {
|
|||||||
/** Ciclos */
|
/** Ciclos */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCategories();
|
loadCategories();
|
||||||
|
loadSubcategories();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -322,7 +340,7 @@ onMounted(() => {
|
|||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">SKU / CÓDIGO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">SKU / CÓDIGO</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PRODUCTO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PRODUCTO</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CLAVE SAT</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CLAVE SAT</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CATEGORÍA</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">CLASIFICACIÓN</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PRECIO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">PRECIO</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">STOCK</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">STOCK</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TOTAL</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TOTAL</th>
|
||||||
@ -347,10 +365,13 @@ onMounted(() => {
|
|||||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ model.key_sat }}</p>
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ model.key_sat }}</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
<td class="px-6 py-4 whitespace-nowrap text-center flex flex-col items-center gap-1">
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{{ model.category?.name || '-' }}
|
{{ model.category?.name || '-' }}
|
||||||
</span>
|
</span>
|
||||||
|
<template v-if="model.subcategory">
|
||||||
|
<span class="text-gray-400 text-xs dark:text-gray-600"> {{ model.subcategory.name }} </span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
|
|||||||
@ -37,6 +37,11 @@ const router = createRouter({
|
|||||||
name: 'pos.category.index',
|
name: 'pos.category.index',
|
||||||
component: () => import('@Pages/POS/Category/Index.vue')
|
component: () => import('@Pages/POS/Category/Index.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'category/:id/subcategories',
|
||||||
|
name: 'pos.category.subcategories',
|
||||||
|
component: () => import('@Pages/POS/Category/Subcategories/Index.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'bundles',
|
path: 'bundles',
|
||||||
name: 'pos.bundles.index',
|
name: 'pos.bundles.index',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user