feature-comercial-module-ts #10

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

View File

@ -11,6 +11,7 @@ import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import InputSwitch from 'primevue/inputswitch';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { useComercialClassificationStore } from '../stores/comercialClassificationStore';
@ -26,6 +27,7 @@ interface Category {
name: string;
description: string;
parent_id: number | null;
is_active: number;
created_at: string;
subcategories: Category[];
}
@ -45,7 +47,16 @@ const formData = ref({
code: '',
name: '',
description: '',
parent_id: null as number | null
parent_id: null as number | null,
is_active: 1 as number
});
// Computed para el switch (convierte number a boolean y viceversa)
const isActiveSwitch = computed({
get: () => formData.value.is_active === 1,
set: (value: boolean) => {
formData.value.is_active = value ? 1 : 0;
}
});
// Transform API data to component structure
@ -56,6 +67,7 @@ const transformClassifications = (classifications: ComercialClassification[]): C
name: cls.name,
description: cls.description || '',
parent_id: cls.parent_id,
is_active: cls.is_active,
created_at: cls.created_at,
subcategories: cls.children ? cls.children.map(child => ({
id: child.id,
@ -63,6 +75,7 @@ const transformClassifications = (classifications: ComercialClassification[]): C
name: child.name,
description: child.description || '',
parent_id: child.parent_id,
is_active: child.is_active,
created_at: child.created_at,
subcategories: []
})) : []
@ -100,7 +113,8 @@ const addNewCategory = () => {
code: '',
name: '',
description: '',
parent_id: null
parent_id: null,
is_active: 1
};
showCreateModal.value = true;
};
@ -161,7 +175,8 @@ const createClassification = async () => {
code: '',
name: '',
description: '',
parent_id: null
parent_id: null,
is_active: 1
};
} catch (error) {
console.error('Error saving classification:', error);
@ -186,7 +201,8 @@ const addSubcategory = () => {
code: '',
name: '',
description: '',
parent_id: selectedCategory.value.id
parent_id: selectedCategory.value.id,
is_active: 1
};
showCreateModal.value = true;
}
@ -201,7 +217,8 @@ const editCategory = () => {
code: selectedCategory.value.code,
name: selectedCategory.value.name,
description: selectedCategory.value.description,
parent_id: selectedCategory.value.parent_id
parent_id: selectedCategory.value.parent_id,
is_active: selectedCategory.value.is_active
};
showCreateModal.value = true;
};
@ -256,7 +273,8 @@ const editSubcategory = (subcategory: Category) => {
code: subcategory.code,
name: subcategory.name,
description: subcategory.description,
parent_id: subcategory.parent_id
parent_id: subcategory.parent_id,
is_active: subcategory.is_active
};
showCreateModal.value = true;
};
@ -600,6 +618,24 @@ onMounted(() => {
Deja vacío para crear una clasificación raíz
</small>
</div>
<!-- Active Status -->
<div>
<div class="flex items-center justify-between">
<div>
<label for="is_active" class="block text-sm font-medium mb-1">
Estado
</label>
<small class="text-surface-500 dark:text-surface-400">
Activa o desactiva esta clasificación comercial
</small>
</div>
<InputSwitch
id="is_active"
v-model="isActiveSwitch"
/>
</div>
</div>
</div>
<template #footer>

View File

@ -11,7 +11,7 @@ export const comercialClassificationService = {
* Get all comercial classifications with pagination
* GET /api/comercial-classifications
*/
async getClassifications(page = 1, perPage = 10): Promise<ComercialClassificationResponse> {
async getClassifications(page = 1, perPage = 100): Promise<ComercialClassificationResponse> {
const response = await api.get(`/api/comercial-classifications`, {
params: { page, per_page: perPage }
});

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, onMounted } from 'vue';
import Card from 'primevue/card';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import Dropdown from 'primevue/dropdown';
import FileUpload from 'primevue/fileupload';
import { useComercialClassificationStore } from '../../catalog/stores/comercialClassificationStore';
import { useUnitOfMeasureStore } from '../../catalog/stores/unitOfMeasureStore';
import type { Product, CreateProductData } from '../types/product';
// Props
@ -24,6 +26,10 @@ const emit = defineEmits<{
cancel: [];
}>();
// Stores
const clasificationsStore = useComercialClassificationStore();
const unitOfMeasureStore = useUnitOfMeasureStore();
// Form data
const formData = ref<CreateProductData>({
code: '',
@ -34,7 +40,8 @@ const formData = ref<CreateProductData>({
unit_of_measure_id: 0,
suggested_sale_price: 0,
attributes: {},
is_active: true
is_active: true,
classifications: []
});
// Temporary string for price input
@ -54,33 +61,90 @@ interface AttributeField {
const attributes = ref<AttributeField[]>([]);
// Categories (placeholder - should come from a store)
const categories = ref([
{ label: 'Selecciona una categoría', value: null },
{ label: 'Electrónicos', value: 1 },
{ label: 'Ropa', value: 2 },
{ label: 'Hogar', value: 3 },
{ label: 'Oficina', value: 4 }
]);
// Units of measure from store
const unitsOfMeasure = computed(() => {
return unitOfMeasureStore.activeUnits.map(unit => ({
label: unit.name,
value: unit.id
}));
});
const selectedCategory = ref(null);
// Search classifications
const searchClassification = ref('');
const subcategories = ref([
{ label: 'Selecciona una subcategoría', value: null },
{ label: 'Ratones y Teclados', value: 1 },
{ label: 'Monitores', value: 2 },
{ label: 'Cables y Adaptadores', value: 3 }
]);
// Computed para clasificaciones filtradas por búsqueda
const filteredClassifications = computed(() => {
const search = searchClassification.value.toLowerCase().trim();
if (!search) {
// Retornar solo clasificaciones padre (sin parent_id)
return clasificationsStore.classifications.filter(cls => cls.parent_id === null);
}
// Filtrar clasificaciones que coincidan con la búsqueda
return clasificationsStore.classifications
.filter(cls => cls.parent_id === null)
.map(parent => {
// Filtrar hijos que coincidan con la búsqueda
const matchingChildren = parent.children?.filter(child =>
child.name.toLowerCase().includes(search) ||
child.code.toLowerCase().includes(search)
) || [];
// Incluir el padre si coincide o tiene hijos que coincidan
const parentMatches = parent.name.toLowerCase().includes(search) ||
parent.code.toLowerCase().includes(search);
if (parentMatches || matchingChildren.length > 0) {
return {
...parent,
children: matchingChildren.length > 0 ? matchingChildren : parent.children
};
}
return null;
})
.filter(cls => cls !== null);
});
const selectedSubcategory = ref(null);
// Verificar si una clasificación está seleccionada
const isClassificationSelected = (classificationId: number) => {
return formData.value.classifications?.includes(classificationId) || false;
};
// Unit of measure (placeholder - should come from a store)
const unitsOfMeasure = ref([
{ label: 'Unidad', value: 1 },
{ label: 'Kilogramo', value: 2 },
{ label: 'Metro', value: 3 },
{ label: 'Litro', value: 4 }
]);
// Toggle clasificación (agregar o quitar)
const toggleClassification = (classificationId: number) => {
if (!formData.value.classifications) {
formData.value.classifications = [];
}
const index = formData.value.classifications.indexOf(classificationId);
if (index > -1) {
// Quitar clasificación
formData.value.classifications.splice(index, 1);
} else {
// Agregar clasificación
formData.value.classifications.push(classificationId);
}
console.log('📋 Classifications updated:', formData.value.classifications);
};
const loadingClassifications = computed(() => clasificationsStore.loading);
// Load classifications on mount
onMounted(async () => {
console.log('🔄 ProductForm mounted, loading classifications and units of measure...');
try {
await Promise.all([
clasificationsStore.fetchClassifications(),
unitOfMeasureStore.fetchUnits()
]);
console.log('✅ Classifications loaded:', clasificationsStore.classifications.length);
console.log('✅ Units of measure loaded:', unitOfMeasureStore.units.length);
} catch (error) {
console.error('❌ Error loading data:', error);
}
});
// Watch for product changes (when editing)
watch(() => props.product, (newProduct) => {
@ -94,7 +158,8 @@ watch(() => props.product, (newProduct) => {
unit_of_measure_id: newProduct.unit_of_measure_id,
suggested_sale_price: newProduct.suggested_sale_price,
attributes: newProduct.attributes || {},
is_active: newProduct.is_active
is_active: newProduct.is_active,
classifications: newProduct.classifications?.map(c => c.id) || []
};
priceInput.value = newProduct.suggested_sale_price.toString();
@ -165,6 +230,9 @@ const handleSubmit = () => {
formData.value.attributes = attributesObject;
// Log the data being sent
console.log('📤 Datos enviados al servidor:', JSON.stringify(formData.value, null, 2));
emit('save', formData.value);
};
@ -248,15 +316,20 @@ const onUpload = (event: any) => {
<label for="unit-measure" class="block text-sm font-medium mb-2">
Unidad de Medida *
</label>
<Dropdown
<select
id="unit-measure"
v-model="formData.unit_of_measure_id"
:options="unitsOfMeasure"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una unidad"
/>
class="w-full rounded-lg border border-surface-300 dark:border-surface-600 bg-surface-0 dark:bg-surface-900 text-surface-900 dark:text-surface-0 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="0" disabled>Selecciona una unidad</option>
<option
v-for="unit in unitsOfMeasure"
:key="unit.value"
:value="unit.value"
>
{{ unit.label }}
</option>
</select>
</div>
<!-- Suggested Sale Price -->
@ -408,40 +481,90 @@ const onUpload = (event: any) => {
<!-- Organization Card -->
<Card>
<template #title>
<h3 class="text-lg font-bold">Organización</h3>
<h3 class="text-lg font-bold">Clasificaciones de Producto</h3>
</template>
<template #content>
<div class="space-y-6">
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium mb-2">
Categoría
</label>
<Dropdown
id="category"
v-model="selectedCategory"
:options="categories"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una categoría"
<div>
<!-- Search input -->
<div class="relative mb-4">
<InputText
v-model="searchClassification"
type="search"
class="w-full pl-10"
placeholder="Buscar clasificaciones..."
/>
</div>
<!-- Subcategory -->
<div>
<label for="subcategory" class="block text-sm font-medium mb-2">
Subcategoría
</label>
<Dropdown
id="subcategory"
v-model="selectedSubcategory"
:options="subcategories"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una subcategoría"
/>
<!-- Classifications tree -->
<div class="space-y-2 h-96 overflow-y-auto pr-2">
<div
v-for="classification in filteredClassifications"
:key="classification.id"
class="space-y-1"
>
<!-- Parent classification -->
<label class="flex items-center space-x-3 p-2 rounded-md hover:bg-surface-50 dark:hover:bg-surface-800/50 cursor-pointer">
<input
type="checkbox"
:value="classification.id"
:checked="isClassificationSelected(classification.id)"
@change="toggleClassification(classification.id)"
class="w-5 h-5 rounded border-surface-300 dark:border-surface-600 text-primary focus:ring-primary/50"
/>
<span class="text-surface-700 dark:text-surface-300">
{{ classification.name }}
</span>
</label>
<!-- Children classifications -->
<div
v-if="classification.children && classification.children.length > 0"
class="ml-6 pl-4 border-l-2 border-surface-200 dark:border-surface-700 space-y-1"
>
<label
v-for="child in classification.children"
:key="child.id"
class="flex items-center space-x-3 p-2 rounded-md hover:bg-surface-50 dark:hover:bg-surface-800/50 cursor-pointer"
>
<input
type="checkbox"
:value="child.id"
:checked="isClassificationSelected(child.id)"
@change="toggleClassification(child.id)"
class="w-5 h-5 rounded border-surface-300 dark:border-surface-600 text-primary focus:ring-primary/50"
/>
<span class="text-surface-700 dark:text-surface-300">
{{ child.name }}
</span>
</label>
</div>
</div>
<!-- Empty state -->
<div
v-if="filteredClassifications.length === 0 && !loadingClassifications"
class="text-center py-8 text-surface-500"
>
<i class="pi pi-inbox text-4xl mb-2"></i>
<p>No se encontraron clasificaciones</p>
</div>
<!-- Loading state -->
<div
v-if="loadingClassifications"
class="text-center py-8 text-surface-500"
>
<i class="pi pi-spin pi-spinner text-4xl mb-2"></i>
<p>Cargando clasificaciones...</p>
</div>
</div>
<!-- Selected count -->
<div class="mt-4 pt-4 border-t border-surface-200 dark:border-surface-700">
<div class="text-sm text-surface-600 dark:text-surface-400">
<span class="font-medium">{{ formData.classifications?.length || 0 }}</span>
{{ (formData.classifications?.length || 0) === 1 ? 'clasificación seleccionada' : 'clasificaciones seleccionadas' }}
</div>
</div>
</div>
</template>

View File

@ -30,6 +30,7 @@ export interface CreateProductData {
suggested_sale_price: number;
attributes?: Record<string, string[]>;
is_active?: boolean;
classifications?: number[];
}
export interface UpdateProductData {
@ -42,6 +43,7 @@ export interface UpdateProductData {
suggested_sale_price?: number;
attributes?: Record<string, string[]>;
is_active?: boolean;
classifications?: number[];
}
export interface ProductPagination {