feature-comercial-module-ts #10

Merged
edgar.mendez merged 41 commits from feature-comercial-module-ts into qa 2026-02-13 19:52:10 +00:00
19 changed files with 1988 additions and 432 deletions
Showing only changes of commit 3bea03f9db - Show all commits

8
components.d.ts vendored
View File

@ -13,25 +13,33 @@ declare module 'vue' {
export interface GlobalComponents {
AppConfig: typeof import('./src/components/layout/AppConfig.vue')['default']
AppTopbar: typeof import('./src/components/Holos/AppTopbar.vue')['default']
Avatar: typeof import('primevue/avatar')['default']
Badge: typeof import('primevue/badge')['default']
Breadcrumb: typeof import('primevue/breadcrumb')['default']
Button: typeof import('primevue/button')['default']
Card: typeof import('primevue/card')['default']
Checkbox: typeof import('primevue/checkbox')['default']
Chip: typeof import('primevue/chip')['default']
Column: typeof import('primevue/column')['default']
DataTable: typeof import('primevue/datatable')['default']
Dialog: typeof import('primevue/dialog')['default']
Dropdown: typeof import('primevue/dropdown')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
IconField: typeof import('primevue/iconfield')['default']
InputGroup: typeof import('primevue/inputgroup')['default']
InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
InputIcon: typeof import('primevue/inputicon')['default']
InputNumber: typeof import('primevue/inputnumber')['default']
InputText: typeof import('primevue/inputtext')['default']
KpiCard: typeof import('./src/components/shared/KpiCard.vue')['default']
Menu: typeof import('primevue/menu')['default']
Message: typeof import('primevue/message')['default']
ProgressSpinner: typeof import('primevue/progressspinner')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default']
Textarea: typeof import('primevue/textarea')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
}
export interface GlobalDirectives {

136
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",
@ -1299,6 +1300,30 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.3.tgz",
@ -1466,6 +1491,15 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/birpc": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.7.0.tgz",
"integrity": "sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -1512,6 +1546,21 @@
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"license": "MIT",
"dependencies": {
"is-what": "^5.2.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -1860,6 +1909,24 @@
"node": ">= 0.4"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -2174,6 +2241,12 @@
"node": ">= 0.6"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mlly": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
@ -2247,6 +2320,12 @@
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -2265,6 +2344,36 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
@ -2361,6 +2470,12 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rollup": {
"version": "4.52.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
@ -2411,6 +2526,27 @@
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
"license": "MIT",
"dependencies": {
"copy-anything": "^4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tailwindcss": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",

View File

@ -14,6 +14,7 @@
"@tailwindcss/vite": "^4.1.16",
"@vueuse/core": "^14.0.0",
"axios": "^1.13.2",
"pinia": "^3.0.4",
"primeicons": "^7.0.0",
"primevue": "^4.4.1",
"tailwindcss-primeui": "^0.6.1",

View File

@ -1,5 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
interface MenuItem {
label: string;
@ -14,6 +18,14 @@ const menuItems = ref<MenuItem[]>([
icon: 'pi pi-chart-line',
to: '/'
},
{
label: 'Almacén',
icon: 'pi pi-box',
items: [
{ label: 'Almacenes', icon: 'pi pi-warehouse', to: '/warehouse' },
{ label: 'Administrar Clasificaciones', icon: 'pi pi-sitemap', to: '/warehouse/classifications' }
]
},
{
label: 'Ventas',
icon: 'pi pi-shopping-cart',
@ -66,7 +78,6 @@ const menuItems = ref<MenuItem[]>([
const sidebarVisible = ref(true);
const openItems = ref<string[]>([]);
const currentRoute = ref(window.location.pathname);
const toggleSidebar = () => {
sidebarVisible.value = !sidebarVisible.value;
@ -87,13 +98,39 @@ const isItemOpen = (label: string) => {
const isRouteActive = (to: string | undefined) => {
if (!to) return false;
return currentRoute.value === to;
// Coincidencia exacta
if (route.path === to) {
return true;
}
// Para la ruta raíz, solo coincidencia exacta
if (to === '/') {
return false;
}
// Si la ruta actual es hija (ej: /warehouse/create)
// y el item es el padre (ej: /warehouse)
// SOLO marcar activo si la ruta hija NO está explícitamente en el menú
if (route.path.startsWith(to + '/')) {
// Verificar si la ruta actual está definida como un item del menú
const isExplicitRoute = menuItems.value.some(item => {
if (item.items) {
return item.items.some(subItem => subItem.to === route.path);
}
return item.to === route.path;
});
// Si NO está explícitamente en el menú, entonces es una ruta hija (create, edit, etc)
return !isExplicitRoute;
}
return false;
};
// Simular cambio de ruta (en producción usarías Vue Router)
// Navegar usando Vue Router
const navigateTo = (to: string) => {
currentRoute.value = to;
// window.history.pushState({}, '', to);
router.push(to);
};
// Funciones de animación para los submenús

View File

@ -3,8 +3,11 @@ import "./assets/styles/main.css";
import Aura from "@primeuix/themes/aura";
import { definePreset } from "@primeuix/themes";
import PrimeVue from "primevue/config";
import ConfirmationService from 'primevue/confirmationservice';
import ToastService from 'primevue/toastservice';
import StyleClass from "primevue/styleclass";
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import { useAuth } from "./modules/auth/composables/useAuth";
@ -29,12 +32,16 @@ const MyPreset = definePreset(Aura, {
});
const app = createApp(App);
const pinia = createPinia();
// Inicializar autenticación desde localStorage
const { initAuth } = useAuth();
initAuth();
app.use(pinia);
app.use(router);
app.use(ConfirmationService);
app.use(ToastService);
app.use(PrimeVue, {
theme: {
preset: MyPreset,

View File

@ -1,130 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useWarehouse } from '../composables/useWarehouse';
import type { Product } from '../types/warehouse';
const { products, loading, loadProducts } = useWarehouse();
const selectedProducts = ref<Product[]>([]);
const searchQuery = ref('');
onMounted(() => {
loadProducts();
});
const getStockStatus = (product: Product) => {
if (product.quantity === 0) return { label: 'Sin Stock', severity: 'danger' };
if (product.quantity < product.minStock) return { label: 'Stock Bajo', severity: 'warning' };
if (product.quantity > product.maxStock) return { label: 'Sobrestock', severity: 'info' };
return { label: 'Normal', severity: 'success' };
};
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('es-MX', {
style: 'currency',
currency: 'MXN'
}).format(value);
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(new Date(date));
};
</script>
<template>
<div class="p-6">
<!-- Search Bar -->
<div class="mb-4">
<div class="relative">
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-surface-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Buscar productos..."
class="w-full pl-10 pr-4 py-2 rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-0 dark:bg-surface-800 text-surface-900 dark:text-surface-0 focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
</div>
<!-- DataTable using PrimeVue -->
<DataTable
v-model:selection="selectedProducts"
:value="products"
:loading="loading"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[5, 10, 20, 50]"
:globalFilterFields="['name', 'sku', 'category']"
:globalFilter="searchQuery"
stripedRows
showGridlines
class="text-sm"
>
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
<Column field="sku" header="SKU" sortable>
<template #body="{ data }">
<span class="font-mono text-xs">{{ data.sku }}</span>
</template>
</Column>
<Column field="name" header="Producto" sortable>
<template #body="{ data }">
<div>
<div class="font-medium text-surface-900 dark:text-surface-0">{{ data.name }}</div>
<div class="text-xs text-surface-500">{{ data.category }}</div>
</div>
</template>
</Column>
<Column field="location" header="Ubicación" sortable></Column>
<Column field="quantity" header="Cantidad" sortable>
<template #body="{ data }">
<div class="flex items-center gap-2">
<span class="font-semibold">{{ data.quantity }}</span>
<Tag
:value="getStockStatus(data).label"
:severity="getStockStatus(data).severity"
/>
</div>
</template>
</Column>
<Column field="minStock" header="Stock Mín." sortable>
<template #body="{ data }">
<span class="text-surface-600 dark:text-surface-400">{{ data.minStock }}</span>
</template>
</Column>
<Column field="unitPrice" header="Precio Unit." sortable>
<template #body="{ data }">
<span class="font-medium">{{ formatCurrency(data.unitPrice) }}</span>
</template>
</Column>
<Column field="lastUpdated" header="Última Actualización" sortable>
<template #body="{ data }">
<span class="text-xs text-surface-500">{{ formatDate(data.lastUpdated) }}</span>
</template>
</Column>
<Column header="Acciones">
<template #body>
<div class="flex gap-2">
<Button icon="pi pi-pencil" size="small" text rounded />
<Button icon="pi pi-trash" size="small" text rounded severity="danger" />
</div>
</template>
</Column>
</DataTable>
</div>
</template>
<style scoped>
/* Estilos adicionales si es necesario */
</style>

View File

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

View File

@ -0,0 +1,585 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import Button from 'primevue/button';
import Card from 'primevue/card';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { warehouseClassificationService } from '../services/warehouseClasificationService';
import type { Classification } from '../types/warehouse.clasification';
const confirm = useConfirm();
const toast = useToast();
interface Category {
id: number;
code: string;
name: string;
description: string;
parent_id: number | null;
created_at: string;
subcategories: Category[];
}
const categories = ref<Category[]>([]);
const selectedCategory = ref<Category | null>(null);
const showCreateModal = ref(false);
const isSubmitting = ref(false);
const loading = ref(false);
const isEditMode = ref(false);
const editingId = ref<number | null>(null);
// Form data
const formData = ref({
code: '',
name: '',
description: '',
parent_id: null as number | null
});
// Transform API data to component structure
const transformClassifications = (classifications: Classification[]): Category[] => {
return classifications.map(cls => ({
id: cls.id,
code: cls.code,
name: cls.name,
description: cls.description,
parent_id: cls.parent_id,
created_at: cls.created_at,
subcategories: cls.children ? cls.children.map(child => ({
id: child.id,
code: child.code,
name: child.name,
description: child.description,
parent_id: child.parent_id,
created_at: child.created_at,
subcategories: []
})) : []
}));
};
const loadClassifications = async () => {
try {
loading.value = true;
const response = await warehouseClassificationService.getClassifications();
const classificationsData = response.data.data.warehouse_classifications.data;
categories.value = transformClassifications(classificationsData);
// Select first category by default
if (categories.value.length > 0 && categories.value[0]) {
selectedCategory.value = categories.value[0];
}
} catch (error) {
console.error('Error loading classifications:', error);
} finally {
loading.value = false;
}
};
const selectCategory = (category: Category) => {
selectedCategory.value = category;
};
const addNewCategory = () => {
// Reset form
isEditMode.value = false;
editingId.value = null;
formData.value = {
code: '',
name: '',
description: '',
parent_id: null
};
showCreateModal.value = true;
};
const closeModal = () => {
showCreateModal.value = false;
isEditMode.value = false;
editingId.value = null;
};
const createClassification = async () => {
try {
isSubmitting.value = true;
if (isEditMode.value && editingId.value) {
// Update existing classification
await warehouseClassificationService.updateClassification(editingId.value, formData.value);
toast.add({
severity: 'success',
summary: 'Clasificación Actualizada',
detail: `La clasificación "${formData.value.name}" ha sido actualizada exitosamente.`,
life: 3000
});
} else {
// Create new classification
await warehouseClassificationService.createClassification(formData.value);
toast.add({
severity: 'success',
summary: 'Clasificación Creada',
detail: `La clasificación "${formData.value.name}" ha sido creada exitosamente.`,
life: 3000
});
}
// Reload classifications
await loadClassifications();
showCreateModal.value = false;
isEditMode.value = false;
editingId.value = null;
// Reset form
formData.value = {
code: '',
name: '',
description: '',
parent_id: null
};
} catch (error) {
console.error('Error saving classification:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: isEditMode.value
? 'No se pudo actualizar la clasificación. Por favor, intenta nuevamente.'
: 'No se pudo crear la clasificación. Por favor, intenta nuevamente.',
life: 3000
});
} finally {
isSubmitting.value = false;
}
};
const addSubcategory = () => {
if (selectedCategory.value) {
isEditMode.value = false;
editingId.value = null;
formData.value = {
code: '',
name: '',
description: '',
parent_id: selectedCategory.value.id
};
showCreateModal.value = true;
}
};
const editCategory = () => {
if (!selectedCategory.value) return;
isEditMode.value = true;
editingId.value = selectedCategory.value.id;
formData.value = {
code: selectedCategory.value.code,
name: selectedCategory.value.name,
description: selectedCategory.value.description,
parent_id: selectedCategory.value.parent_id
};
showCreateModal.value = true;
};
const deleteCategory = () => {
if (!selectedCategory.value) return;
confirm.require({
message: `¿Estás seguro de eliminar la clasificación "${selectedCategory.value.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 {
const categoryName = selectedCategory.value!.name;
await warehouseClassificationService.deleteClassification(selectedCategory.value!.id);
toast.add({
severity: 'success',
summary: 'Clasificación Eliminada',
detail: `La clasificación "${categoryName}" ha sido eliminada exitosamente.`,
life: 3000
});
// Clear selection
selectedCategory.value = null;
// Reload classifications
await loadClassifications();
} catch (error) {
console.error('Error deleting classification:', error);
toast.add({
severity: 'error',
summary: 'Error al Eliminar',
detail: 'No se pudo eliminar la clasificación. Verifica si tiene subclasificaciones asociadas.',
life: 3000
});
}
}
});
};
const editSubcategory = (subcategory: Category) => {
isEditMode.value = true;
editingId.value = subcategory.id;
formData.value = {
code: subcategory.code,
name: subcategory.name,
description: subcategory.description,
parent_id: subcategory.parent_id
};
showCreateModal.value = true;
};
const deleteSubcategory = (subcategory: Category) => {
confirm.require({
message: `¿Estás seguro de eliminar la subclasificación "${subcategory.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 warehouseClassificationService.deleteClassification(subcategory.id);
toast.add({
severity: 'success',
summary: 'Subclasificación Eliminada',
detail: `La subclasificación "${subcategory.name}" ha sido eliminada exitosamente.`,
life: 3000
});
// Reload classifications
await loadClassifications();
} catch (error) {
console.error('Error deleting subcategory:', error);
toast.add({
severity: 'error',
summary: 'Error al Eliminar',
detail: 'No se pudo eliminar la subclasificación.',
life: 3000
});
}
}
});
};
onMounted(() => {
loadClassifications();
});
</script>
<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Confirm Dialog -->
<ConfirmDialog></ConfirmDialog>
<!-- 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">
Administración de Clasificaciones
</h1>
</div>
<!-- Two-Panel Layout -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Panel (Category Tree) -->
<Card class="lg:col-span-1">
<template #content>
<div class="flex flex-col gap-4">
<!-- Add Category Button -->
<Button
label="Agregar Nueva Clasificación"
icon="pi pi-plus"
class="w-full"
@click="addNewCategory"
/>
<!-- Categories List -->
<div v-if="loading" class="flex items-center justify-center py-8">
<ProgressSpinner style="width: 50px; height: 50px" />
</div>
<div v-else-if="categories.length === 0" 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>
<p class="text-sm text-surface-500 dark:text-surface-400">
No hay clasificaciones creadas
</p>
</div>
<div v-else class="flex flex-col gap-1">
<div
v-for="category in categories"
:key="category.id"
:class="[
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-4 py-3 transition-colors',
selectedCategory?.id === category.id
? 'bg-primary/10 text-primary'
: 'hover:bg-surface-100 dark:hover:bg-surface-800'
]"
@click="selectCategory(category)"
>
<div class="flex items-center gap-4">
<div
:class="[
'flex size-10 shrink-0 items-center justify-center rounded-lg',
selectedCategory?.id === category.id
? 'bg-primary/20 text-primary'
: 'bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-400'
]"
>
<i class="pi pi-tag"></i>
</div>
<p
:class="[
'flex-1 truncate text-base',
selectedCategory?.id === category.id
? 'font-semibold text-primary'
: 'font-normal text-surface-800 dark:text-surface-300'
]"
>
{{ category.name }}
</p>
</div>
<div :class="selectedCategory?.id === category.id ? 'text-primary' : 'text-surface-400'">
<i class="pi pi-chevron-right"></i>
</div>
</div>
</div>
</div>
</template>
</Card>
<!-- Right Panel (Details View) -->
<Card v-if="!selectedCategory" class="lg:col-span-2">
<template #content>
<div class="flex flex-col items-center justify-center py-16">
<i class="pi pi-tag text-6xl text-surface-300 dark:text-surface-600 mb-4"></i>
<h3 class="text-xl font-semibold text-surface-700 dark:text-surface-300 mb-2">
Selecciona una clasificación
</h3>
<p class="text-sm text-surface-500 dark:text-surface-400">
Elige una clasificación de la lista para ver sus detalles y subclasificaciones
</p>
</div>
</template>
</Card>
<Card v-else class="lg:col-span-2">
<template #content>
<div class="flex flex-col gap-6">
<!-- Header -->
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-xs font-medium uppercase tracking-wider text-surface-500 dark:text-surface-400">
Detalles de la Clasificación
</p>
<h3 class="text-xl font-bold text-surface-900 dark:text-white">
{{ selectedCategory.name }}
</h3>
<p class="mt-1 text-sm text-surface-600 dark:text-surface-400">
{{ selectedCategory.description }}
</p>
</div>
<div class="flex items-center gap-2">
<Button
icon="pi pi-pencil"
outlined
rounded
@click="editCategory"
/>
<Button
icon="pi pi-trash"
severity="danger"
outlined
rounded
@click="deleteCategory"
/>
</div>
</div>
<!-- Subcategory Section -->
<div class="flex flex-col">
<div class="mb-4 flex items-center justify-between">
<h4 class="font-semibold text-surface-800 dark:text-surface-200">
Subclasificaciones ({{ selectedCategory.subcategories.length }})
</h4>
<Button
label="Agregar Subclasificación"
icon="pi pi-plus"
size="small"
@click="addSubcategory"
/>
</div>
<!-- Subcategory Table -->
<DataTable
:value="selectedCategory.subcategories"
stripedRows
responsiveLayout="scroll"
>
<Column field="name" header="Nombre" sortable>
<template #body="slotProps">
<span class="font-medium text-surface-900 dark:text-white">
{{ slotProps.data.name }}
</span>
</template>
</Column>
<Column field="created_at" header="Fecha de Creación" sortable>
<template #body="slotProps">
<span class="text-surface-500 dark:text-surface-400">
{{ new Date(slotProps.data.created_at).toLocaleDateString('es-MX', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) }}
</span>
</template>
</Column>
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right">
<template #body="slotProps">
<div class="flex items-center justify-end gap-2">
<Button
icon="pi pi-pencil"
text
rounded
size="small"
@click="editSubcategory(slotProps.data)"
/>
<Button
icon="pi pi-trash"
text
rounded
size="small"
severity="danger"
@click="deleteSubcategory(slotProps.data)"
/>
</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 subclasificaciones
</h3>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-2">
Agrega subclasificaciones para organizar mejor esta categoría
</p>
</div>
</template>
</DataTable>
</div>
</div>
</template>
</Card>
</div>
<!-- Create/Edit Classification Modal -->
<Dialog
v-model:visible="showCreateModal"
modal
:header="isEditMode ? 'Editar Clasificación' : 'Crear Nueva Clasificación'"
:style="{ width: '500px' }"
>
<div class="flex flex-col gap-4 py-4">
<!-- Code -->
<div>
<label for="code" class="block text-sm font-medium mb-2">
Código <span class="text-red-500">*</span>
</label>
<InputText
id="code"
v-model="formData.code"
class="w-full"
placeholder="Ej: GS-06"
/>
</div>
<!-- Name -->
<div>
<label for="name" class="block text-sm font-medium mb-2">
Nombre <span class="text-red-500">*</span>
</label>
<InputText
id="name"
v-model="formData.name"
class="w-full"
placeholder="Ej: PRINCIPAL"
/>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium mb-2">
Descripción
</label>
<Textarea
id="description"
v-model="formData.description"
rows="3"
class="w-full"
placeholder="Descripción opcional de la clasificación"
/>
</div>
<!-- Parent Category -->
<div>
<label for="parent" class="block text-sm font-medium mb-2">
Clasificación Padre (Opcional)
</label>
<Dropdown
id="parent"
v-model="formData.parent_id"
:options="categories"
optionLabel="name"
optionValue="id"
placeholder="Seleccionar clasificación padre..."
class="w-full"
showClear
/>
<small class="text-surface-500 dark:text-surface-400">
Deja vacío para crear una clasificación raíz
</small>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button
label="Cancelar"
severity="secondary"
outlined
@click="closeModal"
:disabled="isSubmitting"
/>
<Button
:label="isEditMode ? 'Actualizar Clasificación' : 'Crear Clasificación'"
@click="createClassification"
:loading="isSubmitting"
:disabled="!formData.code || !formData.name"
/>
</div>
</template>
</Dialog>
</div>
</template>

View File

@ -1,33 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight text-foreground">
Módulo Personalizado
</h1>
<p className="text-muted-foreground">
Este es un módulo en blanco listo para personalizar
</p>
</div>
<Card>
<template #title>Módulo en Blanco</template>
<template #subtitle>Personaliza este módulo según tus necesidades</template>
<template #content>
<div
className="flex min-h-[400px] items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/20">
<div className="text-center">
<FileStack className="mx-auto h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">Espacio para tu contenido</h3>
<p className="mt-2 text-sm text-muted-foreground">
Agrega componentes, tablas, formularios o cualquier funcionalidad aquí
</p>
</div>
</div>
</template>
</Card>
</div>
</template>

View File

@ -0,0 +1,424 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const breadcrumbItems = ref([
{ label: 'Almacenes', route: '/warehouse' },
{ label: 'Crear Almacén' }
]);
const home = ref({
icon: 'pi pi-home',
route: '/'
});
// Form data
const formData = ref({
name: '',
code: '',
address: '',
status: 'active',
capacity: null,
phone: '',
email: ''
});
const statusOptions = [
{ label: 'Activo', value: 'active' },
{ label: 'Inactivo', value: 'inactive' },
{ label: 'En Mantenimiento', value: 'maintenance' }
];
// Categories
const assignedCategories = ref([
{ id: 1, name: 'Almacenamiento a Granel', color: 'info' },
{ id: 2, name: 'Mercancía General', color: 'success' },
{ id: 3, name: 'Control de Temperatura > Refrigerado', color: 'warn' }
]);
const selectedParentCategory = ref(null);
const selectedSubCategory = ref(null);
const newCategoryName = ref('');
const newCategoryParent = ref(null);
const parentCategories = [
{ label: 'Control de Temperatura', value: 'temp' },
{ label: 'Materiales Peligrosos', value: 'hazmat' },
{ label: 'Alto Valor', value: 'high-value' }
];
const subCategories = [
{ label: 'Refrigerado', value: 'refrigerated' },
{ label: 'Congelado', value: 'frozen' }
];
// Staff
const assignedStaff = ref([
{
id: 1,
name: 'Jane Cooper',
role: 'Gerente de Almacén',
avatar: 'https://i.pravatar.cc/150?img=1'
},
{
id: 2,
name: 'Cody Fisher',
role: 'Operador de Montacargas',
avatar: 'https://i.pravatar.cc/150?img=2'
},
{
id: 3,
name: 'Esther Howard',
role: 'Auxiliar de Recepción',
avatar: 'https://i.pravatar.cc/150?img=3'
}
]);
const saveDisabled = ref(true);
const removeCategory = (id: number) => {
assignedCategories.value = assignedCategories.value.filter(cat => cat.id !== id);
};
const addCategory = () => {
if (newCategoryName.value) {
const newId = Math.max(...assignedCategories.value.map(c => c.id), 0) + 1;
assignedCategories.value.push({
id: newId,
name: newCategoryName.value,
color: 'info'
});
newCategoryName.value = '';
newCategoryParent.value = null;
}
};
const addStaff = () => {
console.log('Add staff');
};
const editStaff = (id: number) => {
console.log('Edit staff:', id);
};
const removeStaff = (id: number) => {
assignedStaff.value = assignedStaff.value.filter(staff => staff.id !== id);
};
const cancel = () => {
router.push('/warehouse');
};
const save = () => {
console.log('Save warehouse:', formData.value);
};
</script>
<template>
<div class="space-y-6">
<!-- Header con Breadcrumb y Acciones -->
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-col gap-2">
<!-- Breadcrumb -->
<Breadcrumb :home="home" :model="breadcrumbItems">
<template #item="{ item }">
<a
v-if="item.route"
:href="item.route"
@click.prevent="router.push(item.route)"
class="text-primary hover:underline"
>
{{ item.label }}
</a>
<span v-else class="text-surface-600 dark:text-surface-400">
{{ item.label }}
</span>
</template>
</Breadcrumb>
<!-- Title -->
<h1 class="text-3xl font-black leading-tight tracking-tight text-surface-900 dark:text-white">
Crear Nuevo Almacén
</h1>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
<Button
label="Cancelar"
outlined
@click="cancel"
/>
<Button
label="Guardar Cambios"
:disabled="saveDisabled"
@click="save"
/>
</div>
</div>
<!-- Form Content -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Main Content - 2 columns -->
<div class="lg:col-span-2 space-y-6">
<!-- Warehouse Details Card -->
<Card>
<template #title>Detalles del Almacén</template>
<template #content>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-6">
<!-- Warehouse Name -->
<div class="sm:col-span-4">
<label for="name" class="block text-sm font-medium mb-2">
Nombre del Almacén
</label>
<InputText
id="name"
v-model="formData.name"
class="w-full"
placeholder="Ej: Centro de Distribución Norte"
/>
</div>
<!-- Warehouse Code -->
<div class="sm:col-span-2">
<label for="code" class="block text-sm font-medium mb-2">
Código de Almacén
</label>
<InputText
id="code"
v-model="formData.code"
class="w-full"
placeholder="WH-001"
disabled
:class="'bg-surface-100 dark:bg-surface-700'"
/>
</div>
<!-- Address -->
<div class="col-span-full">
<label for="address" class="block text-sm font-medium mb-2">
Ubicación / Dirección
</label>
<Textarea
id="address"
v-model="formData.address"
rows="3"
class="w-full"
placeholder="Dirección completa del almacén"
/>
</div>
<!-- Status -->
<div class="sm:col-span-3">
<label for="status" class="block text-sm font-medium mb-2">
Estado
</label>
<Dropdown
id="status"
v-model="formData.status"
:options="statusOptions"
optionLabel="label"
optionValue="value"
class="w-full"
/>
</div>
<!-- Capacity -->
<div class="sm:col-span-3">
<label for="capacity" class="block text-sm font-medium mb-2">
Capacidad (Metros Cuadrados)
</label>
<InputNumber
id="capacity"
v-model="formData.capacity"
class="w-full"
:min="0"
/>
</div>
<!-- Contact Phone -->
<div class="sm:col-span-3">
<label for="phone" class="block text-sm font-medium mb-2">
Teléfono de Contacto
</label>
<InputText
id="phone"
v-model="formData.phone"
class="w-full"
placeholder="555-0101"
/>
</div>
<!-- Contact Email -->
<div class="sm:col-span-3">
<label for="email" class="block text-sm font-medium mb-2">
Correo de Contacto
</label>
<InputText
id="email"
v-model="formData.email"
type="email"
class="w-full"
placeholder="almacen@empresa.com"
/>
</div>
</div>
</template>
</Card>
<!-- Warehouse Categories Card -->
<Card>
<template #title>Categorías del Almacén</template>
<template #content>
<div class="space-y-6">
<!-- Assigned Categories -->
<div>
<label class="block text-sm font-medium mb-2">
Categorías Asignadas
</label>
<div class="flex flex-wrap gap-2">
<Chip
v-for="category in assignedCategories"
:key="category.id"
:label="category.name"
removable
@remove="removeCategory(category.id)"
/>
<span v-if="assignedCategories.length === 0" class="text-sm text-surface-500">
No hay categorías asignadas
</span>
</div>
</div>
<!-- Select Categories -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label for="parent-category" class="block text-sm font-medium mb-2">
Categoría
</label>
<Dropdown
id="parent-category"
v-model="selectedParentCategory"
:options="parentCategories"
optionLabel="label"
placeholder="Selecciona una categoría..."
class="w-full"
/>
</div>
<div>
<label for="sub-category" class="block text-sm font-medium mb-2">
Subcategoría (Opcional)
</label>
<Dropdown
id="sub-category"
v-model="selectedSubCategory"
:options="subCategories"
optionLabel="label"
placeholder="Selecciona una subcategoría..."
class="w-full"
/>
</div>
</div>
<!-- Create New Category -->
<div>
<label for="new-category" class="block text-sm font-medium mb-2">
O Crear Nueva Categoría
</label>
<div class="flex gap-2">
<div class="flex-1 space-y-2">
<InputText
id="new-category"
v-model="newCategoryName"
class="w-full"
placeholder="Ej: Farmacéutico"
/>
<Dropdown
v-model="newCategoryParent"
:options="parentCategories"
optionLabel="label"
placeholder="Selecciona categoría padre (opcional)..."
class="w-full"
/>
</div>
<Button
icon="pi pi-plus"
severity="secondary"
outlined
@click="addCategory"
class="self-start"
/>
</div>
</div>
</div>
</template>
</Card>
</div>
<!-- Sidebar - 1 column -->
<div class="lg:col-span-1">
<!-- Assigned Staff Card -->
<Card>
<template #title>
<div class="flex items-center justify-between">
<span>Personal Asignado</span>
<Button
icon="pi pi-plus"
label="Agregar"
size="small"
severity="secondary"
outlined
@click="addStaff"
/>
</div>
</template>
<template #content>
<div class="space-y-4">
<div
v-for="staff in assignedStaff"
:key="staff.id"
class="flex items-center gap-4"
>
<Avatar
:image="staff.avatar"
:label="staff.name.charAt(0)"
shape="circle"
size="large"
/>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-surface-900 dark:text-white truncate">
{{ staff.name }}
</p>
<p class="text-sm text-surface-500 dark:text-surface-400 truncate">
{{ staff.role }}
</p>
</div>
<div class="flex gap-1">
<Button
icon="pi pi-pencil"
text
rounded
size="small"
severity="secondary"
@click="editStaff(staff.id)"
/>
<Button
icon="pi pi-trash"
text
rounded
size="small"
severity="danger"
@click="removeStaff(staff.id)"
/>
</div>
</div>
</div>
</template>
</Card>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,184 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useWarehouseStore } from '../../../stores/warehouseStore';
const router = useRouter();
const warehouseStore = useWarehouseStore();
const searchQuery = ref('');
const selectedStatus = ref('all');
const selectedLocation = ref('all');
// Usar el estado del store
const warehouses = computed(() => warehouseStore.warehouses);
const loading = computed(() => warehouseStore.loading);
const statusOptions = [
{ label: 'All', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Full', value: 'full' },
{ label: 'Inactive', value: 'inactive' },
{ label: 'Needs Attention', value: 'needs_attention' },
];
const locationOptions = [
{ label: 'All', value: 'all' },
{ label: 'Springfield, IL', value: 'springfield' },
{ label: 'Riverside, CA', value: 'riverside' },
{ label: 'Atlanta, GA', value: 'atlanta' },
{ label: 'Columbus, OH', value: 'columbus' },
{ label: 'Seattle, WA', value: 'seattle' },
];
const getStatusConfig = (isActive: boolean) => {
return isActive
? { label: 'Activo', severity: 'success' }
: { label: 'Inactivo', severity: 'secondary' };
};
const clearFilters = () => {
searchQuery.value = '';
selectedStatus.value = 'all';
selectedLocation.value = 'all';
};
const createWarehouse = () => {
router.push({ name: 'WarehouseCreate' });
};
const loadWarehouses = async () => {
await warehouseStore.fetchWarehouses();
};
onMounted(() => {
loadWarehouses();
});
</script>
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex flex-wrap justify-between gap-4 items-center">
<div class="flex min-w-72 flex-col gap-1">
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
Almacenes
</h1>
<p class="text-surface-500 dark:text-surface-400 text-base font-normal leading-normal">
Administra, rastrea y organiza todos tus almacenes en un solo lugar.
</p>
</div>
<Button
label="Crear Nuevo Almacén "
icon="pi pi-plus"
@click="createWarehouse"
class="min-w-[200px]"
/>
</div>
<!-- Table Card -->
<Card class="shadow-sm">
<template #content>
<!-- Search and Filters -->
<div class="flex flex-col md:flex-row justify-between gap-4 mb-4">
<div class="flex-1">
<IconField iconPosition="left">
<InputIcon class="pi pi-search" />
<InputText
v-model="searchQuery"
placeholder="Search by name or location..."
class="w-full"
/>
</IconField>
</div>
<div class="flex gap-2 flex-wrap">
<Dropdown
v-model="selectedStatus"
:options="statusOptions"
optionLabel="label"
optionValue="value"
placeholder="Status: All"
class="w-40"
/>
<Dropdown
v-model="selectedLocation"
:options="locationOptions"
optionLabel="label"
optionValue="value"
placeholder="Location: All"
class="w-40"
/>
<Button
label="Clear"
icon="pi pi-times"
outlined
@click="clearFilters"
/>
</div>
</div>
<!-- Table -->
<DataTable
:value="warehouses"
:loading="loading"
:paginator="true"
:rows="5"
:rowsPerPageOptions="[5, 10, 20]"
stripedRows
responsiveLayout="scroll"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} results"
>
<Column field="code" header="Código" sortable>
<template #body="slotProps">
<span class="font-mono text-surface-700 dark:text-surface-300">
{{ slotProps.data.code }}
</span>
</template>
</Column>
<Column field="name" header="Nombre" sortable>
<template #body="slotProps">
<div>
<span class="font-medium text-surface-900 dark:text-white block">
{{ slotProps.data.name }}
</span>
<span class="text-xs text-surface-500 dark:text-surface-400">
{{ slotProps.data.description }}
</span>
</div>
</template>
</Column>
<Column field="address" header="Ubicación" sortable>
<template #body="slotProps">
<span class="text-surface-500 dark:text-surface-400 text-sm">
{{ slotProps.data.address }}
</span>
</template>
</Column>
<Column field="is_active" header="Estado" sortable>
<template #body="slotProps">
<Tag
:value="getStatusConfig(slotProps.data.is_active).label"
:severity="getStatusConfig(slotProps.data.is_active).severity"
/>
</template>
</Column>
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right">
<template #body>
<Button
icon="pi pi-ellipsis-v"
text
rounded
@click="(event) => event.stopPropagation()"
/>
</template>
</Column>
</DataTable>
</template>
</Card>
</div>
</template>

View File

@ -0,0 +1,323 @@
<!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

@ -1,82 +0,0 @@
import { ref, computed } from 'vue';
import type { Product, WarehouseStats } from '../types/warehouse';
import { warehouseService } from '../services/warehouseService';
export function useWarehouse() {
const products = ref<Product[]>([]);
const stats = ref<WarehouseStats | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
// Computed properties
const lowStockProducts = computed(() =>
products.value.filter(p => p.quantity < p.minStock)
);
const totalInventoryValue = computed(() =>
products.value.reduce((sum, p) => sum + (p.quantity * p.unitPrice), 0)
);
// Methods
const loadProducts = async () => {
loading.value = true;
error.value = null;
try {
products.value = await warehouseService.getProducts();
} catch (err) {
error.value = 'Error al cargar productos';
console.error(err);
} finally {
loading.value = false;
}
};
const loadStats = async () => {
loading.value = true;
error.value = null;
try {
stats.value = await warehouseService.getStats();
} catch (err) {
error.value = 'Error al cargar estadísticas';
console.error(err);
} finally {
loading.value = false;
}
};
const updateQuantity = async (productId: string, quantity: number) => {
loading.value = true;
error.value = null;
try {
const updated = await warehouseService.updateProductQuantity(productId, quantity);
if (updated) {
const index = products.value.findIndex(p => p.id === productId);
if (index !== -1) {
products.value[index] = updated;
}
}
} catch (err) {
error.value = 'Error al actualizar cantidad';
console.error(err);
} finally {
loading.value = false;
}
};
return {
// State
products,
stats,
loading,
error,
// Computed
lowStockProducts,
totalInventoryValue,
// Methods
loadProducts,
loadStats,
updateQuantity
};
}

View File

@ -0,0 +1,51 @@
import api from '../../../services/api';
import type {
CreateClassificationData,
ClassificationsResponse
} from '../types/warehouse.clasification';
export const warehouseClassificationService = {
async getClassifications() {
try {
const response = await api.get<ClassificationsResponse>('/api/catalogs/warehouse-classifications');
console.log('Classifications response:', response);
return response;
} catch (error) {
console.error('Error fetching classifications:', error);
throw error;
}
},
async createClassification(data: CreateClassificationData) {
try {
const response = await api.post('/api/catalogs/warehouse-classifications', data);
console.log('Classification created:', response);
return response;
} catch (error) {
console.error('Error creating classification:', error);
throw error;
}
},
async updateClassification(id: number, data: CreateClassificationData) {
try {
const response = await api.put(`/api/catalogs/warehouse-classifications/${id}`, data);
console.log('Classification updated:', response);
return response;
} catch (error) {
console.error('Error updating classification:', error);
throw error;
}
},
async deleteClassification(id: number) {
try {
const response = await api.delete(`/api/catalogs/warehouse-classifications/${id}`);
console.log('Classification deleted:', response);
return response;
} catch (error) {
console.error('Error deleting classification:', error);
throw error;
}
}
};

View File

@ -1,152 +1,15 @@
import type { Product, WarehouseMovement, WarehouseStats } from '../types/warehouse';
import api from '../../../services/api';
import type { WarehousesResponse } from '../types/warehouse';
// Simulación de datos de ejemplo
const mockProducts: Product[] = [
{
id: '1',
name: 'Laptop Dell XPS 15',
sku: 'LAP-001',
category: 'Electrónica',
quantity: 45,
minStock: 10,
maxStock: 100,
unitPrice: 1299.99,
location: 'A-1-01',
lastUpdated: new Date('2025-11-01')
},
{
id: '2',
name: 'Mouse Logitech MX Master',
sku: 'MOU-001',
category: 'Accesorios',
quantity: 8,
minStock: 15,
maxStock: 50,
unitPrice: 99.99,
location: 'B-2-03',
lastUpdated: new Date('2025-11-03')
},
{
id: '3',
name: 'Teclado Mecánico RGB',
sku: 'KEY-001',
category: 'Accesorios',
quantity: 120,
minStock: 20,
maxStock: 150,
unitPrice: 149.99,
location: 'B-2-05',
lastUpdated: new Date('2025-11-04')
}
];
const mockMovements: WarehouseMovement[] = [
{
id: '1',
productId: '1',
productName: 'Laptop Dell XPS 15',
type: 'in',
quantity: 20,
reason: 'Compra a proveedor',
date: new Date('2025-11-01'),
user: 'Admin'
},
{
id: '2',
productId: '2',
productName: 'Mouse Logitech MX Master',
type: 'out',
quantity: 5,
reason: 'Venta',
date: new Date('2025-11-03'),
user: 'Admin'
}
];
/**
* Servicio para gestionar operaciones del almacén
*/
export const warehouseService = {
/**
* Obtiene todos los productos del inventario
*/
async getProducts(): Promise<Product[]> {
// Simular llamada API
return new Promise((resolve) => {
setTimeout(() => resolve(mockProducts), 500);
});
},
/**
* Obtiene un producto por ID
*/
async getProductById(id: string): Promise<Product | null> {
return new Promise((resolve) => {
setTimeout(() => {
const product = mockProducts.find(p => p.id === id);
resolve(product || null);
}, 300);
});
},
/**
* Obtiene los movimientos del almacén
*/
async getMovements(): Promise<WarehouseMovement[]> {
return new Promise((resolve) => {
setTimeout(() => resolve(mockMovements), 500);
});
},
/**
* Obtiene las estadísticas del almacén
*/
async getStats(): Promise<WarehouseStats> {
return new Promise((resolve) => {
setTimeout(() => {
const stats: WarehouseStats = {
totalProducts: mockProducts.reduce((sum, p) => sum + p.quantity, 0),
totalValue: mockProducts.reduce((sum, p) => sum + (p.quantity * p.unitPrice), 0),
lowStockItems: mockProducts.filter(p => p.quantity < p.minStock).length,
recentMovements: mockMovements.length
};
resolve(stats);
}, 300);
});
},
/**
* Crea un nuevo movimiento de inventario
*/
async createMovement(movement: Omit<WarehouseMovement, 'id' | 'date'>): Promise<WarehouseMovement> {
return new Promise((resolve) => {
setTimeout(() => {
const newMovement: WarehouseMovement = {
...movement,
id: String(mockMovements.length + 1),
date: new Date()
};
mockMovements.push(newMovement);
resolve(newMovement);
}, 500);
});
},
/**
* Actualiza la cantidad de un producto
*/
async updateProductQuantity(productId: string, quantity: number): Promise<Product | null> {
return new Promise((resolve) => {
setTimeout(() => {
const product = mockProducts.find(p => p.id === productId);
if (product) {
product.quantity = quantity;
product.lastUpdated = new Date();
resolve(product);
} else {
resolve(null);
async getWarehouses() {
try {
const response = await api.get<WarehousesResponse>('/api/warehouses');
console.log('Warehouses response:', response);
return response;
} catch (error) {
console.error('Error fetching warehouses:', error);
throw error;
}
}, 500);
});
}
};

View File

@ -0,0 +1,46 @@
export interface CreateClassificationData {
code: string;
name: string;
description: string;
parent_id: number | null;
}
export interface Classification {
id: number;
code: string;
name: string;
description: string;
is_active: boolean;
parent_id: number | null;
created_at: string;
updated_at: string;
deleted_at: string | null;
children?: Classification[];
}
export interface ClassificationsPagination {
current_page: number;
data: Classification[];
first_page_url: string;
from: number | null;
last_page: number;
last_page_url: string;
links: {
url: string | null;
label: string;
active: boolean;
}[];
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number | null;
total: number;
}
export interface ClassificationsResponse {
status: string;
data: {
warehouse_classifications: ClassificationsPagination;
};
}

View File

@ -1,37 +1,41 @@
// Types para el módulo Warehouse
export interface Product {
id: string;
export interface Warehouse {
id: number;
code: string;
name: string;
sku: string;
category: string;
quantity: number;
minStock: number;
maxStock: number;
unitPrice: number;
location: string;
lastUpdated: Date;
description: string;
address: string;
is_active: boolean;
created_at: string;
updated_at: string;
deleted_at: string | null;
classifications: any[];
}
export interface WarehouseMovement {
id: string;
productId: string;
productName: string;
type: 'in' | 'out' | 'adjustment';
quantity: number;
reason: string;
date: Date;
user: string;
export interface WarehousePagination {
current_page: number;
data: Warehouse[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
links: PaginationLink[];
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
}
export interface WarehouseStats {
totalProducts: number;
totalValue: number;
lowStockItems: number;
recentMovements: number;
export interface PaginationLink {
url: string | null;
label: string;
active: boolean;
}
export interface InventoryFilter {
search?: string;
category?: string;
lowStock?: boolean;
export interface WarehousesResponse {
status: string;
data: {
warehouses: WarehousePagination;
};
}

View File

@ -4,7 +4,10 @@ import { useAuth } from '../modules/auth/composables/useAuth';
// Importar vistas
import Login from '../modules/auth/components/Login.vue';
import MainLayout from '../MainLayout.vue';
import WarehouseDashboard from '../modules/warehouse/components/WarehouseDashboard.vue';
import WarehouseIndex from '../modules/warehouse/components/WarehouseIndex.vue';
import WarehouseForm from '../modules/warehouse/components/WarehouseForm.vue';
import WarehouseCategory from '../modules/warehouse/components/WarehouseCategory.vue';
import WarehouseClassification from '../modules/warehouse/components/WarehouseClassification.vue';
const routes: RouteRecordRaw[] = [
{
@ -26,7 +29,7 @@ const routes: RouteRecordRaw[] = [
{
path: '',
name: 'Dashboard',
component: WarehouseDashboard,
component: WarehouseIndex,
meta: {
title: 'Dashboard',
requiresAuth: true
@ -35,14 +38,53 @@ const routes: RouteRecordRaw[] = [
{
path: 'warehouse',
name: 'Warehouse',
redirect: '/warehouse/inventory',
meta: {
title: 'Almacén',
requiresAuth: true
},
children: [
{
path: '',
name: 'WarehouseHome',
component: WarehouseIndex,
meta: {
title: 'Almacén - Dashboard',
requiresAuth: true
}
},
{
path: 'create',
name: 'WarehouseCreate',
component: WarehouseForm,
meta: {
title: 'Crear Almacén',
requiresAuth: true
}
},
{
path: 'classifications',
name: 'WarehouseClassifications',
component: WarehouseClassification,
meta: {
title: 'Clasificaciones de Almacén',
requiresAuth: true
}
},
{
path: 'inventory',
name: 'WarehouseInventory',
component: WarehouseDashboard,
component: WarehouseIndex,
meta: {
title: 'Inventario',
title: 'Inventario - Almacén',
requiresAuth: true
}
},
{
path: 'movements',
name: 'WarehouseMovements',
component: WarehouseIndex,
meta: {
title: 'Movimientos - Almacén',
requiresAuth: true
}
}

View File

@ -0,0 +1,85 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { warehouseService } from '../modules/warehouse/services/warehouseService';
import type { Warehouse } from '../modules/warehouse/types/warehouse';
export const useWarehouseStore = defineStore('warehouse', () => {
// State
const warehouses = ref<Warehouse[]>([]);
const loading = ref(false);
const loaded = ref(false);
const error = ref<string | null>(null);
// Getters
const activeWarehouses = computed(() =>
warehouses.value.filter(w => w.is_active)
);
const inactiveWarehouses = computed(() =>
warehouses.value.filter(w => !w.is_active)
);
const warehouseCount = computed(() => warehouses.value.length);
const getWarehouseById = computed(() => {
return (id: number) => warehouses.value.find(w => w.id === id);
});
const getWarehouseByCode = computed(() => {
return (code: string) => warehouses.value.find(w => w.code === code);
});
// Actions
const fetchWarehouses = async (force = false) => {
// Si ya están cargados y no se fuerza la recarga, no hacer nada
if (loaded.value && !force) {
console.log('Warehouses already loaded from store');
return;
}
try {
loading.value = true;
error.value = null;
const response = await warehouseService.getWarehouses();
warehouses.value = response.data.data.warehouses.data;
loaded.value = true;
console.log('Warehouses loaded into store:', warehouses.value.length);
} catch (err) {
error.value = err instanceof Error ? err.message : 'Error loading warehouses';
console.error('Error in warehouse store:', err);
throw err;
} finally {
loading.value = false;
}
};
const refreshWarehouses = () => {
return fetchWarehouses(true);
};
const clearWarehouses = () => {
warehouses.value = [];
loaded.value = false;
error.value = null;
};
return {
// State
warehouses,
loading,
loaded,
error,
// Getters
activeWarehouses,
inactiveWarehouses,
warehouseCount,
getWarehouseById,
getWarehouseByCode,
// Actions
fetchWarehouses,
refreshWarehouses,
clearWarehouses,
};
});