feat: integrate classification store and enhance warehouse management components

This commit is contained in:
Edgar Mendez Mendoza 2025-11-07 12:45:32 -06:00
parent 3bea03f9db
commit eeead68189
7 changed files with 352 additions and 157 deletions

1
components.d.ts vendored
View File

@ -40,6 +40,7 @@ declare module 'vue' {
Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default'] Sidebar: typeof import('./src/components/layout/Sidebar.vue')['default']
Tag: typeof import('primevue/tag')['default'] Tag: typeof import('primevue/tag')['default']
Textarea: typeof import('primevue/textarea')['default'] Textarea: typeof import('primevue/textarea')['default']
Toast: typeof import('primevue/toast')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default'] TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useConfirm } from 'primevue/useconfirm'; import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import Button from 'primevue/button'; import Button from 'primevue/button';
@ -13,11 +13,12 @@ import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea'; import Textarea from 'primevue/textarea';
import ProgressSpinner from 'primevue/progressspinner'; import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { warehouseClassificationService } from '../services/warehouseClasificationService'; import { useClassificationStore } from '../stores/classificationStore';
import type { Classification } from '../types/warehouse.clasification'; import type { Classification } from '../types/warehouse.clasification';
const confirm = useConfirm(); const confirm = useConfirm();
const toast = useToast(); const toast = useToast();
const classificationStore = useClassificationStore();
interface Category { interface Category {
id: number; id: number;
@ -29,14 +30,16 @@ interface Category {
subcategories: Category[]; subcategories: Category[];
} }
const categories = ref<Category[]>([]);
const selectedCategory = ref<Category | null>(null); const selectedCategory = ref<Category | null>(null);
const showCreateModal = ref(false); const showCreateModal = ref(false);
const isSubmitting = ref(false); const isSubmitting = ref(false);
const loading = ref(false);
const isEditMode = ref(false); const isEditMode = ref(false);
const editingId = ref<number | null>(null); const editingId = ref<number | null>(null);
// Computed properties from store
const loading = computed(() => classificationStore.loading);
const categories = computed(() => transformClassifications(classificationStore.classifications));
// Form data // Form data
const formData = ref({ const formData = ref({
code: '', code: '',
@ -68,10 +71,7 @@ const transformClassifications = (classifications: Classification[]): Category[]
const loadClassifications = async () => { const loadClassifications = async () => {
try { try {
loading.value = true; await classificationStore.fetchClassifications();
const response = await warehouseClassificationService.getClassifications();
const classificationsData = response.data.data.warehouse_classifications.data;
categories.value = transformClassifications(classificationsData);
// Select first category by default // Select first category by default
if (categories.value.length > 0 && categories.value[0]) { if (categories.value.length > 0 && categories.value[0]) {
@ -79,8 +79,12 @@ const loadClassifications = async () => {
} }
} catch (error) { } catch (error) {
console.error('Error loading classifications:', error); console.error('Error loading classifications:', error);
} finally { toast.add({
loading.value = false; severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar las clasificaciones.',
life: 3000
});
} }
}; };
@ -113,7 +117,7 @@ const createClassification = async () => {
if (isEditMode.value && editingId.value) { if (isEditMode.value && editingId.value) {
// Update existing classification // Update existing classification
await warehouseClassificationService.updateClassification(editingId.value, formData.value); await classificationStore.updateClassification(editingId.value, formData.value);
toast.add({ toast.add({
severity: 'success', severity: 'success',
@ -123,7 +127,7 @@ const createClassification = async () => {
}); });
} else { } else {
// Create new classification // Create new classification
await warehouseClassificationService.createClassification(formData.value); await classificationStore.createClassification(formData.value);
toast.add({ toast.add({
severity: 'success', severity: 'success',
@ -133,9 +137,6 @@ const createClassification = async () => {
}); });
} }
// Reload classifications
await loadClassifications();
showCreateModal.value = false; showCreateModal.value = false;
isEditMode.value = false; isEditMode.value = false;
editingId.value = null; editingId.value = null;
@ -203,7 +204,7 @@ const deleteCategory = () => {
accept: async () => { accept: async () => {
try { try {
const categoryName = selectedCategory.value!.name; const categoryName = selectedCategory.value!.name;
await warehouseClassificationService.deleteClassification(selectedCategory.value!.id); await classificationStore.deleteClassification(selectedCategory.value!.id);
toast.add({ toast.add({
severity: 'success', severity: 'success',
@ -214,9 +215,6 @@ const deleteCategory = () => {
// Clear selection // Clear selection
selectedCategory.value = null; selectedCategory.value = null;
// Reload classifications
await loadClassifications();
} catch (error) { } catch (error) {
console.error('Error deleting classification:', error); console.error('Error deleting classification:', error);
toast.add({ toast.add({
@ -253,7 +251,7 @@ const deleteSubcategory = (subcategory: Category) => {
acceptClass: 'p-button-danger', acceptClass: 'p-button-danger',
accept: async () => { accept: async () => {
try { try {
await warehouseClassificationService.deleteClassification(subcategory.id); await classificationStore.deleteClassification(subcategory.id);
toast.add({ toast.add({
severity: 'success', severity: 'success',
@ -261,9 +259,6 @@ const deleteSubcategory = (subcategory: Category) => {
detail: `La subclasificación "${subcategory.name}" ha sido eliminada exitosamente.`, detail: `La subclasificación "${subcategory.name}" ha sido eliminada exitosamente.`,
life: 3000 life: 3000
}); });
// Reload classifications
await loadClassifications();
} catch (error) { } catch (error) {
console.error('Error deleting subcategory:', error); console.error('Error deleting subcategory:', error);
toast.add({ toast.add({

View File

@ -1,8 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import Breadcrumb from 'primevue/breadcrumb';
import Button from 'primevue/button';
import Card from 'primevue/card';
import InputText from 'primevue/inputtext';
import InputSwitch from 'primevue/inputswitch';
import Textarea from 'primevue/textarea';
import Dropdown from 'primevue/dropdown';
import Chip from 'primevue/chip';
import Avatar from 'primevue/avatar';
import Toast from 'primevue/toast';
import { warehouseService } from '../services/warehouseService';
import { useClassificationStore } from '../stores/classificationStore';
const router = useRouter(); const router = useRouter();
const toast = useToast();
const classificationStore = useClassificationStore();
const breadcrumbItems = ref([ const breadcrumbItems = ref([
{ label: 'Almacenes', route: '/warehouse' }, { label: 'Almacenes', route: '/warehouse' },
@ -18,41 +33,88 @@ const home = ref({
const formData = ref({ const formData = ref({
name: '', name: '',
code: '', code: '',
description: '',
address: '', address: '',
status: 'active', is_active: true,
capacity: null,
phone: '', phone: '',
email: '' email: ''
}); });
const statusOptions = [ const isSubmitting = ref(false);
{ label: 'Activo', value: 'active' },
{ label: 'Inactivo', value: 'inactive' }, // Categories from store
{ label: 'En Mantenimiento', value: 'maintenance' } const availableClassifications = computed(() => {
]; return classificationStore.activeClassifications.map(cls => ({
id: cls.id,
name: cls.name,
code: cls.code,
parent_id: cls.parent_id,
children: cls.children || []
}));
});
// Get root classifications (categories without parent)
const rootClassifications = computed(() =>
availableClassifications.value.filter(c => c.parent_id === null)
);
// Categories // Categories
const assignedCategories = ref([ const assignedCategories = ref<any[]>([]);
{ id: 1, name: 'Almacenamiento a Granel', color: 'info' },
{ id: 2, name: 'Mercancía General', color: 'success' }, // Get classification IDs from assigned categories
{ id: 3, name: 'Control de Temperatura > Refrigerado', color: 'warn' } const getClassificationIds = () => {
]); return assignedCategories.value.map(cat => cat.id);
};
const selectedParentCategory = ref(null); const selectedParentCategory = ref(null);
const selectedSubCategory = ref(null); const selectedSubCategory = ref(null);
const newCategoryName = ref('');
const newCategoryParent = ref(null);
const parentCategories = [ // Get subcategories for selected parent
{ label: 'Control de Temperatura', value: 'temp' }, const availableSubcategories = computed(() => {
{ label: 'Materiales Peligrosos', value: 'hazmat' }, if (!selectedParentCategory.value) return [];
{ label: 'Alto Valor', value: 'high-value' } const parent = availableClassifications.value.find(c => c.id === selectedParentCategory.value);
]; return parent?.children || [];
});
const subCategories = [ const addSelectedCategory = () => {
{ label: 'Refrigerado', value: 'refrigerated' }, if (!selectedParentCategory.value) return;
{ label: 'Congelado', value: 'frozen' }
]; const parent = availableClassifications.value.find(c => c.id === selectedParentCategory.value);
if (!parent) return;
let categoryToAdd = parent;
let categoryName = parent.name;
// If subcategory is selected, use it instead
if (selectedSubCategory.value) {
const subcat = parent.children?.find((c: any) => c.id === selectedSubCategory.value);
if (subcat) {
categoryToAdd = subcat;
categoryName = `${parent.name} > ${subcat.name}`;
}
}
// Check if already added
if (assignedCategories.value.some(c => c.id === categoryToAdd.id)) {
toast.add({
severity: 'warn',
summary: 'Categoría ya agregada',
detail: 'Esta categoría ya está asignada al almacén.',
life: 3000
});
return;
}
assignedCategories.value.push({
id: categoryToAdd.id,
name: categoryName,
code: categoryToAdd.code
});
// Reset selections
selectedParentCategory.value = null;
selectedSubCategory.value = null;
};
// Staff // Staff
const assignedStaff = ref([ const assignedStaff = ref([
@ -76,25 +138,12 @@ const assignedStaff = ref([
} }
]); ]);
const saveDisabled = ref(true); const saveDisabled = ref(false);
const removeCategory = (id: number) => { const removeCategory = (id: number) => {
assignedCategories.value = assignedCategories.value.filter(cat => cat.id !== id); 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 = () => { const addStaff = () => {
console.log('Add staff'); console.log('Add staff');
}; };
@ -111,13 +160,50 @@ const cancel = () => {
router.push('/warehouse'); router.push('/warehouse');
}; };
const save = () => { const save = async () => {
console.log('Save warehouse:', formData.value); try {
isSubmitting.value = true;
const warehouseData = {
code: formData.value.code,
name: formData.value.name,
description: formData.value.description,
classifications: getClassificationIds()
};
await warehouseService.createWarehouse(warehouseData);
// Redirect to warehouse list with success message
router.push({
path: '/warehouse',
query: {
created: 'true',
name: formData.value.name
}
});
} catch (error) {
console.error('Error creating warehouse:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo crear el almacén. Por favor, intenta nuevamente.',
life: 3000
});
} finally {
isSubmitting.value = false;
}
}; };
onMounted(async () => {
await classificationStore.fetchClassifications();
});
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Header con Breadcrumb y Acciones --> <!-- Header con Breadcrumb y Acciones -->
<div class="flex flex-wrap items-center justify-between gap-4"> <div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@ -150,10 +236,12 @@ const save = () => {
label="Cancelar" label="Cancelar"
outlined outlined
@click="cancel" @click="cancel"
:disabled="isSubmitting"
/> />
<Button <Button
label="Guardar Cambios" label="Guardar Cambios"
:disabled="saveDisabled" :disabled="saveDisabled || !formData.name || !formData.code"
:loading="isSubmitting"
@click="save" @click="save"
/> />
</div> </div>
@ -191,8 +279,20 @@ const save = () => {
v-model="formData.code" v-model="formData.code"
class="w-full" class="w-full"
placeholder="WH-001" placeholder="WH-001"
disabled />
:class="'bg-surface-100 dark:bg-surface-700'" </div>
<!-- Description -->
<div class="col-span-full">
<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 general del almacén"
/> />
</div> </div>
@ -215,54 +315,15 @@ const save = () => {
<label for="status" class="block text-sm font-medium mb-2"> <label for="status" class="block text-sm font-medium mb-2">
Estado Estado
</label> </label>
<Dropdown <div class="flex items-center gap-3">
id="status" <InputSwitch
v-model="formData.status" id="status"
:options="statusOptions" v-model="formData.is_active"
optionLabel="label" />
optionValue="value" <span class="text-sm font-medium">
class="w-full" {{ formData.is_active ? 'Activo' : 'Inactivo' }}
/> </span>
</div> </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>
</div> </div>
</template> </template>
@ -301,10 +362,12 @@ const save = () => {
<Dropdown <Dropdown
id="parent-category" id="parent-category"
v-model="selectedParentCategory" v-model="selectedParentCategory"
:options="parentCategories" :options="rootClassifications"
optionLabel="label" optionLabel="name"
optionValue="id"
placeholder="Selecciona una categoría..." placeholder="Selecciona una categoría..."
class="w-full" class="w-full"
showClear
/> />
</div> </div>
@ -315,43 +378,28 @@ const save = () => {
<Dropdown <Dropdown
id="sub-category" id="sub-category"
v-model="selectedSubCategory" v-model="selectedSubCategory"
:options="subCategories" :options="availableSubcategories"
optionLabel="label" optionLabel="name"
optionValue="id"
placeholder="Selecciona una subcategoría..." placeholder="Selecciona una subcategoría..."
class="w-full" class="w-full"
showClear
:disabled="!selectedParentCategory"
/> />
</div> </div>
</div> </div>
<!-- Create New Category --> <!-- Add Category Button -->
<div> <div>
<label for="new-category" class="block text-sm font-medium mb-2"> <Button
O Crear Nueva Categoría label="Agregar Categoría Seleccionada"
</label> icon="pi pi-plus"
<div class="flex gap-2"> severity="secondary"
<div class="flex-1 space-y-2"> outlined
<InputText @click="addSelectedCategory"
id="new-category" :disabled="!selectedParentCategory"
v-model="newCategoryName" class="w-full"
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>
</div> </div>
</template> </template>
@ -359,8 +407,8 @@ const save = () => {
</div> </div>
<!-- Sidebar - 1 column --> <!-- Sidebar - 1 column -->
<div class="lg:col-span-1"> <!-- <div class="lg:col-span-1">
<!-- Assigned Staff Card -->
<Card> <Card>
<template #title> <template #title>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@ -418,7 +466,7 @@ const save = () => {
</div> </div>
</template> </template>
</Card> </Card>
</div> </div> -->
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,9 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast';
import { useWarehouseStore } from '../../../stores/warehouseStore'; import { useWarehouseStore } from '../../../stores/warehouseStore';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const toast = useToast();
const warehouseStore = useWarehouseStore(); const warehouseStore = useWarehouseStore();
const searchQuery = ref(''); const searchQuery = ref('');
@ -47,17 +51,31 @@ const createWarehouse = () => {
router.push({ name: 'WarehouseCreate' }); router.push({ name: 'WarehouseCreate' });
}; };
const loadWarehouses = async () => { onMounted(async () => {
await warehouseStore.fetchWarehouses(); // Reload warehouses to show new data
}; await warehouseStore.refreshWarehouses();
onMounted(() => { // Check if we just created a warehouse
loadWarehouses(); if (route.query.created === 'true') {
const warehouseName = route.query.name as string || 'el almacén';
toast.add({
severity: 'success',
summary: 'Almacén Creado',
detail: `El almacén "${warehouseName}" ha sido creado exitosamente.`,
life: 4000
});
// Clean up the query params
router.replace({ path: '/warehouse' });
}
}); });
</script> </script>
<template> <template>
<div class="space-y-6"> <div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Header --> <!-- Header -->
<div class="flex flex-wrap justify-between gap-4 items-center"> <div class="flex flex-wrap justify-between gap-4 items-center">
<div class="flex min-w-72 flex-col gap-1"> <div class="flex min-w-72 flex-col gap-1">

View File

@ -1,5 +1,5 @@
import api from '../../../services/api'; import api from '../../../services/api';
import type { WarehousesResponse } from '../types/warehouse'; import type { WarehousesResponse, CreateWarehouseData } from '../types/warehouse';
export const warehouseService = { export const warehouseService = {
async getWarehouses() { async getWarehouses() {
@ -11,5 +11,16 @@ export const warehouseService = {
console.error('Error fetching warehouses:', error); console.error('Error fetching warehouses:', error);
throw error; throw error;
} }
},
async createWarehouse(data: CreateWarehouseData) {
try {
const response = await api.post('/api/warehouses', data);
console.log('Warehouse created:', response);
return response;
} catch (error) {
console.error('Error creating warehouse:', error);
throw error;
}
} }
}; };

View File

@ -0,0 +1,115 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { warehouseClassificationService } from '../services/warehouseClasificationService';
import type { Classification } from '../types/warehouse.clasification';
export const useClassificationStore = defineStore('warehouseClassifications', () => {
// State
const classifications = ref<Classification[]>([]);
const loading = ref(false);
const loaded = ref(false);
const error = ref<string | null>(null);
// Getters
const rootClassifications = computed(() =>
classifications.value.filter(c => c.parent_id === null)
);
const activeClassifications = computed(() =>
classifications.value.filter(c => c.is_active)
);
const getClassificationById = computed(() => {
return (id: number) => classifications.value.find(c => c.id === id);
});
const getChildrenOf = computed(() => {
return (parentId: number) =>
classifications.value.filter(c => c.parent_id === parentId);
});
// Actions
const fetchClassifications = async (force = false) => {
if (loaded.value && !force) {
return classifications.value;
}
try {
loading.value = true;
error.value = null;
const response = await warehouseClassificationService.getClassifications();
classifications.value = response.data.data.warehouse_classifications.data;
loaded.value = true;
return classifications.value;
} catch (err) {
error.value = 'Error al cargar las clasificaciones';
console.error('Error fetching classifications:', err);
throw err;
} finally {
loading.value = false;
}
};
const refreshClassifications = async () => {
return fetchClassifications(true);
};
const clearClassifications = () => {
classifications.value = [];
loaded.value = false;
error.value = null;
};
const createClassification = async (data: any) => {
try {
const response = await warehouseClassificationService.createClassification(data);
await refreshClassifications();
return response;
} catch (err) {
error.value = 'Error al crear la clasificación';
throw err;
}
};
const updateClassification = async (id: number, data: any) => {
try {
const response = await warehouseClassificationService.updateClassification(id, data);
await refreshClassifications();
return response;
} catch (err) {
error.value = 'Error al actualizar la clasificación';
throw err;
}
};
const deleteClassification = async (id: number) => {
try {
const response = await warehouseClassificationService.deleteClassification(id);
await refreshClassifications();
return response;
} catch (err) {
error.value = 'Error al eliminar la clasificación';
throw err;
}
};
return {
// State
classifications,
loading,
loaded,
error,
// Getters
rootClassifications,
activeClassifications,
getClassificationById,
getChildrenOf,
// Actions
fetchClassifications,
refreshClassifications,
clearClassifications,
createClassification,
updateClassification,
deleteClassification
};
});

View File

@ -11,6 +11,13 @@ export interface Warehouse {
classifications: any[]; classifications: any[];
} }
export interface CreateWarehouseData {
code: string;
name: string;
description: string;
classifications: number[];
}
export interface WarehousePagination { export interface WarehousePagination {
current_page: number; current_page: number;
data: Warehouse[]; data: Warehouse[];