feat: actualizar terminología de categoría a clasificación en toda la aplicación

This commit is contained in:
Juan Felipe Zapata Moreno 2026-03-06 10:08:09 -06:00
parent fe79f843f6
commit b7154af381
18 changed files with 77 additions and 75 deletions

View File

@ -71,7 +71,7 @@ const handleAddToCart = () => {
<div class="flex items-center gap-1 mb-3">
<GoogleIcon name="category" class="text-sm text-gray-400" />
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ product.category?.name || 'Sin categoría' }}
{{ product.category?.name || 'Sin clasificación' }}
</span>
</div>

View File

@ -502,7 +502,7 @@ export default {
},
sku: 'SKU / Código',
product: 'Producto',
category: 'Categoría',
category: 'Clasificación',
stock: 'Stock',
state: 'Estado',
cost: 'Costo',

View File

@ -24,12 +24,12 @@ const form = useForm({
const createCategory = () => {
form.post(apiURL('categorias'), {
onSuccess: (data) => {
Notify.success('Categoría creada exitosamente');
Notify.success('Clasificación creada exitosamente');
emit('created', data?.model);
closeModal();
},
onError: () => {
Notify.error('Error al crear la categoría');
Notify.error('Error al crear la clasificación');
}
});
};
@ -46,7 +46,7 @@ const closeModal = () => {
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
Crear Categoría
Crear Clasificación
</h3>
<button
@click="closeModal"
@ -68,7 +68,7 @@ const closeModal = () => {
<FormInput
v-model="form.name"
type="text"
placeholder="Nombre de la categoría"
placeholder="Nombre de la clasificación"
required
/>
<FormError :message="form.errors?.name" />
@ -83,7 +83,7 @@ const closeModal = () => {
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 categoría"
placeholder="Descripción de la clasificación"
></textarea>
<FormError :message="form.errors?.description" />
</div>

View File

@ -37,7 +37,7 @@ const handleClose = () => {
<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 Categoría
Eliminar Clasificación
</h3>
</div>
<button
@ -53,7 +53,7 @@ const handleClose = () => {
<!-- 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 categoría?
¿Estás seguro de que deseas eliminar esta clasificación?
</p>
<div v-if="category" 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">
@ -89,7 +89,7 @@ const handleClose = () => {
<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">
Esta acción es permanente y no se puede deshacer. Los productos asociados a esta categoría perderán su categorización.
Esta acción es permanente y no se puede deshacer. Los productos asociados a esta clasificación perderán su clasificación.
</p>
</div>
</div>
@ -109,7 +109,7 @@ const handleClose = () => {
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 Categoría
Eliminar Clasificación
</button>
</div>
</div>

View File

@ -26,12 +26,12 @@ const form = useForm({
const updateCategory = () => {
form.put(apiURL(`categorias/${props.category.id}`), {
onSuccess: () => {
Notify.success('Categoría actualizada exitosamente');
Notify.success('Clasificación actualizada exitosamente');
emit('updated');
closeModal();
},
onError: () => {
Notify.error('Error al actualizar la categoría');
Notify.error('Error al actualizar la clasificación');
}
});
};
@ -57,7 +57,7 @@ watch(() => props.category, (newCategory) => {
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
Editar Categoría
Editar Clasificación
</h3>
<button
@click="closeModal"
@ -79,7 +79,7 @@ watch(() => props.category, (newCategory) => {
<FormInput
v-model="form.name"
type="text"
placeholder="Nombre de la categoría"
placeholder="Nombre de la clasificación"
required
/>
<FormError :message="form.errors?.name" />
@ -94,7 +94,7 @@ watch(() => props.category, (newCategory) => {
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 categoría"
placeholder="Descripción de la clasificación"
></textarea>
<FormError :message="form.errors?.description" />
</div>

View File

@ -72,15 +72,15 @@ const confirmDelete = async (id) => {
});
if (response.ok) {
Notify.success('Categoría eliminada exitosamente');
Notify.success('Clasificación eliminada exitosamente');
closeDeleteModal();
searcher.search();
} else {
Notify.error('Error al eliminar la categoría');
Notify.error('Error al eliminar la clasificación');
}
} catch (error) {
console.error('Error:', error);
Notify.error('Error al eliminar la categoría');
Notify.error('Error al eliminar la clasificación');
}
};
@ -102,7 +102,7 @@ onMounted(() => {
@click="openCreateModal"
>
<GoogleIcon name="add" class="text-xl" />
Nueva Categoría
Nueva Clasificación
</button>
</SearcherHead>
@ -169,7 +169,7 @@ onMounted(() => {
<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"
title="Gestionar subclasificaciones"
>
<GoogleIcon name="account_tree" class="text-xl" />
</button>
@ -182,7 +182,7 @@ onMounted(() => {
<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 categoría"
title="Eliminar clasificación"
>
<GoogleIcon name="delete" class="text-xl" />
</button>

View File

@ -43,14 +43,14 @@ const createSubcategory = () => {
onSuccess: (data) => {
Notify.success(
entries.value.length === 1
? 'Subcategoría creada exitosamente'
: 'Subcategorías creadas exitosamente'
? 'Subclasificación creada exitosamente'
: 'Subclasificaciones creadas exitosamente'
);
emit('created', data?.models ?? data?.model);
closeModal();
},
onError: () => {
Notify.error('Error al crear la subcategoría');
Notify.error('Error al crear la subclasificación');
}
});
};
@ -68,7 +68,7 @@ const closeModal = () => {
<!-- 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
Crear Subclasificación
</h3>
<button
@click="closeModal"
@ -92,7 +92,7 @@ const closeModal = () => {
>
<div v-if="entries.length > 1" class="flex items-center justify-between">
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">
Subcategoría {{ index + 1 }}
Subclasificación {{ index + 1 }}
</span>
<button
type="button"
@ -114,7 +114,7 @@ const closeModal = () => {
<FormInput
v-model="entry.name"
type="text"
placeholder="Nombre de la subcategoría"
placeholder="Nombre de la subclasificación"
required
/>
<FormError :message="form.errors?.[`${index}.name`] ?? form.errors?.name" />
@ -129,7 +129,7 @@ const closeModal = () => {
v-model="entry.description"
rows="2"
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"
placeholder="Descripción de la subclasificación"
></textarea>
<FormError :message="form.errors?.[`${index}.description`] ?? form.errors?.description" />
</div>
@ -159,7 +159,7 @@ const closeModal = () => {
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
Agregar otra subcategoría
Agregar otra subclasificación
</button>
<!-- Botones -->

View File

@ -28,13 +28,13 @@ const form = useForm({
const createSubcategory = () => {
form.post(apiURL(`categorias/${props.categoryId}/subcategorias`), {
onSuccess: (data) => {
Notify.success('Subcategoría creada exitosamente');
Notify.success('Subclasificación creada exitosamente');
const created = data?.model ?? (data?.models ? data.models[0] : []);
emit('created', created);
closeModal();
},
onError: () => {
Notify.error('Error al crear la subcategoría');
Notify.error('Error al crear la subclasificación');
}
});
};
@ -51,7 +51,7 @@ const closeModal = () => {
<!-- 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
Crear Subclasificación
</h3>
<button
@click="closeModal"
@ -73,7 +73,7 @@ const closeModal = () => {
<FormInput
v-model="form.name"
type="text"
placeholder="Nombre de la subcategoría"
placeholder="Nombre de la subclasificación"
required
/>
<FormError :message="form.errors?.name" />
@ -88,7 +88,7 @@ const closeModal = () => {
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"
placeholder="Descripción de la subclasificación"
></textarea>
<FormError :message="form.errors?.description" />
</div>

View File

@ -37,7 +37,7 @@ const handleClose = () => {
<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
Eliminar Subclasificación
</h3>
</div>
<button
@ -53,7 +53,7 @@ const handleClose = () => {
<!-- 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?
¿Estás seguro de que deseas eliminar esta subclasificación?
</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">
@ -89,7 +89,7 @@ const handleClose = () => {
<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.
Los productos que tenían asignada esta subclasificación quedarán sin subclasificación automáticamente.
</p>
</div>
</div>
@ -109,7 +109,7 @@ const handleClose = () => {
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
Eliminar Subclasificación
</button>
</div>
</div>

View File

@ -30,12 +30,12 @@ const form = useForm({
const updateSubcategory = () => {
form.put(apiURL(`categorias/${props.categoryId}/subcategorias/${props.subcategory.id}`), {
onSuccess: () => {
Notify.success('Subcategoría actualizada exitosamente');
Notify.success('Subclasificación actualizada exitosamente');
emit('updated');
closeModal();
},
onError: () => {
Notify.error('Error al actualizar la subcategoría');
Notify.error('Error al actualizar la subclasificación');
}
});
};
@ -61,7 +61,7 @@ watch(() => props.subcategory, (newSubcategory) => {
<!-- 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
Editar Subclasificación
</h3>
<button
@click="closeModal"
@ -83,7 +83,7 @@ watch(() => props.subcategory, (newSubcategory) => {
<FormInput
v-model="form.name"
type="text"
placeholder="Nombre de la subcategoría"
placeholder="Nombre de la subclasificación"
required
/>
<FormError :message="form.errors?.name" />
@ -98,7 +98,7 @@ watch(() => props.subcategory, (newSubcategory) => {
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"
placeholder="Descripción de la subclasificación"
></textarea>
<FormError :message="form.errors?.description" />
</div>

View File

@ -93,15 +93,15 @@ const confirmDelete = async (id) => {
});
if (response.ok) {
Notify.success('Subcategoría eliminada exitosamente');
Notify.success('Subclasificación eliminada exitosamente');
closeDeleteModal();
searcher.search();
} else {
Notify.error('Error al eliminar la subcategoría');
Notify.error('Error al eliminar la subclasificación');
}
} catch (error) {
console.error('Error:', error);
Notify.error('Error al eliminar la subcategoría');
Notify.error('Error al eliminar la subclasificación');
}
};
@ -120,14 +120,14 @@ onMounted(() => {
<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"
title="Volver a clasificaciones"
>
<GoogleIcon name="arrow_back" class="text-xl" />
</button>
<div>
<p class="text-xs text-gray-500 dark:text-gray-400">Categoría</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Clasificación</p>
<h1 class="text-xl font-bold text-gray-900 dark:text-gray-100">
{{ categoryName || 'Subcategorías' }}
{{ categoryName || 'Subclasificaciones' }}
</h1>
</div>
</div>
@ -136,7 +136,7 @@ onMounted(() => {
@click="openCreateModal"
>
<GoogleIcon name="add" class="text-xl" />
Nueva Subcategoría
Nueva Subclasificación
</button>
</div>
@ -204,14 +204,14 @@ onMounted(() => {
<button
@click="openEditModal(model)"
class="text-indigo-600 hover:text-indigo-900 transition-colors"
title="Editar subcategoría"
title="Editar subclasificación"
>
<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"
title="Eliminar subclasificación"
>
<GoogleIcon name="delete" class="text-xl" />
</button>
@ -226,7 +226,7 @@ onMounted(() => {
name="category"
class="text-6xl mb-2 opacity-50"
/>
<p class="font-semibold">No hay subcategorías registradas</p>
<p class="font-semibold">No hay subclasificaciones registradas</p>
</div>
</td>
</template>

View File

@ -157,10 +157,12 @@ const sendInvoiceByWhatsapp = async () => {
sendingWhatsapp.value = true;
try {
await whatsappService.sendInvoice({
const ticket = request.sale?.invoice_number || `SOL-${request.id}`;
await whatsappService.sendDocument({
phone_number: request.client.phone,
invoice_number: request.sale?.invoice_number || `SOL-${request.id}`,
pdf_url: request.invoice_pdf_url,
document_url: request.invoice_pdf_url,
filename: `${ticket}.pdf`,
ticket,
customer_name: request.client.name
});
@ -419,7 +421,7 @@ const sendInvoiceByWhatsapp = async () => {
{{ item.product_name }}
</p>
<p class="text-xs text-green-700 dark:text-green-300">
SKU: {{ item.inventory?.sku || 'N/A' }} {{ item.inventory?.category?.name || 'Sin categoría' }}
SKU: {{ item.inventory?.sku || 'N/A' }} {{ item.inventory?.category?.name || 'Sin clasificación' }}
</p>
<div v-if="item.serials && item.serials.length > 0" class="mt-1">
<p class="text-xs text-green-600 dark:text-green-400">

View File

@ -65,7 +65,7 @@ const handleClose = () => {
<div class="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
<span class="font-mono font-medium">SKU: {{ product.sku }}</span>
<span class="text-gray-400"></span>
<span>{{ product.category?.name || 'Sin categoría' }}</span>
<span>{{ product.category?.name || 'Sin clasificación' }}</span>
</div>
</div>
<div class="text-right">

View File

@ -58,7 +58,7 @@ const loadCategories = async () => {
categories.value = result.data.categories.data;
}
} catch (error) {
console.error('Error al cargar categorías:', error);
console.error('Error al cargar clasificaciones:', error);
}
};
@ -75,7 +75,7 @@ const loadSubcategories = async () => {
categories.value = result.data.subcategories.data;
}
} catch (error) {
console.error('Error al cargar subcategorías:', error);
console.error('Error al cargar subclasificaciones:', error);
}
};
@ -266,13 +266,13 @@ onMounted(() => {
<div class="pt-4 pb-2">
<div class="flex items-end gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Filtrar por categoría:</label>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Filtrar por clasificación:</label>
<select
v-model="selectedCategory"
@change="handleCategoryChange"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
<option value="">Todas las categorías</option>
<option value="">Todas las clasificaciones</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>

View File

@ -103,7 +103,7 @@ onMounted(() => {
<p class="text-sm text-gray-500 dark:text-gray-400">
SKU: {{ inventory.sku }}
<span v-if="inventory.category">
| Categoría: {{ inventory.category.name }}
| Clasificación: {{ inventory.category.name }}
</span>
</p>
</div>

View File

@ -64,7 +64,7 @@ const loadCategories = async () => {
categories.value = result.data.categories.data;
}
} catch (error) {
console.error('Error al cargar categorías:', error);
console.error('Error al cargar clasificaciones:', error);
}
};
@ -175,13 +175,13 @@ onMounted(async () => {
<div class="pt-4 pb-2">
<div class="flex items-end gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Filtrar por categoría:</label>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Filtrar por clasificación:</label>
<select
v-model="selectedCategory"
@change="handleCategoryChange"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
<option value="">Todas las categorías</option>
<option value="">Todas las clasificaciones</option>
<option v-for="category in categories" :key="category.id" :value="category.id">
{{ category.name }}
</option>
@ -207,7 +207,7 @@ onMounted(async () => {
<template #head>
<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">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">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>

View File

@ -6,19 +6,19 @@ import axios from 'axios';
*/
const whatsappService = {
/**
* Enviar factura por WhatsApp
* @param {Object} data - Datos de la factura
* Enviar documento por WhatsApp
* @param {Object} data - Datos del documento
* @returns {Promise}
*/
async sendInvoice({ phone_number, invoice_number, pdf_url, xml_url = null, customer_name }) {
async sendDocument({ phone_number, document_url, filename, ticket, customer_name }) {
try {
const { data } = await axios.post(
apiURL('whatsapp/send-invoice'),
apiURL('whatsapp/send-document'),
{
phone_number,
invoice_number,
pdf_url,
xml_url,
document_url,
filename,
ticket,
customer_name
},
{