feature-comercial-module-ts #13

Merged
edgar.mendez merged 38 commits from feature-comercial-module-ts into develop 2026-03-04 15:07:09 +00:00
7 changed files with 352 additions and 157 deletions
Showing only changes of commit eeead68189 - Show all commits

1
components.d.ts vendored
View File

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

View File

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

View File

@ -1,8 +1,23 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, onMounted, computed } from 'vue';
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 toast = useToast();
const classificationStore = useClassificationStore();
const breadcrumbItems = ref([
{ label: 'Almacenes', route: '/warehouse' },
@ -18,41 +33,88 @@ const home = ref({
const formData = ref({
name: '',
code: '',
description: '',
address: '',
status: 'active',
capacity: null,
is_active: true,
phone: '',
email: ''
});
const statusOptions = [
{ label: 'Activo', value: 'active' },
{ label: 'Inactivo', value: 'inactive' },
{ label: 'En Mantenimiento', value: 'maintenance' }
];
const isSubmitting = ref(false);
// Categories from store
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
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 assignedCategories = ref<any[]>([]);
// Get classification IDs from assigned categories
const getClassificationIds = () => {
return assignedCategories.value.map(cat => cat.id);
};
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' }
];
// Get subcategories for selected parent
const availableSubcategories = computed(() => {
if (!selectedParentCategory.value) return [];
const parent = availableClassifications.value.find(c => c.id === selectedParentCategory.value);
return parent?.children || [];
});
const subCategories = [
{ label: 'Refrigerado', value: 'refrigerated' },
{ label: 'Congelado', value: 'frozen' }
];
const addSelectedCategory = () => {
if (!selectedParentCategory.value) return;
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
const assignedStaff = ref([
@ -76,25 +138,12 @@ const assignedStaff = ref([
}
]);
const saveDisabled = ref(true);
const saveDisabled = ref(false);
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');
};
@ -111,13 +160,50 @@ const cancel = () => {
router.push('/warehouse');
};
const save = () => {
console.log('Save warehouse:', formData.value);
const save = async () => {
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>
<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Header con Breadcrumb y Acciones -->
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex flex-col gap-2">
@ -150,10 +236,12 @@ const save = () => {
label="Cancelar"
outlined
@click="cancel"
:disabled="isSubmitting"
/>
<Button
label="Guardar Cambios"
:disabled="saveDisabled"
:disabled="saveDisabled || !formData.name || !formData.code"
:loading="isSubmitting"
@click="save"
/>
</div>
@ -191,8 +279,20 @@ const save = () => {
v-model="formData.code"
class="w-full"
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>
@ -215,54 +315,15 @@ const save = () => {
<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 class="flex items-center gap-3">
<InputSwitch
id="status"
v-model="formData.is_active"
/>
<span class="text-sm font-medium">
{{ formData.is_active ? 'Activo' : 'Inactivo' }}
</span>
</div>
</div>
</div>
</template>
@ -301,10 +362,12 @@ const save = () => {
<Dropdown
id="parent-category"
v-model="selectedParentCategory"
:options="parentCategories"
optionLabel="label"
:options="rootClassifications"
optionLabel="name"
optionValue="id"
placeholder="Selecciona una categoría..."
class="w-full"
showClear
/>
</div>
@ -315,43 +378,28 @@ const save = () => {
<Dropdown
id="sub-category"
v-model="selectedSubCategory"
:options="subCategories"
optionLabel="label"
:options="availableSubcategories"
optionLabel="name"
optionValue="id"
placeholder="Selecciona una subcategoría..."
class="w-full"
showClear
:disabled="!selectedParentCategory"
/>
</div>
</div>
<!-- Create New Category -->
<!-- Add Category Button -->
<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>
<Button
label="Agregar Categoría Seleccionada"
icon="pi pi-plus"
severity="secondary"
outlined
@click="addSelectedCategory"
:disabled="!selectedParentCategory"
class="w-full"
/>
</div>
</div>
</template>
@ -359,8 +407,8 @@ const save = () => {
</div>
<!-- Sidebar - 1 column -->
<div class="lg:col-span-1">
<!-- Assigned Staff Card -->
<!-- <div class="lg:col-span-1">
<Card>
<template #title>
<div class="flex items-center justify-between">
@ -418,7 +466,7 @@ const save = () => {
</div>
</template>
</Card>
</div>
</div> -->
</div>
</div>
</template>

View File

@ -1,9 +1,13 @@
<script setup lang="ts">
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';
const router = useRouter();
const route = useRoute();
const toast = useToast();
const warehouseStore = useWarehouseStore();
const searchQuery = ref('');
@ -47,17 +51,31 @@ const createWarehouse = () => {
router.push({ name: 'WarehouseCreate' });
};
const loadWarehouses = async () => {
await warehouseStore.fetchWarehouses();
};
onMounted(() => {
loadWarehouses();
onMounted(async () => {
// Reload warehouses to show new data
await warehouseStore.refreshWarehouses();
// Check if we just created a warehouse
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>
<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Header -->
<div class="flex flex-wrap justify-between gap-4 items-center">
<div class="flex min-w-72 flex-col gap-1">

View File

@ -1,5 +1,5 @@
import api from '../../../services/api';
import type { WarehousesResponse } from '../types/warehouse';
import type { WarehousesResponse, CreateWarehouseData } from '../types/warehouse';
export const warehouseService = {
async getWarehouses() {
@ -11,5 +11,16 @@ export const warehouseService = {
console.error('Error fetching warehouses:', 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[];
}
export interface CreateWarehouseData {
code: string;
name: string;
description: string;
classifications: number[];
}
export interface WarehousePagination {
current_page: number;
data: Warehouse[];