feature-comercial-module-ts #10
8
components.d.ts
vendored
8
components.d.ts
vendored
@ -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
136
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
5
src/modules/warehouse/components/WarehouseCategory.vue
Normal file
5
src/modules/warehouse/components/WarehouseCategory.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<p>
|
||||
Categorías de Almacenes
|
||||
</p>
|
||||
</template>
|
||||
585
src/modules/warehouse/components/WarehouseClassification.vue
Normal file
585
src/modules/warehouse/components/WarehouseClassification.vue
Normal 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>
|
||||
@ -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>
|
||||
424
src/modules/warehouse/components/WarehouseForm.vue
Normal file
424
src/modules/warehouse/components/WarehouseForm.vue
Normal 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>
|
||||
184
src/modules/warehouse/components/WarehouseIndex.vue
Normal file
184
src/modules/warehouse/components/WarehouseIndex.vue
Normal 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>
|
||||
323
src/modules/warehouse/components/index.html
Normal file
323
src/modules/warehouse/components/index.html
Normal 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&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>
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
46
src/modules/warehouse/types/warehouse.clasification.d.ts
vendored
Normal file
46
src/modules/warehouse/types/warehouse.clasification.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
62
src/modules/warehouse/types/warehouse.d.ts
vendored
62
src/modules/warehouse/types/warehouse.d.ts
vendored
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
85
src/stores/warehouseStore.ts
Normal file
85
src/stores/warehouseStore.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user