feat: Implement product management module with CRUD functionality

- Added ProductsIndex.vue for displaying and managing products.
- Created index.html for the product management interface.
- Developed productService.ts for API interactions related to products.
- Established productStore.ts using Pinia for state management of products.
- Defined product types in product.d.ts for TypeScript support.
- Integrated toast notifications and confirmation dialogs for user feedback.
- Implemented pagination and search functionality in the product table.
- Added form for creating and editing products with validation.
This commit is contained in:
Edgar Mendez Mendoza 2025-11-10 13:11:38 -06:00
parent 498a15efd4
commit 73fb017ca6
10 changed files with 1543 additions and 333 deletions

View File

@ -34,6 +34,11 @@ const menuItems = ref<MenuItem[]>([
{ label: 'Administrar Clasificaciones', icon: 'pi pi-sitemap', to: '/warehouse/classifications' }
]
},
{
label: 'Productos',
icon: 'pi pi-shopping-cart',
to: '/products'
},
{
label: 'Configuración',
icon: 'pi pi-cog',

View File

@ -0,0 +1,474 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import Dropdown from 'primevue/dropdown';
import FileUpload from 'primevue/fileupload';
import type { Product, CreateProductData } from '../types/product';
// Props
interface Props {
product?: Product | null;
isEditing?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
product: null,
isEditing: false
});
// Emits
const emit = defineEmits<{
save: [data: CreateProductData];
cancel: [];
}>();
// Form data
const formData = ref<CreateProductData>({
code: '',
sku: '',
name: '',
barcode: '',
description: '',
unit_of_measure_id: 0,
suggested_sale_price: 0,
attributes: {},
is_active: true
});
// Temporary string for price input
const priceInput = ref<string>('0');
// Attributes management
interface AttributeValue {
id: string;
value: string;
}
interface AttributeField {
id: string;
name: string;
values: AttributeValue[];
}
const attributes = ref<AttributeField[]>([]);
// Categories (placeholder - should come from a store)
const categories = ref([
{ label: 'Selecciona una categoría', value: null },
{ label: 'Electrónicos', value: 1 },
{ label: 'Ropa', value: 2 },
{ label: 'Hogar', value: 3 },
{ label: 'Oficina', value: 4 }
]);
const selectedCategory = ref(null);
const subcategories = ref([
{ label: 'Selecciona una subcategoría', value: null },
{ label: 'Ratones y Teclados', value: 1 },
{ label: 'Monitores', value: 2 },
{ label: 'Cables y Adaptadores', value: 3 }
]);
const selectedSubcategory = ref(null);
// Unit of measure (placeholder - should come from a store)
const unitsOfMeasure = ref([
{ label: 'Unidad', value: 1 },
{ label: 'Kilogramo', value: 2 },
{ label: 'Metro', value: 3 },
{ label: 'Litro', value: 4 }
]);
// Watch for product changes (when editing)
watch(() => props.product, (newProduct) => {
if (newProduct) {
formData.value = {
code: newProduct.code,
sku: newProduct.sku,
name: newProduct.name,
barcode: newProduct.barcode || '',
description: newProduct.description || '',
unit_of_measure_id: newProduct.unit_of_measure_id,
suggested_sale_price: newProduct.suggested_sale_price,
attributes: newProduct.attributes || {},
is_active: newProduct.is_active
};
priceInput.value = newProduct.suggested_sale_price.toString();
// Convert attributes object to array format
if (newProduct.attributes) {
attributes.value = Object.entries(newProduct.attributes).map(([key, values], index) => ({
id: `attr-${index}`,
name: key,
values: values.map((val, valIndex) => ({
id: `val-${index}-${valIndex}`,
value: val
}))
}));
}
}
}, { immediate: true });
// Attribute methods
const addAttribute = () => {
const newId = `attr-${Date.now()}`;
attributes.value.push({
id: newId,
name: '',
values: [{ id: `val-${newId}-0`, value: '' }]
});
};
const removeAttribute = (attributeId: string) => {
attributes.value = attributes.value.filter(attr => attr.id !== attributeId);
};
const addAttributeValue = (attributeId: string) => {
const attribute = attributes.value.find(attr => attr.id === attributeId);
if (attribute) {
const newValueId = `val-${attributeId}-${attribute.values.length}`;
attribute.values.push({ id: newValueId, value: '' });
}
};
const removeAttributeValue = (attributeId: string, valueId: string) => {
const attribute = attributes.value.find(attr => attr.id === attributeId);
if (attribute && attribute.values.length > 1) {
attribute.values = attribute.values.filter(val => val.id !== valueId);
}
};
// Form validation
const isFormValid = computed(() => {
return formData.value.name.trim() !== '' &&
formData.value.sku.trim() !== '' &&
formData.value.unit_of_measure_id > 0;
});
// Form submission
const handleSubmit = () => {
if (!isFormValid.value) return;
// Convert attributes array back to object format
const attributesObject: Record<string, string[]> = {};
attributes.value.forEach(attr => {
if (attr.name.trim()) {
attributesObject[attr.name] = attr.values
.map(v => v.value.trim())
.filter(v => v !== '');
}
});
formData.value.attributes = attributesObject;
emit('save', formData.value);
};
const handleCancel = () => {
emit('cancel');
};
// File upload
const onUpload = (event: any) => {
console.log('File uploaded:', event);
// TODO: Implement file upload logic
};
</script>
<template>
<div class="product-form">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left column - Main information -->
<div class="lg:col-span-2 flex flex-col gap-8">
<!-- Product Information Card -->
<Card>
<template #title>
<h2 class="text-lg font-bold">Información del Producto</h2>
</template>
<template #content>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<!-- Product Name -->
<div class="sm:col-span-2">
<label for="product-name" class="block text-sm font-medium mb-2">
Nombre del Producto *
</label>
<InputText
id="product-name"
v-model="formData.name"
class="w-full"
placeholder="ej., Mouse Inalámbrico Premium"
/>
</div>
<!-- SKU -->
<div>
<label for="sku" class="block text-sm font-medium mb-2">
SKU (Código de Producto) *
</label>
<InputText
id="sku"
v-model="formData.sku"
class="w-full"
placeholder="ej., WRLS-MSE-BLK-01"
/>
</div>
<!-- Code -->
<div>
<label for="code" class="block text-sm font-medium mb-2">
Código
</label>
<InputText
id="code"
v-model="formData.code"
class="w-full"
placeholder="ej., PRD-001"
/>
</div>
<!-- Barcode -->
<div>
<label for="barcode" class="block text-sm font-medium mb-2">
Código de Barras
</label>
<InputText
id="barcode"
v-model="formData.barcode"
class="w-full"
placeholder="ej., 1234567890123"
/>
</div>
<!-- Unit of Measure -->
<div>
<label for="unit-measure" class="block text-sm font-medium mb-2">
Unidad de Medida *
</label>
<Dropdown
id="unit-measure"
v-model="formData.unit_of_measure_id"
:options="unitsOfMeasure"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una unidad"
/>
</div>
<!-- Suggested Sale Price -->
<div>
<label for="price" class="block text-sm font-medium mb-2">
Precio Sugerido de Venta
</label>
<InputText
id="price"
v-model="priceInput"
type="number"
class="w-full"
placeholder="0.00"
step="0.01"
min="0"
@input="formData.suggested_sale_price = parseFloat(priceInput) || 0"
/>
</div>
<!-- Description -->
<div class="sm:col-span-2">
<label for="description" class="block text-sm font-medium mb-2">
Descripción del Producto
</label>
<Textarea
id="description"
v-model="formData.description"
class="w-full"
rows="4"
placeholder="Ingresa una descripción detallada del producto..."
/>
</div>
</div>
</template>
</Card>
<!-- Product Attributes Card -->
<Card>
<template #title>
<h2 class="text-lg font-bold">Atributos del Producto</h2>
</template>
<template #content>
<div class="space-y-6">
<!-- Attribute item -->
<div
v-for="attribute in attributes"
:key="attribute.id"
class="p-4 border surface-border rounded-lg"
>
<div class="flex items-center justify-between mb-4">
<label class="block text-sm font-medium">
Nombre del Atributo
</label>
<Button
icon="pi pi-trash"
severity="danger"
text
rounded
@click="removeAttribute(attribute.id)"
/>
</div>
<InputText
v-model="attribute.name"
class="w-full mb-4"
placeholder="ej., resolución"
/>
<label class="block text-sm font-medium mb-2">
Valores del Atributo
</label>
<!-- Attribute values -->
<div
v-for="attrValue in attribute.values"
:key="attrValue.id"
class="flex items-center gap-2 mb-2"
>
<InputText
v-model="attrValue.value"
class="flex-1"
placeholder="ej., 1080p"
/>
<Button
icon="pi pi-minus-circle"
severity="danger"
text
rounded
:disabled="attribute.values.length === 1"
@click="removeAttributeValue(attribute.id, attrValue.id)"
/>
</div>
<Button
label="Agregar Valor"
icon="pi pi-plus-circle"
text
class="mt-2"
@click="addAttributeValue(attribute.id)"
/>
</div>
<!-- Add attribute button -->
<Button
label="Agregar Otro Atributo"
icon="pi pi-plus"
severity="secondary"
outlined
class="w-full"
@click="addAttribute"
/>
</div>
</template>
</Card>
</div>
<!-- Right column - Sidebar -->
<div class="lg:col-span-1 flex flex-col gap-8">
<!-- Product Image Card -->
<Card>
<template #title>
<h3 class="text-lg font-bold">Imagen del Producto</h3>
</template>
<template #content>
<FileUpload
mode="basic"
name="product-image"
accept="image/*"
:maxFileSize="1000000"
chooseLabel="Seleccionar Imagen"
class="w-full"
@upload="onUpload"
>
<template #empty>
<div class="flex flex-col items-center justify-center p-6 border-2 border-dashed surface-border rounded-lg text-center">
<i class="pi pi-upload text-5xl mb-3 text-surface-400"></i>
<p class="text-sm">
<span class="font-semibold text-primary">Click para subir</span> o arrastra y suelta
</p>
<p class="text-xs text-surface-500 mt-1">
SVG, PNG, JPG o GIF (max. 800x400px)
</p>
</div>
</template>
</FileUpload>
</template>
</Card>
<!-- Organization Card -->
<Card>
<template #title>
<h3 class="text-lg font-bold">Organización</h3>
</template>
<template #content>
<div class="space-y-6">
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium mb-2">
Categoría
</label>
<Dropdown
id="category"
v-model="selectedCategory"
:options="categories"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una categoría"
/>
</div>
<!-- Subcategory -->
<div>
<label for="subcategory" class="block text-sm font-medium mb-2">
Subcategoría
</label>
<Dropdown
id="subcategory"
v-model="selectedSubcategory"
:options="subcategories"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una subcategoría"
/>
</div>
</div>
</template>
</Card>
</div>
</div>
<!-- Form Actions -->
<div class="mt-8 pt-6 border-t surface-border flex justify-end items-center gap-4">
<Button
label="Cancelar"
severity="secondary"
outlined
@click="handleCancel"
/>
<Button
:label="isEditing ? 'Actualizar Producto' : 'Guardar Producto'"
:disabled="!isFormValid"
@click="handleSubmit"
/>
</div>
</div>
</template>
<style scoped>
.product-form {
max-width: 1280px;
margin: 0 auto;
}
</style>

