Edgar Mendez Mendoza 3bea03f9db feat: add warehouse management components and services
- Implemented WarehouseForm.vue for creating new warehouses with form validation and category assignment.
- Developed WarehouseIndex.vue for displaying a list of warehouses with search and filter functionalities.
- Created warehouseClasificationService.ts for handling warehouse classification API interactions.
- Defined types for warehouse classifications in warehouse.clasification.d.ts.
- Established a Pinia store (warehouseStore.ts) for managing warehouse state and actions.
- Added an index.html file for the warehouse management interface layout.
2025-11-07 12:14:40 -06:00

585 lines
24 KiB
Vue

<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>