feat: Add is_active field and classifications management to product and commercial classification components

This commit is contained in:
Edgar Mendez Mendoza 2025-11-10 17:11:11 -06:00
parent 73fb017ca6
commit f98c2ba580
4 changed files with 231 additions and 70 deletions

View File

@ -11,6 +11,7 @@ import Dialog from 'primevue/dialog';
import Dropdown from 'primevue/dropdown'; import Dropdown from 'primevue/dropdown';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea'; import Textarea from 'primevue/textarea';
import InputSwitch from 'primevue/inputswitch';
import ProgressSpinner from 'primevue/progressspinner'; import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { useComercialClassificationStore } from '../stores/comercialClassificationStore'; import { useComercialClassificationStore } from '../stores/comercialClassificationStore';
@ -26,6 +27,7 @@ interface Category {
name: string; name: string;
description: string; description: string;
parent_id: number | null; parent_id: number | null;
is_active: number;
created_at: string; created_at: string;
subcategories: Category[]; subcategories: Category[];
} }
@ -45,7 +47,16 @@ const formData = ref({
code: '', code: '',
name: '', name: '',
description: '', 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 // Transform API data to component structure
@ -56,6 +67,7 @@ const transformClassifications = (classifications: ComercialClassification[]): C
name: cls.name, name: cls.name,
description: cls.description || '', description: cls.description || '',
parent_id: cls.parent_id, parent_id: cls.parent_id,
is_active: cls.is_active,
created_at: cls.created_at, created_at: cls.created_at,
subcategories: cls.children ? cls.children.map(child => ({ subcategories: cls.children ? cls.children.map(child => ({
id: child.id, id: child.id,
@ -63,6 +75,7 @@ const transformClassifications = (classifications: ComercialClassification[]): C
name: child.name, name: child.name,
description: child.description || '', description: child.description || '',
parent_id: child.parent_id, parent_id: child.parent_id,
is_active: child.is_active,
created_at: child.created_at, created_at: child.created_at,
subcategories: [] subcategories: []
})) : [] })) : []
@ -100,7 +113,8 @@ const addNewCategory = () => {
code: '', code: '',
name: '', name: '',
description: '', description: '',
parent_id: null parent_id: null,
is_active: 1
}; };
showCreateModal.value = true; showCreateModal.value = true;
}; };
@ -161,7 +175,8 @@ const createClassification = async () => {
code: '', code: '',
name: '', name: '',
description: '', description: '',
parent_id: null parent_id: null,
is_active: 1
}; };
} catch (error) { } catch (error) {
console.error('Error saving classification:', error); console.error('Error saving classification:', error);
@ -186,7 +201,8 @@ const addSubcategory = () => {
code: '', code: '',
name: '', name: '',
description: '', description: '',
parent_id: selectedCategory.value.id parent_id: selectedCategory.value.id,
is_active: 1
}; };
showCreateModal.value = true; showCreateModal.value = true;
} }
@ -201,7 +217,8 @@ const editCategory = () => {
code: selectedCategory.value.code, code: selectedCategory.value.code,
name: selectedCategory.value.name, name: selectedCategory.value.name,
description: selectedCategory.value.description, 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; showCreateModal.value = true;
}; };
@ -256,7 +273,8 @@ const editSubcategory = (subcategory: Category) => {
code: subcategory.code, code: subcategory.code,
name: subcategory.name, name: subcategory.name,
description: subcategory.description, description: subcategory.description,
parent_id: subcategory.parent_id parent_id: subcategory.parent_id,
is_active: subcategory.is_active
}; };
showCreateModal.value = true; showCreateModal.value = true;
}; };
@ -600,6 +618,24 @@ onMounted(() => {
Deja vacío para crear una clasificación raíz Deja vacío para crear una clasificación raíz
</small> </small>
</div> </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> </div>
<template #footer> <template #footer>

View File

@ -11,7 +11,7 @@ export const comercialClassificationService = {
* Get all comercial classifications with pagination * Get all comercial classifications with pagination
* GET /api/comercial-classifications * 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`, { const response = await api.get(`/api/comercial-classifications`, {
params: { page, per_page: perPage } params: { page, per_page: perPage }
}); });

View File

@ -1,10 +1,12 @@
<script setup lang="ts"> <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 InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea'; import Textarea from 'primevue/textarea';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Dropdown from 'primevue/dropdown';
import FileUpload from 'primevue/fileupload'; import FileUpload from 'primevue/fileupload';
import { useComercialClassificationStore } from '../../catalog/stores/comercialClassificationStore';
import { useUnitOfMeasureStore } from '../../catalog/stores/unitOfMeasureStore';
import type { Product, CreateProductData } from '../types/product'; import type { Product, CreateProductData } from '../types/product';
// Props // Props
@ -24,6 +26,10 @@ const emit = defineEmits<{
cancel: []; cancel: [];
}>(); }>();
// Stores
const clasificationsStore = useComercialClassificationStore();
const unitOfMeasureStore = useUnitOfMeasureStore();
// Form data // Form data
const formData = ref<CreateProductData>({ const formData = ref<CreateProductData>({
code: '', code: '',
@ -34,7 +40,8 @@ const formData = ref<CreateProductData>({
unit_of_measure_id: 0, unit_of_measure_id: 0,
suggested_sale_price: 0, suggested_sale_price: 0,
attributes: {}, attributes: {},
is_active: true is_active: true,
classifications: []
}); });
// Temporary string for price input // Temporary string for price input
@ -54,33 +61,90 @@ interface AttributeField {
const attributes = ref<AttributeField[]>([]); const attributes = ref<AttributeField[]>([]);
// Categories (placeholder - should come from a store) // Units of measure from store
const categories = ref([ const unitsOfMeasure = computed(() => {
{ label: 'Selecciona una categoría', value: null }, return unitOfMeasureStore.activeUnits.map(unit => ({
{ label: 'Electrónicos', value: 1 }, label: unit.name,
{ label: 'Ropa', value: 2 }, value: unit.id
{ label: 'Hogar', value: 3 }, }));
{ label: 'Oficina', value: 4 } });
]);
const selectedCategory = ref(null); // Search classifications
const searchClassification = ref('');
const subcategories = ref([ // Computed para clasificaciones filtradas por búsqueda
{ label: 'Selecciona una subcategoría', value: null }, const filteredClassifications = computed(() => {
{ label: 'Ratones y Teclados', value: 1 }, const search = searchClassification.value.toLowerCase().trim();
{ label: 'Monitores', value: 2 },
{ label: 'Cables y Adaptadores', value: 3 }
]);
const selectedSubcategory = ref(null); if (!search) {
// Retornar solo clasificaciones padre (sin parent_id)
return clasificationsStore.classifications.filter(cls => cls.parent_id === null);
}
// Unit of measure (placeholder - should come from a store) // Filtrar clasificaciones que coincidan con la búsqueda
const unitsOfMeasure = ref([ return clasificationsStore.classifications
{ label: 'Unidad', value: 1 }, .filter(cls => cls.parent_id === null)
{ label: 'Kilogramo', value: 2 }, .map(parent => {
{ label: 'Metro', value: 3 }, // Filtrar hijos que coincidan con la búsqueda
{ label: 'Litro', value: 4 } 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);
});
// Verificar si una clasificación está seleccionada
const isClassificationSelected = (classificationId: number) => {
return formData.value.classifications?.includes(classificationId) || false;
};
// 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 for product changes (when editing)
watch(() => props.product, (newProduct) => { watch(() => props.product, (newProduct) => {
@ -94,7 +158,8 @@ watch(() => props.product, (newProduct) => {
unit_of_measure_id: newProduct.unit_of_measure_id, unit_of_measure_id: newProduct.unit_of_measure_id,
suggested_sale_price: newProduct.suggested_sale_price, suggested_sale_price: newProduct.suggested_sale_price,
attributes: newProduct.attributes || {}, 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(); priceInput.value = newProduct.suggested_sale_price.toString();
@ -165,6 +230,9 @@ const handleSubmit = () => {
formData.value.attributes = attributesObject; 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); emit('save', formData.value);
}; };
@ -248,15 +316,20 @@ const onUpload = (event: any) => {
<label for="unit-measure" class="block text-sm font-medium mb-2"> <label for="unit-measure" class="block text-sm font-medium mb-2">
Unidad de Medida * Unidad de Medida *
</label> </label>
<Dropdown <select
id="unit-measure" id="unit-measure"
v-model="formData.unit_of_measure_id" v-model="formData.unit_of_measure_id"
:options="unitsOfMeasure" 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"
optionLabel="label" >
optionValue="value" <option value="0" disabled>Selecciona una unidad</option>
class="w-full" <option
placeholder="Selecciona una unidad" v-for="unit in unitsOfMeasure"
/> :key="unit.value"
:value="unit.value"
>
{{ unit.label }}
</option>
</select>
</div> </div>
<!-- Suggested Sale Price --> <!-- Suggested Sale Price -->
@ -408,40 +481,90 @@ const onUpload = (event: any) => {
<!-- Organization Card --> <!-- Organization Card -->
<Card> <Card>
<template #title> <template #title>
<h3 class="text-lg font-bold">Organización</h3> <h3 class="text-lg font-bold">Clasificaciones de Producto</h3>
</template> </template>
<template #content> <template #content>
<div class="space-y-6"> <div>
<!-- Category --> <!-- Search input -->
<div> <div class="relative mb-4">
<label for="category" class="block text-sm font-medium mb-2"> <InputText
Categoría v-model="searchClassification"
</label> type="search"
<Dropdown class="w-full pl-10"
id="category" placeholder="Buscar clasificaciones..."
v-model="selectedCategory"
:options="categories"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona una categoría"
/> />
</div> </div>
<!-- Subcategory --> <!-- Classifications tree -->
<div> <div class="space-y-2 h-96 overflow-y-auto pr-2">
<label for="subcategory" class="block text-sm font-medium mb-2"> <div
Subcategoría v-for="classification in filteredClassifications"
</label> :key="classification.id"
<Dropdown class="space-y-1"
id="subcategory" >
v-model="selectedSubcategory" <!-- Parent classification -->
:options="subcategories" <label class="flex items-center space-x-3 p-2 rounded-md hover:bg-surface-50 dark:hover:bg-surface-800/50 cursor-pointer">
optionLabel="label" <input
optionValue="value" type="checkbox"
class="w-full" :value="classification.id"
placeholder="Selecciona una subcategoría" :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>
</div> </div>
</template> </template>

View File

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