View File

@ -0,0 +1,358 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import Breadcrumb from 'primevue/breadcrumb';
import Button from 'primevue/button';
import Card from 'primevue/card';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
import Toast from 'primevue/toast';
import ConfirmDialog from 'primevue/confirmdialog';
import ProgressSpinner from 'primevue/progressspinner';
import Paginator from 'primevue/paginator';
import { useProductStore } from '../stores/productStore';
import ProductForm from './ProductForm.vue';
import type { Product, CreateProductData } from '../types/product';
const toast = useToast();
const confirm = useConfirm();
const productStore = useProductStore();
// Breadcrumb
const breadcrumbItems = ref([
{ label: 'Productos' }
]);
const home = ref({
icon: 'pi pi-home',
route: '/'
});
// State
const showDialog = ref(false);
const isEditing = ref(false);
const searchTerm = ref('');
const selectedProduct = ref<Product | null>(null);
// Computed
const products = computed(() => productStore.products);
const loading = computed(() => productStore.loading);
const pagination = computed(() => productStore.pagination);
const dialogTitle = computed(() =>
isEditing.value ? 'Editar Producto' : 'Nuevo Producto'
);
// Methods
const loadProducts = async () => {
try {
await productStore.fetchProducts();
} catch (error) {
console.error('Error loading products:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los productos.',
life: 3000
});
}
};
const getStatusSeverity = (product: Product): "success" | "danger" => {
return product.is_active ? 'success' : 'danger';
};
const getStatusLabel = (product: Product): string => {
return product.is_active ? 'Activo' : 'Inactivo';
};
const openCreateDialog = () => {
selectedProduct.value = null;
isEditing.value = false;
showDialog.value = true;
};
const editProduct = (product: Product) => {
selectedProduct.value = product;
isEditing.value = true;
showDialog.value = true;
};
const handleSaveProduct = async (productData: CreateProductData) => {
try {
if (isEditing.value && selectedProduct.value) {
await productStore.updateProduct(selectedProduct.value.id, productData);
toast.add({
severity: 'success',
summary: 'Producto Actualizado',
detail: 'El producto ha sido actualizado exitosamente.',
life: 3000
});
} else {
await productStore.createProduct(productData);
toast.add({
severity: 'success',
summary: 'Producto Creado',
detail: 'El producto ha sido creado exitosamente.',
life: 3000
});
}
showDialog.value = false;
selectedProduct.value = null;
} catch (error) {
console.error('Error saving product:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo guardar el producto.',
life: 3000
});
}
};
const handleCancelForm = () => {
showDialog.value = false;
selectedProduct.value = null;
};
const deleteProductConfirm = (product: Product) => {
confirm.require({
message: `¿Estás seguro de eliminar el producto "${product.name}"?`,
header: 'Confirmar Eliminación',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Eliminar',
rejectClass: 'p-button-secondary p-button-outlined',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await productStore.deleteProduct(product.id);
toast.add({
severity: 'success',
summary: 'Producto Eliminado',
detail: `El producto "${product.name}" ha sido eliminado exitosamente.`,
life: 3000
});
} catch (error) {
console.error('Error deleting product:', error);
toast.add({
severity: 'error',
summary: 'Error al Eliminar',
detail: 'No se pudo eliminar el producto.',
life: 3000
});
}
}
});
};
const onSearch = () => {
if (searchTerm.value.trim()) {
console.log('Searching:', searchTerm.value);
// TODO: Implementar búsqueda cuando el API lo soporte
}
};
const onPageChange = (event: any) => {
productStore.changePage(event.page + 1);
};
onMounted(() => {
loadProducts();
});
</script>
<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Confirm Dialog -->
<ConfirmDialog></ConfirmDialog>
<!-- Breadcrumb -->
<Breadcrumb :home="home" :model="breadcrumbItems" />
<!-- Page Heading -->
<div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-2xl font-bold leading-tight tracking-tight text-surface-900 dark:text-white">
Catálogo de Productos
</h1>
<Button
label="Nuevo Producto"
icon="pi pi-plus"
@click="openCreateDialog"
/>
</div>
<!-- Filters & Search -->
<Card>
<template #content>
<div class="flex gap-4">
<!-- Search -->
<div class="flex-1">
<span class="p-input-icon-left w-full">
<i class="pi pi-search" />
<InputText
v-model="searchTerm"
placeholder="Buscar por nombre, código o SKU..."
class="w-full"
@keyup.enter="onSearch"
/>
</span>
</div>
</div>
</template>
</Card>
<!-- Products Table -->
<Card>
<template #content>
<div v-if="loading" class="flex items-center justify-center py-8">
<ProgressSpinner style="width: 50px; height: 50px" />
</div>
<DataTable
v-else
:value="products"
stripedRows
responsiveLayout="scroll"
class="p-datatable-sm"
>
<!-- Product Name -->
<Column field="name" header="Producto" sortable style="min-width: 200px">
<template #body="slotProps">
<div class="flex flex-col">
<span class="font-medium text-surface-900 dark:text-white">
{{ slotProps.data.name }}
</span>
<span class="text-xs text-surface-500 dark:text-surface-400">
{{ slotProps.data.description }}
</span>
</div>
</template>
</Column>
<!-- Code -->
<Column field="code" header="Código" sortable style="min-width: 100px">
<template #body="slotProps">
<span class="font-mono text-sm text-primary-600 dark:text-primary-400">
{{ slotProps.data.code }}
</span>
</template>
</Column>
<!-- SKU -->
<Column field="sku" header="SKU" sortable style="min-width: 120px">
<template #body="slotProps">
<span class="font-mono text-sm text-surface-600 dark:text-surface-400">
{{ slotProps.data.sku }}
</span>
</template>
</Column>
<!-- Barcode -->
<Column field="barcode" header="Código de Barras" style="min-width: 140px">
<template #body="slotProps">
<span class="font-mono text-xs text-surface-500 dark:text-surface-400">
{{ slotProps.data.barcode || '-' }}
</span>
</template>
</Column>
<!-- Price -->
<Column field="suggested_sale_price" header="Precio Sugerido" sortable style="min-width: 130px">
<template #body="slotProps">
<span class="font-semibold text-green-600 dark:text-green-400">
${{ slotProps.data.suggested_sale_price.toLocaleString('es-MX', { minimumFractionDigits: 2 }) }}
</span>
</template>
</Column>
<!-- Status -->
<Column field="is_active" header="Estado" sortable style="min-width: 100px">
<template #body="slotProps">
<Tag
:value="getStatusLabel(slotProps.data)"
:severity="getStatusSeverity(slotProps.data)"
/>
</template>
</Column>
<!-- Actions -->
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 120px">
<template #body="slotProps">
<div class="flex items-center justify-end gap-2">
<Button
icon="pi pi-pencil"
text
rounded
size="small"
@click="editProduct(slotProps.data)"
v-tooltip.top="'Editar'"
/>
<Button
icon="pi pi-trash"
text
rounded
size="small"
severity="danger"
@click="deleteProductConfirm(slotProps.data)"
v-tooltip.top="'Eliminar'"
/>
</div>
</template>
</Column>
<template #empty>
<div class="flex flex-col items-center justify-center py-8 text-center">
<i class="pi pi-inbox text-4xl text-surface-300 dark:text-surface-600 mb-4"></i>
<h3 class="text-lg font-semibold text-surface-900 dark:text-surface-0">
No hay productos
</h3>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">
Agrega tu primer producto para comenzar
</p>
</div>
</template>
</DataTable>
<!-- Pagination -->
<div v-if="!loading && products.length > 0" class="mt-4">
<Paginator
:first="(pagination.currentPage - 1) * pagination.perPage"
:rows="pagination.perPage"
:totalRecords="pagination.total"
:rowsPerPageOptions="[5, 10, 20, 50]"
@page="onPageChange"
>
<template #start>
<span class="text-sm text-surface-500 dark:text-surface-400">
Mostrando {{ pagination.from }} - {{ pagination.to }} de {{ pagination.total }}
</span>
</template>
</Paginator>
</div>
</template>
</Card>
<!-- Create/Edit Product Dialog -->
<Dialog
v-model:visible="showDialog"
modal
:header="dialogTitle"
:style="{ width: '90vw', maxWidth: '1400px' }"
:contentStyle="{ padding: '1.5rem' }"
>
<ProductForm
:product="selectedProduct"
:is-editing="isEditing"
@save="handleSaveProduct"
@cancel="handleCancelForm"
/>
</Dialog>
</div>
</template>

