feature-comercial-module-ts #10
@ -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>
|
||||
|
||||
@ -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 }
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
2
src/modules/products/types/product.d.ts
vendored
2
src/modules/products/types/product.d.ts
vendored
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user