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 401 additions and 272 deletions
Showing only changes of commit 7bd247f0c5 - Show all commits

View File

@ -8,17 +8,13 @@ import Button from 'primevue/button';
import Card from 'primevue/card';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Dialog from 'primevue/dialog';
import InputText from 'primevue/inputtext';
import Dropdown from 'primevue/dropdown';
import InputSwitch from 'primevue/inputswitch';
import Tag from 'primevue/tag';
import Toast from 'primevue/toast';
import ConfirmDialog from 'primevue/confirmdialog';
import ProgressSpinner from 'primevue/progressspinner';
import { useUnitOfMeasureStore } from '../stores/unitOfMeasureStore';
import { unitTypesService } from '../services/unitsTypes';
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../types/unit-measure.interfaces';
import { useUnitOfMeasureStore } from '../../stores/unitOfMeasureStore';
import UnitsForm from './UnitsForm.vue';
import type { UnitOfMeasure, CreateUnitOfMeasureData } from '../../types/unit-measure.interfaces';
const router = useRouter();
const toast = useToast();
@ -39,27 +35,12 @@ const home = ref({
// State
const showDialog = ref(false);
const isEditing = ref(false);
const formData = ref<CreateUnitOfMeasureData>({
code: '',
name: '',
type: 4, // Default: Unidad
abbreviation: '',
is_active: 1
});
const editingId = ref<number | null>(null);
// Type options - loaded from API
const typeOptions = ref<{ label: string; value: number }[]>([]);
const loadingTypes = ref(false);
const selectedUnit = ref<UnitOfMeasure | null>(null);
// Computed
const units = computed(() => unitStore.units);
const loading = computed(() => unitStore.loading);
const dialogTitle = computed(() =>
isEditing.value ? 'Editar Unidad de Medida' : 'Nueva Unidad de Medida'
);
const getStatusConfig = (isActive: number) => {
return isActive === 1
? { label: 'Activa', severity: 'success' }
@ -69,47 +50,20 @@ const getStatusConfig = (isActive: number) => {
// Methods
const openCreateDialog = () => {
isEditing.value = false;
editingId.value = null;
formData.value = {
code: '',
name: '',
type: 4,
abbreviation: '',
is_active: 1
};
selectedUnit.value = null;
showDialog.value = true;
};
const openEditDialog = (unit: UnitOfMeasure) => {
isEditing.value = true;
editingId.value = unit.id;
formData.value = {
code: unit.code,
name: unit.name,
type: unit.type,
abbreviation: unit.abbreviation,
is_active: unit.is_active
};
selectedUnit.value = unit;
showDialog.value = true;
};
const closeDialog = () => {
showDialog.value = false;
formData.value = {
code: '',
name: '',
type: 4,
abbreviation: '',
is_active: 1
};
editingId.value = null;
isEditing.value = false;
};
const saveUnit = async () => {
const handleSaveUnit = async (data: CreateUnitOfMeasureData) => {
try {
if (isEditing.value && editingId.value) {
await unitStore.updateUnit(editingId.value, formData.value);
if (isEditing.value && selectedUnit.value) {
await unitStore.updateUnit(selectedUnit.value.id, data);
toast.add({
severity: 'success',
summary: 'Actualización Exitosa',
@ -117,7 +71,7 @@ const saveUnit = async () => {
life: 3000
});
} else {
await unitStore.createUnit(formData.value);
await unitStore.createUnit(data);
toast.add({
severity: 'success',
summary: 'Creación Exitosa',
@ -125,7 +79,7 @@ const saveUnit = async () => {
life: 3000
});
}
closeDialog();
showDialog.value = false;
} catch (error) {
console.error('Error saving unit:', error);
toast.add({
@ -169,35 +123,9 @@ const deleteUnit = async (id: number) => {
}
};
// Load unit types from API
const loadUnitTypes = async () => {
try {
loadingTypes.value = true;
const response = await unitTypesService.getUnitTypes();
typeOptions.value = response.data.unit_types.map(type => ({
label: type.name,
value: type.id
}));
console.log('Unit types loaded:', typeOptions.value);
} catch (error) {
console.error('Error loading unit types:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los tipos de unidades.',
life: 3000
});
} finally {
loadingTypes.value = false;
}
};
// Lifecycle
onMounted(async () => {
await Promise.all([
unitStore.fetchUnits(),
loadUnitTypes()
]);
await unitStore.fetchUnits();
});
</script>
@ -263,14 +191,6 @@ onMounted(async () => {
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} unidades"
>
<Column field="code" header="Código" sortable style="width: 120px">
<template #body="slotProps">
<span class="font-mono font-medium text-surface-700 dark:text-surface-300">
{{ slotProps.data.code }}
</span>
</template>
</Column>
<Column field="abbreviation" header="Abreviatura" sortable style="width: 150px">
<template #body="slotProps">
<span class="font-mono font-semibold text-primary">
@ -287,10 +207,18 @@ onMounted(async () => {
</template>
</Column>
<Column field="type_name" header="Tipo" sortable style="width: 150px">
<Column field="sat_unit.name" header="Unidad SAT" sortable style="width: 200px">
<template #body="slotProps">
<span class="text-sm text-surface-600 dark:text-surface-400">
{{ slotProps.data.type_name }}
{{ slotProps.data.sat_unit?.name || 'N/A' }}
</span>
</template>
</Column>
<Column field="sat_unit.code" header="Código SAT" sortable style="width: 120px">
<template #body="slotProps">
<span class="font-mono text-sm text-surface-600 dark:text-surface-400">
{{ slotProps.data.sat_unit?.code || 'N/A' }}
</span>
</template>
</Column>
@ -336,107 +264,12 @@ onMounted(async () => {
</template>
</Card>
<!-- Create/Edit Dialog -->
<Dialog
<!-- Create/Edit Form Dialog -->
<UnitsForm
v-model:visible="showDialog"
:header="dialogTitle"
:modal="true"
:closable="true"
:draggable="false"
class="w-full max-w-md"
>
<div class="space-y-4 pt-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: GS1, KG01"
:required="true"
/>
</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: PIEZA, KILOGRAMO, METRO"
:required="true"
/>
</div>
<!-- Abbreviation -->
<div>
<label for="abbreviation" class="block text-sm font-medium mb-2">
Abreviatura <span class="text-red-500">*</span>
</label>
<InputText
id="abbreviation"
v-model="formData.abbreviation"
class="w-full"
placeholder="Ej: PZA, kg, m"
:required="true"
/>
</div>
<!-- Type -->
<div>
<label for="type" class="block text-sm font-medium mb-2">
Tipo <span class="text-red-500">*</span>
</label>
<Dropdown
id="type"
v-model="formData.type"
:options="typeOptions"
optionLabel="label"
optionValue="value"
class="w-full"
placeholder="Selecciona un tipo"
/>
</div>
<!-- Status -->
<div>
<label for="is_active" class="block text-sm font-medium mb-2">
Estado
</label>
<div class="flex items-center gap-3">
<InputSwitch
id="is_active"
:model-value="formData.is_active === 1"
@update:model-value="formData.is_active = $event ? 1 : 0"
/>
<span class="text-sm font-medium">
{{ formData.is_active === 1 ? 'Activa' : 'Inactiva' }}
</span>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button
label="Cancelar"
severity="secondary"
outlined
@click="closeDialog"
/>
<Button
:label="isEditing ? 'Actualizar' : 'Crear'"
:disabled="!formData.code || !formData.name || !formData.abbreviation"
@click="saveUnit"
/>
</div>
</template>
</Dialog>
:unit="selectedUnit"
:is-editing="isEditing"
@save="handleSaveUnit"
/>
</div>
</template>

View File

@ -0,0 +1,261 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Dropdown from 'primevue/dropdown';
import InputSwitch from 'primevue/inputswitch';
import { satUnitsService } from '../../services/sat-units.services';
import type { UnitOfMeasure, CreateUnitOfMeasureData, SatUnit } from '../../types/unit-measure.interfaces';
interface Props {
visible: boolean;
unit?: UnitOfMeasure | null;
isEditing?: boolean;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'save', data: CreateUnitOfMeasureData): void;
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
unit: null,
isEditing: false
});
const emit = defineEmits<Emits>();
// Form data
const formData = ref<CreateUnitOfMeasureData>({
name: '',
abbreviation: '',
code_sat: 1,
is_active: 1
});
// Estado interno del switch (boolean para el UI)
const isActiveSwitch = ref(true);
// SAT Units
const satUnits = ref<SatUnit[]>([]);
const loadingSatUnits = ref(false);
const searchQuery = ref('');
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
// Options para el dropdown
const satUnitOptions = computed(() =>
satUnits.value.map(unit => ({
label: `${unit.code} - ${unit.name}`,
value: unit.id
}))
);
// Computed
const dialogTitle = computed(() =>
props.isEditing ? 'Editar Unidad de Medida' : 'Nueva Unidad de Medida'
);
const isFormValid = computed(() => {
return !!(formData.value.name && formData.value.abbreviation);
});
const emptyMessage = computed(() => {
if (props.isEditing && satUnits.value.length === 1) {
return 'Escribe para buscar otra unidad SAT...';
}
return 'No se encontraron unidades. Escribe para buscar.';
});
// Load SAT Units
const loadSatUnits = async (search: string) => {
// Solo buscar si hay texto
if (!search || search.trim().length === 0) {
// Si estamos editando y hay una unidad actual, mantenerla
if (props.unit?.sat_unit) {
satUnits.value = [props.unit.sat_unit];
} else {
satUnits.value = [];
}
return;
}
try {
loadingSatUnits.value = true;
const response = await satUnitsService.getSatUnits(search);
satUnits.value = response.data;
} catch (error) {
console.error('Error loading SAT units:', error);
// Si hay error y estamos editando, mantener la unidad actual
if (props.unit?.sat_unit) {
satUnits.value = [props.unit.sat_unit];
} else {
satUnits.value = [];
}
} finally {
loadingSatUnits.value = false;
}
};
// Debounced search
const handleSearchChange = (event: any) => {
const query = event.value || '';
searchQuery.value = query;
// Limpiar timeout anterior
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Aplicar debounce de 500ms
searchTimeout = setTimeout(() => {
loadSatUnits(query);
}, 500);
};
// Methods
const resetForm = () => {
formData.value = {
name: '',
abbreviation: '',
code_sat: null,
is_active: 1
};
isActiveSwitch.value = true;
};
// Watch para actualizar el formulario cuando cambie la unidad
watch(() => props.unit, (newUnit) => {
if (newUnit) {
formData.value = {
name: newUnit.name,
abbreviation: newUnit.abbreviation,
code_sat: newUnit.code_sat,
is_active: newUnit.is_active
};
isActiveSwitch.value = newUnit.is_active === 1;
// Precargar la unidad SAT actual en el dropdown
if (newUnit.sat_unit) {
satUnits.value = [newUnit.sat_unit];
}
} else {
resetForm();
satUnits.value = []; // Limpiar el dropdown
}
}, { immediate: true });
const handleClose = () => {
emit('update:visible', false);
satUnits.value = []; // Limpiar las opciones del dropdown
resetForm();
};
const handleSave = () => {
// Convertir el switch boolean a number para el backend
formData.value.is_active = isActiveSwitch.value ? 1 : 0;
emit('save', { ...formData.value });
handleClose();
};
</script>
<template>
<Dialog
:visible="visible"
@update:visible="emit('update:visible', $event)"
:header="dialogTitle"
:modal="true"
:closable="true"
:draggable="false"
class="w-full max-w-md"
>
<div class="space-y-4 pt-4">
<!-- 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: PIEZA, KILOGRAMO, METRO"
:required="true"
/>
</div>
<!-- Abbreviation -->
<div>
<label for="abbreviation" class="block text-sm font-medium mb-2">
Abreviatura <span class="text-red-500">*</span>
</label>
<InputText
id="abbreviation"
v-model="formData.abbreviation"
class="w-full"
placeholder="Ej: PZA, kg, m"
:required="true"
/>
</div>
<!-- Code SAT -->
<div>
<label for="code_sat" class="block text-sm font-medium mb-2">
Unidad SAT <span class="text-red-500">*</span>
</label>
<Dropdown
id="code_sat"
v-model="formData.code_sat"
:options="satUnitOptions"
optionLabel="label"
optionValue="value"
:loading="loadingSatUnits"
placeholder="Escribe para buscar una unidad SAT..."
class="w-full"
:filter="true"
filterPlaceholder="Buscar unidad SAT"
:showClear="false"
@filter="handleSearchChange"
:emptyFilterMessage="emptyMessage"
/>
<small class="text-surface-500 dark:text-surface-400">
Unidad del catálogo SAT
</small>
</div>
<!-- Status -->
<div>
<label for="is_active" class="block text-sm font-medium mb-2">
Estado
</label>
<div class="flex items-center gap-3">
<InputSwitch
id="is_active"
v-model="isActiveSwitch"
/>
<span class="text-sm font-medium">
{{ isActiveSwitch ? 'Activa' : 'Inactiva' }}
</span>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<Button
label="Cancelar"
severity="secondary"
outlined
@click="handleClose"
/>
<Button
:label="isEditing ? 'Actualizar' : 'Crear'"
:disabled="!isFormValid"
@click="handleSave"
/>
</div>
</template>
</Dialog>
</template>

View File

@ -0,0 +1,17 @@
import api from '../../../services/api';
import type { SatUnit } from '../types/unit-measure.interfaces';
// Respuesta del endpoint de unidades SAT
export interface SatUnitsResponse {
data: SatUnit[];
}
export const satUnitsService = {
/**
* Get all SAT units with search filter
*/
async getSatUnits(search: string = ''): Promise<SatUnitsResponse> {
const response = await api.get(`/api/sat/units?search=${search}`);
return response.data;
}
};

View File

@ -1,28 +1,27 @@
import api from '../../../services/api';
import type {
UnitOfMeasureResponse,
CreateUnitOfMeasureData,
UnitOfMeasurePaginatedResponse,
UnitOfMeasureUnpaginatedResponse,
CreateUnitOfMeasureData,
UpdateUnitOfMeasureData,
SingleUnitOfMeasureResponse
SingleUnitOfMeasureResponse,
UnitOfMeasureResponseById
} from '../types/unit-measure.interfaces';
export const unitOfMeasureService = {
/**
* Get all units of measure with pagination
* Get all units of measure with optional pagination and search
*/
async getUnits(page = 1, perPage = 10): Promise<UnitOfMeasureResponse> {
const response = await api.get(`/api/catalogs/units-of-measure`, {
params: { page, per_page: perPage }
});
console.log('Units of Measure response:', response);
async getUnits(paginate: boolean = true, search: string = ''): Promise<UnitOfMeasurePaginatedResponse | UnitOfMeasureUnpaginatedResponse> {
const response = await api.get(`/api/catalogs/units-of-measure?paginate=${paginate}&search=${search}`);
return response.data;
},
/**
* Get a single unit of measure by ID
*/
async getUnitById(id: number): Promise<SingleUnitOfMeasureResponse> {
async getUnitById(id: number): Promise<UnitOfMeasureResponseById> {
const response = await api.get(`/api/catalogs/units-of-measure/${id}`);
return response.data;
},

View File

@ -1,7 +1,11 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { unitOfMeasureService } from '../services/unit-measure.services';
import type { UnitOfMeasure, CreateUnitOfMeasureData, UpdateUnitOfMeasureData } from '../types/unit-measure.interfaces';
import type {
UnitOfMeasure,
CreateUnitOfMeasureData,
UpdateUnitOfMeasureData
} from '../types/unit-measure.interfaces';
export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
// State
@ -31,7 +35,7 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
});
// Actions
const fetchUnits = async (force = false) => {
const fetchUnits = async (force = false, paginate = true, search = '') => {
// Si ya están cargados y no se fuerza la recarga, no hacer nada
if (loaded.value && !force) {
console.log('Units of measure already loaded from store');
@ -42,8 +46,17 @@ export const useUnitOfMeasureStore = defineStore('unitOfMeasure', () => {
loading.value = true;
error.value = null;
const response = await unitOfMeasureService.getUnits();
units.value = response.data.units_of_measure.data;
const response = await unitOfMeasureService.getUnits(paginate, search);
// Manejar respuesta paginada o no paginada
if ('current_page' in response) {
// Respuesta paginada
units.value = response.data;
} else {
// Respuesta no paginada
units.value = response.data;
}
loaded.value = true;
console.log('Units of measure loaded into store:', units.value.length);

View File

@ -1,75 +1,81 @@
/**
* Unit of Measure Type Definitions
*/
export interface UnitOfMeasure {
id: number;
code: string;
name: string;
type: number;
type_name: string;
abbreviation: string;
is_active: number; // API returns 0 or 1
created_at: string;
updated_at: string;
deleted_at: string | null;
// Interface para la unidad SAT
export interface SatUnit {
id: number;
code: string;
name: string;
symbol: string;
created_at: string;
updated_at: string;
}
// Interface para la Unidad de Medida
export interface UnitOfMeasure {
id: number;
name: string;
abbreviation: string;
is_active: number;
created_at: string;
updated_at: string;
deleted_at: string | null;
code_sat: number;
sat_unit: SatUnit;
}
// Interfaces para crear y actualizar unidades de medida
export interface CreateUnitOfMeasureData {
code: string;
name: string;
type: number;
abbreviation: string;
is_active?: number;
name: string;
abbreviation: string;
code_sat: number | null;
is_active: number;
}
export interface UpdateUnitOfMeasureData {
code?: string;
name?: string;
type?: number;
abbreviation?: string;
is_active?: number;
name?: string;
abbreviation?: string;
code_sat?: number | null;
is_active?: number;
}
export interface UnitOfMeasurePagination {
current_page: number;
last_page: number;
per_page: number;
total: number;
from: number;
to: number;
first_page_url: string;
last_page_url: string;
next_page_url: string | null;
prev_page_url: string | null;
path: string;
// Interface para los links de paginación
export interface PaginationLink {
url: string | null;
label: string;
active: boolean;
}
export interface UnitOfMeasureData {
current_page: number;
data: UnitOfMeasure[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
// Interface genérica para respuestas paginadas
export interface PaginatedResponse<T> {
current_page: number;
data: T[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
links: PaginationLink[];
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
}
export interface UnitOfMeasureResponse {
status: string;
data: {
units_of_measure: UnitOfMeasureData;
};
// Interface genérica para respuestas sin paginación
export interface UnpaginatedResponse<T> {
data: T[];
}
// Tipo específico para la respuesta paginada de Unidades de Medida
export type UnitOfMeasurePaginatedResponse = PaginatedResponse<UnitOfMeasure>;
// Tipo específico para la respuesta no paginada de Unidades de Medida
export type UnitOfMeasureUnpaginatedResponse = UnpaginatedResponse<UnitOfMeasure>;
export type UnitOfMeasureResponseById = {
data: UnitOfMeasure;
};
export interface SingleUnitOfMeasureResponse {
status: string;
data: {
unit_of_measure: UnitOfMeasure;
};
}
message: string;
data: UnitOfMeasure;
}

View File

@ -8,7 +8,7 @@ import WarehouseIndex from '../modules/warehouse/components/WarehouseIndex.vue';
import WarehouseForm from '../modules/warehouse/components/WarehouseForm.vue';
import WarehouseDetails from '../modules/warehouse/components/WarehouseDetails.vue';
import WarehouseClassification from '../modules/warehouse/components/WarehouseClassification.vue';
import UnitOfMeasure from '../modules/catalog/components/UnitOfMeasure.vue';
import Units from '../modules/catalog/components/units/Units.vue';
import ComercialClassification from '../modules/catalog/components/ComercialClassification.vue';
import ProductsIndex from '../modules/products/components/ProductsIndex.vue';
import ProductForm from '../modules/products/components/ProductForm.vue';
@ -141,7 +141,7 @@ const routes: RouteRecordRaw[] = [
{
path: 'units-of-measure',
name: 'UnitsOfMeasure',
component: UnitOfMeasure,
component: Units,
meta: {
title: 'Unidades de Medida',
requiresAuth: true