View File

@ -0,0 +1,364 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>WMS - Add New Product</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&amp;display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary": "#137fec",
"background-light": "#f6f7f8",
"background-dark": "#101922",
},
fontFamily: {
"display": ["Inter", "sans-serif"]
},
borderRadius: {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
},
},
}
</script>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-gray-800 dark:text-gray-200">
<div class="flex h-screen w-full">
<aside
class="flex w-64 flex-col border-r border-gray-200 dark:border-gray-800 bg-white dark:bg-background-dark">
<div class="flex h-full flex-col justify-between p-4">
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3 p-2">
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10"
data-alt="Admin user avatar"
style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuCztlHhjKvu2qkn2Xi2zFHagNsNToKwcTg3vQr0KtTqBCo13dK1yyz9HzB2uLCiciLyDfnrf7pREvdblPqCcUiN0HqlSbkFwY1dpQLMbJ4hmpVgHVWaLaUCMXju06qyGQSdg2ChGVcbTQIrk-RNI2-hDOFnfrI1PD89RNSsByXGRsdkYWSyEYFOFk7bT4l7aIaasB6cdVxDfNwJdvVx15wb7-qOHZHFTPMbrkkzmjGec-f7iVqTi5U1ykNDclBSezBM97TfXajTwRJE");'>
</div>
<div class="flex flex-col">
<h1 class="text-gray-900 dark:text-white text-base font-medium leading-normal">Admin User
</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm font-normal leading-normal">Warehouse
Manager</p>
</div>
</div>
<nav class="flex flex-col gap-2">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined text-gray-800 dark:text-gray-200">dashboard</span>
<p class="text-sm font-medium leading-normal">Dashboard</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 rounded-lg bg-primary/10 text-primary dark:bg-primary/20"
href="#">
<span class="material-symbols-outlined">warehouse</span>
<p class="text-sm font-medium leading-normal">Inventory</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined text-gray-800 dark:text-gray-200">receipt_long</span>
<p class="text-sm font-medium leading-normal">Orders</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span
class="material-symbols-outlined text-gray-800 dark:text-gray-200">local_shipping</span>
<p class="text-sm font-medium leading-normal">Suppliers</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined text-gray-800 dark:text-gray-200">pie_chart</span>
<p class="text-sm font-medium leading-normal">Reports</p>
</a>
</nav>
</div>
<div class="flex flex-col gap-2">
<nav class="flex flex-col gap-1">
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined text-gray-800 dark:text-gray-200">settings</span>
<p class="text-sm font-medium leading-normal">Settings</p>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined text-gray-800 dark:text-gray-200">help</span>
<p class="text-sm font-medium leading-normal">Help</p>
</a>
</nav>
<button
class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal tracking-[0.015em] hover:bg-gray-300 dark:hover:bg-gray-700">
<span class="truncate">Logout</span>
</button>
</div>
</div>
</aside>
<main class="flex-1 flex flex-col h-screen overflow-y-auto">
<header
class="flex sticky top-0 items-center justify-between whitespace-nowrap border-b border-solid border-gray-200 dark:border-gray-800 px-10 py-3 bg-white/80 dark:bg-background-dark/80 backdrop-blur-sm z-10">
<div class="flex items-center gap-4 text-gray-900 dark:text-white">
<div class="size-6 text-primary">
<svg fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.435 7.182a.75.75 0 0 0-.87-.11L12 10.435 3.435 7.072a.75.75 0 0 0-.87.11.75.75 0 0 0-.11.87l2.122 7.878a.75.75 0 0 0 .869.59l6-1.635a.75.75 0 0 0 .108 0l6 1.635a.75.75 0 0 0 .87-.59l2.12-7.878a.75.75 0 0 0-.11-.87zM12 12.18l-5.693-1.55L12 4.288l5.693 6.342L12 12.18z">
</path>
</svg>
</div>
<h2 class="text-lg font-bold leading-tight tracking-[-0.015em]">WMS Dashboard</h2>
</div>
<div class="flex flex-1 justify-end items-center gap-4">
<label class="relative flex-grow max-w-sm">
<span
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-500">search</span>
<input
class="form-input w-full rounded-lg border-none bg-gray-100 dark:bg-gray-800 h-10 pl-10 pr-4 text-sm text-gray-900 dark:text-white placeholder:text-gray-500"
placeholder="Search items, orders..." value="" />
</label>
<div class="flex gap-2">
<button
class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">notifications</span>
</button>
<button
class="flex items-center justify-center rounded-lg h-10 w-10 bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl">help_outline</span>
</button>
</div>
<div class="bg-center bg-no-repeat aspect-square bg-cover rounded-full size-10"
data-alt="Admin user avatar"
style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuBNpxncwdjcD3fLZbRbit_NZyuFBZhcpV-Bvdlu6NiZL3kb65hsFRsDkTNHtC0zJxG3HGV1TInl_DCfafj3axNGSwW4-UNj1sZWhiHCYE2aK9hm-FYjrNGiEh0UqKya1EAYMTM5Z4k8qKOWPEPdIaZz9X98tPC5FIn5lbRRusCTuQmgRL-QxK9SdIMA3TflImwA1vyh3zq44j8EkkNTQWf94-e82GDHs5MwHIkK0S-Gg4d950IyRTpoABSa9qXA5yPzoaT9jCGjCodL");'>
</div>
</div>
</header>
<div class="flex-1 p-6 lg:p-10">
<div class="mx-auto max-w-7xl">
<div class="flex flex-wrap gap-2 mb-6">
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal"
href="#">Dashboard</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<a class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal"
href="#">Inventory</a>
<span class="text-gray-500 dark:text-gray-400 text-sm font-medium leading-normal">/</span>
<span class="text-gray-800 dark:text-gray-200 text-sm font-medium leading-normal">Add New
Product</span>
</div>
<div class="flex flex-wrap justify-between items-center gap-4 mb-8">
<div class="flex min-w-72 flex-col gap-2">
<h1 class="text-gray-900 dark:text-white text-3xl font-bold tracking-tight">Add New Product
</h1>
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">Fill in the
details below to add a new product to the catalog.</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="lg:col-span-2 flex flex-col gap-8">
<div
class="bg-white dark:bg-gray-900/50 p-6 rounded-xl border border-gray-200 dark:border-gray-800">
<h2 class="text-lg font-bold text-gray-900 dark:text-white mb-6">Product Information
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="product-name">Product Name *</label>
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="product-name" placeholder="e.g., Premium Wireless Mouse" type="text" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="sku">SKU (Stock Keeping Unit) *</label>
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="sku" placeholder="e.g., WRLS-MSE-BLK-01" type="text" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="supplier-id">Supplier ID</label>
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="supplier-id" placeholder="e.g., SUP-ACME-45" type="text" />
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="description">Product Description</label>
<textarea
class="form-textarea w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="description"
placeholder="Enter a detailed description of the product..."
rows="4"></textarea>
</div>
</div>
</div>
<div
class="bg-white dark:bg-gray-900/50 p-6 rounded-xl border border-gray-200 dark:border-gray-800">
<h2 class="text-lg font-bold text-gray-900 dark:text-white mb-6">Product Attributes</h2>
<div class="space-y-6" id="attributes-container">
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div class="flex items-center justify-between mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300"
for="attribute-name-1">Attribute Name</label>
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</div>
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary mb-4"
id="attribute-name-1" placeholder="e.g., resolucion" type="text"
value="resolucion" />
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="attribute-values-1">Attribute Values</label>
<div class="flex items-center gap-2 mb-2">
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="attribute-values-1" placeholder="e.g., 1080" type="text"
value="1080" />
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span
class="material-symbols-outlined text-xl">remove_circle_outline</span>
</button>
</div>
<div class="flex items-center gap-2 mb-2">
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
placeholder="e.g., 4K" type="text" value="4K" />
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span
class="material-symbols-outlined text-xl">remove_circle_outline</span>
</button>
</div>
<div class="flex items-center gap-2 mb-4">
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
placeholder="e.g., 2K" type="text" value="2K" />
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span
class="material-symbols-outlined text-xl">remove_circle_outline</span>
</button>
</div>
<button
class="flex items-center gap-2 text-sm font-medium text-primary hover:text-primary/90">
<span class="material-symbols-outlined">add_circle_outline</span>
<span>Add Value</span>
</button>
</div>
<div class="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div class="flex items-center justify-between mb-4">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300"
for="attribute-name-2">Attribute Name</label>
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span class="material-symbols-outlined text-xl">delete</span>
</button>
</div>
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary mb-4"
id="attribute-name-2" placeholder="e.g., medida" type="text"
value="medida" />
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="attribute-values-2">Attribute Values</label>
<div class="flex items-center gap-2 mb-2">
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="attribute-values-2" placeholder="e.g., 24" type="text" value="24" />
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span
class="material-symbols-outlined text-xl">remove_circle_outline</span>
</button>
</div>
<div class="flex items-center gap-2 mb-4">
<input
class="form-input w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
placeholder="e.g., 36" type="text" value="36" />
<button class="text-gray-400 hover:text-red-500 dark:hover:text-red-400">
<span
class="material-symbols-outlined text-xl">remove_circle_outline</span>
</button>
</div>
<button
class="flex items-center gap-2 text-sm font-medium text-primary hover:text-primary/90">
<span class="material-symbols-outlined">add_circle_outline</span>
<span>Add Value</span>
</button>
</div>
</div>
<button
class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-primary/10 dark:bg-primary/20 text-primary text-sm font-bold leading-normal tracking-[0.015em] hover:bg-primary/20 dark:hover:bg-primary/30 w-full mt-4">
<span class="material-symbols-outlined mr-2">add</span>
<span class="truncate">Add Another Attribute</span>
</button>
</div>
</div>
<div class="lg:col-span-1 flex flex-col gap-8">
<div
class="bg-white dark:bg-gray-900/50 p-6 rounded-xl border border-gray-200 dark:border-gray-800">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">Product Image</h3>
<div
class="flex flex-col items-center justify-center p-6 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-lg text-center cursor-pointer hover:border-primary dark:hover:border-primary transition-colors">
<div class="mb-2 text-gray-400 dark:text-gray-500">
<span class="material-symbols-outlined text-5xl">upload_file</span>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400"><span
class="font-semibold text-primary">Click to upload</span> or drag and drop
</p>
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">SVG, PNG, JPG or GIF (max.
800x400px)</p>
</div>
</div>
<div
class="bg-white dark:bg-gray-900/50 p-6 rounded-xl border border-gray-200 dark:border-gray-800">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6">Organization</h3>
<div class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="category">Category</label>
<select
class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="category">
<option>Select a category</option>
<option>Electronics</option>
<option>Apparel</option>
<option>Home Goods</option>
<option>Office Supplies</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
for="subcategory">Subcategory</label>
<select
class="form-select w-full rounded-lg border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-primary focus:border-primary"
id="subcategory">
<option>Select a subcategory</option>
<option>Mice &amp; Keyboards</option>
<option>Monitors</option>
<option>Cables &amp; Adapters</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div
class="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 flex justify-end items-center gap-4">
<button
class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-sm font-bold leading-normal hover:bg-gray-300 dark:hover:bg-gray-700">
<span class="truncate">Cancel</span>
</button>
<button
class="flex min-w-[84px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-primary text-white text-sm font-bold leading-normal tracking-[0.015em] hover:bg-primary/90">
<span class="truncate">Save Product</span>
</button>
</div>
</div>
</div>
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,57 @@
import api from '../../../services/api';
import type {
ProductResponse,
SingleProductResponse,
CreateProductData,
UpdateProductData,
} from '../types/product';
/**
* Obtener todos los productos con paginación y filtros
*/
export const getProducts = async (): Promise<ProductResponse> => {
const response = await api.get<ProductResponse>('/api/products');
return response.data;
};
/**
* Obtener un producto por ID
*/
export const getProductById = async (id: number): Promise<SingleProductResponse> => {
const response = await api.get<SingleProductResponse>(`/api/products/${id}`);
return response.data;
};
/**
* Crear un nuevo producto
*/
export const createProduct = async (data: CreateProductData): Promise<SingleProductResponse> => {
const response = await api.post<SingleProductResponse>('/api/products', data);
return response.data;
};
/**
* Actualizar un producto
*/
export const updateProduct = async (
id: number,
data: UpdateProductData
): Promise<SingleProductResponse> => {
const response = await api.put<SingleProductResponse>(`/api/products/${id}`, data);
return response.data;
};
/**
* Eliminar un producto
*/
export const deleteProduct = async (id: number): Promise<void> => {
await api.delete(`/api/products/${id}`);
};
export const productService = {
getProducts,
getProductById,
createProduct,
updateProduct,
deleteProduct
};

