feature-comercial-module-ts #13
@ -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',
|
||||
|
||||
474
src/modules/products/components/ProductForm.vue
Normal file
474
src/modules/products/components/ProductForm.vue
Normal 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>
|
||||
358
src/modules/products/components/ProductsIndex.vue
Normal file
358
src/modules/products/components/ProductsIndex.vue
Normal 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>
|
||||
364
src/modules/products/components/index.html
Normal file
364
src/modules/products/components/index.html
Normal 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&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 & Keyboards</option>
|
||||
<option>Monitors</option>
|
||||
<option>Cables & 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>
|
||||
57
src/modules/products/services/productService.ts
Normal file
57
src/modules/products/services/productService.ts
Normal 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
|
||||
};
|
||||
170
src/modules/products/stores/productStore.ts
Normal file
170
src/modules/products/stores/productStore.ts
Normal 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
88
src/modules/products/types/product.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<p>
|
||||
Categorías de Almacenes
|
||||
</p>
|
||||
</template>
|
||||
@ -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&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 & 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 & 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 & 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 & 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 & 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>
|
||||
@ -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
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user