View File

@ -0,0 +1,170 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { productService } from '../services/productService';
import type { Product, CreateProductData, UpdateProductData } from '../types/product';
export const useProductStore = defineStore('product', () => {
// State
const products = ref<Product[]>([]);
const currentPage = ref(1);
const perPage = ref(10);
const totalProducts = ref(0);
const lastPage = ref(1);
const loading = ref(false);
const error = ref<string | null>(null);
// Getters
const activeProducts = computed(() =>
products.value.filter(product => product.is_active === true)
);
const inactiveProducts = computed(() =>
products.value.filter(product => product.is_active === false)
);
const productCount = computed(() => products.value.length);
const getProductById = computed(() => {
return (id: number) => products.value.find(product => product.id === id);
});
const getProductBySku = computed(() => {
return (sku: string) =>
products.value.find(product => product.sku.toLowerCase() === sku.toLowerCase());
});
const pagination = computed(() => ({
currentPage: currentPage.value,
perPage: perPage.value,
total: totalProducts.value,
lastPage: lastPage.value,
from: (currentPage.value - 1) * perPage.value + 1,
to: Math.min(currentPage.value * perPage.value, totalProducts.value)
}));
// Actions
const fetchProducts = async () => {
try {
loading.value = true;
error.value = null;
const response = await productService.getProducts();
products.value = response.data.products.data;
currentPage.value = response.data.products.current_page;
perPage.value = response.data.products.per_page;
totalProducts.value = response.data.products.total;
lastPage.value = response.data.products.last_page;
console.log('Products loaded into store:', products.value.length);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Error loading products';
console.error('Error in product store:', err);
throw err;
} finally {
loading.value = false;
}
};
const refreshProducts = () => {
return fetchProducts();
};
const createProduct = async (data: CreateProductData) => {
try {
loading.value = true;
error.value = null;
await productService.createProduct(data);
// Refrescar la lista después de crear
await refreshProducts();
console.log('Product created successfully');
} catch (err) {
error.value = err instanceof Error ? err.message : 'Error creating product';
console.error('Error creating product:', err);
throw err;
} finally {
loading.value = false;
}
};
const updateProduct = async (id: number, data: UpdateProductData) => {
try {
loading.value = true;
error.value = null;
await productService.updateProduct(id, data);
// Refrescar la lista después de actualizar
await refreshProducts();
console.log('Product updated successfully');
} catch (err) {
error.value = err instanceof Error ? err.message : 'Error updating product';
console.error('Error updating product:', err);
throw err;
} finally {
loading.value = false;
}
};
const deleteProduct = async (id: number) => {
try {
loading.value = true;
error.value = null;
await productService.deleteProduct(id);
// Refrescar la lista después de eliminar
await refreshProducts();
console.log('Product deleted successfully');
} catch (err) {
error.value = err instanceof Error ? err.message : 'Error deleting product';
console.error('Error deleting product:', err);
throw err;
} finally {
loading.value = false;
}
};
const changePage = async (page: number) => {
currentPage.value = page;
await fetchProducts();
};
const changePerPage = async (itemsPerPage: number) => {
perPage.value = itemsPerPage;
currentPage.value = 1; // Reset to first page when changing items per page
await fetchProducts();
};
return {
// State
products,
currentPage,
perPage,
totalProducts,
lastPage,
loading,
error,
// Getters
activeProducts,
inactiveProducts,
productCount,
getProductById,
getProductBySku,
pagination,
// Actions
fetchProducts,
refreshProducts,
createProduct,
updateProduct,
deleteProduct,
changePage,
changePerPage
};
});

88
src/modules/products/types/product.d.ts vendored Normal file
View File

@ -0,0 +1,88 @@
/**
* Product Type Definitions
*/
export interface Product {
id: number;
code: string;
sku: string;
name: string;
barcode?: string;
description?: string;
unit_of_measure_id: number;
suggested_sale_price: number;
attributes?: Record<string, string[]>;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at?: string | null;
classifications?: any[];
warehouses?: any[];
}
export interface CreateProductData {
code: string;
sku: string;
name: string;
barcode?: string;
description?: string;
unit_of_measure_id: number;
suggested_sale_price: number;
attributes?: Record<string, string[]>;
is_active?: boolean;
}
export interface UpdateProductData {
code?: string;
sku?: string;
name?: string;
barcode?: string;
description?: string;
unit_of_measure_id?: number;
suggested_sale_price?: number;
attributes?: Record<string, string[]>;
is_active?: boolean;
}
export interface ProductPagination {
current_page: number;
last_page: number;
per_page: number;
total: number;
from: number;
to: number;
}
export interface ProductData {
current_page: number;
data: Product[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
links: Array<{
url: string | null;
label: string;
active: boolean;
}>;
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
}
export interface ProductResponse {
status: string;
data: {
products: ProductData;
};
}
export interface SingleProductResponse {
status: string;
data: {
product: Product;
};
}

View File

@ -1,5 +0,0 @@
<template>
<p>
Categorías de Almacenes
</p>
</template>

View File

@ -1,323 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="en">
<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>Warehouse Management - Category Management</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&amp;display=swap"
rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary": "#137fec",
"background-light": "#f6f7f8",
"background-dark": "#101922",
},
fontFamily: {
"display": ["Inter", "sans-serif"]
},
borderRadius: {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark font-display text-gray-800 dark:text-gray-200">
<div class="flex h-screen w-full">
<!-- SideNavBar -->
<aside
class="flex w-64 flex-col border-r border-gray-200 dark:border-gray-800 bg-white dark:bg-background-dark p-4">
<div class="flex items-center gap-3 p-3">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-white">
<span class="material-symbols-outlined text-xl"> all_inbox </span>
</div>
<h1 class="text-lg font-bold text-gray-900 dark:text-white">Warehouse OS</h1>
</div>
<div class="mt-8 flex flex-1 flex-col justify-between">
<nav class="flex flex-col gap-2">
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined"> dashboard </span>
<p class="text-sm font-medium">Dashboard</p>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined"> inventory_2 </span>
<p class="text-sm font-medium">Inventory</p>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined"> receipt_long </span>
<p class="text-sm font-medium">Orders</p>
</a>
<a class="flex items-center gap-3 rounded-lg bg-primary/10 px-3 py-2 text-primary dark:text-primary-300"
href="#">
<span class="material-symbols-outlined"> category </span>
<p class="text-sm font-medium">Categories</p>
</a>
<a class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
href="#">
<span class="material-symbols-outlined"> group </span>
<p class="text-sm font-medium">Admin</p>
</a>
</nav>
<div class="flex flex-col gap-1">
<div
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">
<span class="material-symbols-outlined"> settings </span>
<p class="text-sm font-medium">Settings</p>
</div>
<div
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800">
<span class="material-symbols-outlined"> logout </span>
<p class="text-sm font-medium">Logout</p>
</div>
</div>
</div>
</aside>
<!-- Main Content Area -->
<div class="flex flex-1 flex-col">
<!-- TopNavBar -->
<header
class="flex h-16 items-center justify-between whitespace-nowrap border-b border-solid border-gray-200 dark:border-gray-800 bg-white dark:bg-background-dark px-6">
<h2 class="text-lg font-semibold leading-tight tracking-[-0.015em] text-gray-900 dark:text-white">
Warehouse Management</h2>
<div class="flex flex-1 items-center justify-end gap-4">
<label class="relative flex h-10 w-full max-w-sm items-center">
<span class="material-symbols-outlined pointer-events-none absolute left-3 text-gray-500">
search </span>
<input
class="form-input h-full w-full rounded-lg border-none bg-background-light dark:bg-gray-800 pl-10 pr-4 text-sm text-gray-900 dark:text-gray-200 placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary/50"
placeholder="Search..." type="search" />
</label>
<button
class="flex h-10 w-10 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-background-light dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl"> notifications </span>
</button>
<div class="flex items-center gap-3">
<div class="aspect-square size-10 rounded-full bg-cover bg-center" data-alt="Admin User Avatar"
style='background-image: url("https://lh3.googleusercontent.com/aida-public/AB6AXuB5vU3rC0xkPN11NdHb7lV2JjPHtA5iz-JZRXyvJCf2Gr2EzGGez0z29FlFX8i9lK1ozAXhO9K8l5Y1ZX7qtKGHhe9tlko1jM-eFmSlew_yn5B_8pMxKbaqcEfct87c9EnraBpBb2ivyFOyxJnNdakHgQEkdRY7zJ_9DkihDlL3WDSFPpvtllvE15xbWdKDO0RM1JAqljPjn5HbDYMaU5431xGGTBkxeYG9b8j0GPq2cS-bAnCD1-FzohDFuMbytuKp8C9ntd0cS2mp");'>
</div>
<div class="flex flex-col text-sm">
<p class="font-semibold text-gray-800 dark:text-white">Alex Turner</p>
<p class="text-gray-500 dark:text-gray-400">Administrator</p>
</div>
</div>
</div>
</header>
<!-- Page Content -->
<main class="flex flex-1 flex-col overflow-y-auto p-6">
<!-- PageHeading -->
<div class="flex flex-wrap items-center justify-between gap-3">
<p class="text-2xl font-bold leading-tight tracking-tight text-gray-900 dark:text-white">Category
Management</p>
</div>
<!-- Two-Panel Layout -->
<div class="mt-6 grid flex-1 grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Panel (Category Tree) -->
<div
class="flex flex-col gap-4 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/50 p-4 lg:col-span-1">
<button
class="flex h-10 w-full cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-4 text-sm font-bold leading-normal tracking-[0.015em] text-white hover:bg-primary/90">
<span class="material-symbols-outlined text-xl"> add </span>
<span>Add New Category</span>
</button>
<div class="flex flex-col gap-1">
<!-- List Item: Selected -->
<div
class="flex cursor-pointer items-center justify-between gap-4 rounded-lg bg-primary/10 px-4 py-3">
<div class="flex items-center gap-4">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/20 text-primary">
<span class="material-symbols-outlined"> category </span>
</div>
<p class="flex-1 truncate text-base font-semibold text-primary">Electronics</p>
</div>
<div class="shrink-0 text-primary">
<span class="material-symbols-outlined"> chevron_right </span>
</div>
</div>
<!-- List Item: Unselected -->
<div
class="group flex cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-800">
<div class="flex items-center gap-4">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-background-light dark:bg-gray-800 text-gray-600 dark:text-gray-400">
<span class="material-symbols-outlined"> book </span>
</div>
<p class="flex-1 truncate text-base font-normal text-gray-800 dark:text-gray-300">
Books &amp; Media</p>
</div>
<div
class="shrink-0 text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-200">
<span class="material-symbols-outlined"> chevron_right </span>
</div>
</div>
<div
class="group flex cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-800">
<div class="flex items-center gap-4">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-background-light dark:bg-gray-800 text-gray-600 dark:text-gray-400">
<span class="material-symbols-outlined"> chair </span>
</div>
<p class="flex-1 truncate text-base font-normal text-gray-800 dark:text-gray-300">
Home &amp; Furniture</p>
</div>
<div
class="shrink-0 text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-200">
<span class="material-symbols-outlined"> chevron_right </span>
</div>
</div>
<div
class="group flex cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-800">
<div class="flex items-center gap-4">
<div
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-background-light dark:bg-gray-800 text-gray-600 dark:text-gray-400">
<span class="material-symbols-outlined"> checkroom </span>
</div>
<p class="flex-1 truncate text-base font-normal text-gray-800 dark:text-gray-300">
Apparel</p>
</div>
<div
class="shrink-0 text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-200">
<span class="material-symbols-outlined"> chevron_right </span>
</div>
</div>
</div>
</div>
<!-- Right Panel (Details View) -->
<div
class="flex flex-col gap-6 rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/50 p-6 lg:col-span-2">
<!-- Header -->
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p
class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Category Details</p>
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Electronics</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">Items related to consumer
electronics, computers, and accessories.</p>
</div>
<div class="flex items-center gap-2">
<button
class="flex h-9 w-9 cursor-pointer items-center justify-center overflow-hidden rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
<span class="material-symbols-outlined text-xl"> edit </span>
</button>
<button
class="flex h-9 w-9 cursor-pointer items-center justify-center overflow-hidden rounded-lg border border-red-500/50 bg-red-500/10 text-red-600 hover:bg-red-500/20 dark:border-red-500/50 dark:bg-red-500/10 dark:text-red-400 dark:hover:bg-red-500/20">
<span class="material-symbols-outlined text-xl"> delete </span>
</button>
</div>
</div>
<!-- Subcategory Section -->
<div class="flex flex-col">
<div class="mb-4 flex items-center justify-between">
<h4 class="font-semibold text-gray-800 dark:text-gray-200">Subcategories (4)</h4>
<button
class="flex h-9 cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-lg bg-primary px-3 text-sm font-bold text-white hover:bg-primary/90">
<span class="material-symbols-outlined text-lg"> add </span>
<span>Add Subcategory</span>
</button>
</div>
<!-- Subcategory Table/List -->
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800">
<div class="flex flex-col">
<!-- Table Header -->
<div
class="grid grid-cols-3 bg-gray-50 dark:bg-gray-800/50 px-4 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
<div class="col-span-1">Name</div>
<div class="col-span-1">Date Created</div>
<div class="col-span-1 text-right">Actions</div>
</div>
<!-- Table Body -->
<div class="divide-y divide-gray-200 dark:divide-gray-800">
<!-- Row 1 -->
<div
class="grid grid-cols-3 items-center px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
<div class="col-span-1 font-medium">Smartphones</div>
<div class="col-span-1 text-gray-500 dark:text-gray-400">2023-03-15</div>
<div class="col-span-1 flex items-center justify-end gap-2">
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"><span
class="material-symbols-outlined text-xl"> edit </span></button>
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-red-500 hover:bg-red-500/10"><span
class="material-symbols-outlined text-xl"> delete
</span></button>
</div>
</div>
<!-- Row 2 -->
<div
class="grid grid-cols-3 items-center px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
<div class="col-span-1 font-medium">Laptops &amp; Computers</div>
<div class="col-span-1 text-gray-500 dark:text-gray-400">2023-03-15</div>
<div class="col-span-1 flex items-center justify-end gap-2">
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"><span
class="material-symbols-outlined text-xl"> edit </span></button>
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-red-500 hover:bg-red-500/10"><span
class="material-symbols-outlined text-xl"> delete
</span></button>
</div>
</div>
<!-- Row 3 -->
<div
class="grid grid-cols-3 items-center px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
<div class="col-span-1 font-medium">Audio &amp; Headphones</div>
<div class="col-span-1 text-gray-500 dark:text-gray-400">2023-04-01</div>
<div class="col-span-1 flex items-center justify-end gap-2">
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"><span
class="material-symbols-outlined text-xl"> edit </span></button>
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-red-500 hover:bg-red-500/10"><span
class="material-symbols-outlined text-xl"> delete
</span></button>
</div>
</div>
<!-- Row 4 -->
<div
class="grid grid-cols-3 items-center px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
<div class="col-span-1 font-medium">Cameras &amp; Drones</div>
<div class="col-span-1 text-gray-500 dark:text-gray-400">2023-05-20</div>
<div class="col-span-1 flex items-center justify-end gap-2">
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200"><span
class="material-symbols-outlined text-xl"> edit </span></button>
<button
class="flex h-8 w-8 items-center justify-center rounded-lg text-red-500 hover:bg-red-500/10"><span
class="material-symbols-outlined text-xl"> delete
</span></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</body>
</html>

View File

@ -10,6 +10,8 @@ import WarehouseForm from '../modules/warehouse/components/WarehouseForm.vue';
import WarehouseClassification from '../modules/warehouse/components/WarehouseClassification.vue';
import UnitOfMeasure from '../modules/catalog/components/UnitOfMeasure.vue';
import ComercialClassification from '../modules/catalog/components/ComercialClassification.vue';
import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
import ProductForm from '../modules/products/components/ProductForm.vue';
const routes: RouteRecordRaw[] = [
{
@ -119,6 +121,26 @@ const routes: RouteRecordRaw[] = [
}
},
]
},
{
path: 'products',
name: 'Products',
component: ProductsIndex,
meta: {
title: 'Productos',
requiresAuth: true
},
children: [
{
path: 'create',
name: 'ProductCreate',
component: ProductForm,
meta: {
title: 'Crear Producto',
requiresAuth: true
}
},
]
}
]
},
@ -136,16 +158,16 @@ const router = createRouter({
// Navigation Guard
router.beforeEach((to, _from, next) => {
const { isAuthenticated } = useAuth();
// Actualizar título de la página
document.title = to.meta.title
? `${to.meta.title} - GOLS Control`
document.title = to.meta.title
? `${to.meta.title} - GOLS Control`
: 'GOLS Control';
// Verificar si la ruta requiere autenticación
if (to.meta.requiresAuth && !isAuthenticated.value) {
// Redirigir al login si no está autenticado
next({
next({
name: 'Login',
query: { redirect: to.fullPath } // Guardar ruta para redireccionar después del login
});