feat: agregar manejo de números de serie en movimientos y actualización de PDF
This commit is contained in:
parent
898643cdab
commit
156c915403
@ -78,7 +78,25 @@ const handleClose = () => {
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
emit('edit', movement.value);
|
||||
// Si es un movimiento múltiple, usar el valor actual
|
||||
if (isMultiProduct.value) {
|
||||
emit('edit', movement.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Para movimientos individuales, hacer fetch para obtener datos completos (incluidos seriales)
|
||||
loading.value = true;
|
||||
api.get(apiURL(`movimientos/${movement.value.id}`), {
|
||||
onSuccess: (data) => {
|
||||
emit('edit', data.movement || data);
|
||||
},
|
||||
onError: () => {
|
||||
window.Notify.error('Error al cargar el movimiento');
|
||||
},
|
||||
onFinish: () => {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditProduct = (product) => {
|
||||
|
||||
@ -32,9 +32,13 @@ const form = useForm({
|
||||
origin_warehouse_id: '',
|
||||
destination_warehouse_id: '',
|
||||
invoice_reference: '',
|
||||
notes: ''
|
||||
notes: '',
|
||||
serial_numbers: [] // Array de números de serie
|
||||
});
|
||||
|
||||
/** Estado para manejo de seriales */
|
||||
const serialsText = ref(''); // Texto editable (uno por línea)
|
||||
|
||||
/** Computed */
|
||||
const movementTypeInfo = computed(() => {
|
||||
const types = {
|
||||
@ -71,6 +75,43 @@ const allowsDecimals = computed(() => {
|
||||
return props.movement?.inventory?.unit_of_measure?.allows_decimals || false;
|
||||
});
|
||||
|
||||
const hasSerials = computed(() => {
|
||||
return props.movement?.inventory?.track_serials || false;
|
||||
});
|
||||
|
||||
const serialsArray = computed(() => {
|
||||
return serialsText.value
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
});
|
||||
|
||||
const serialsValidation = computed(() => {
|
||||
if (!hasSerials.value) return { valid: true, message: '' };
|
||||
|
||||
const quantity = Number(form.quantity);
|
||||
const serialCount = serialsArray.value.length;
|
||||
|
||||
if (quantity > 0 && serialCount === 0) {
|
||||
return { valid: false, message: 'Debe ingresar números de serie' };
|
||||
}
|
||||
|
||||
if (serialCount !== quantity) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Debe ingresar ${quantity} número(s) de serie (actual: ${serialCount})`
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar duplicados
|
||||
const uniqueSerials = new Set(serialsArray.value);
|
||||
if (uniqueSerials.size !== serialCount) {
|
||||
return { valid: false, message: 'Hay números de serie duplicados' };
|
||||
}
|
||||
|
||||
return { valid: true, message: '' };
|
||||
});
|
||||
|
||||
/** Métodos */
|
||||
const loadWarehouses = () => {
|
||||
loading.value = true;
|
||||
@ -91,12 +132,28 @@ const loadWarehouses = () => {
|
||||
};
|
||||
|
||||
const updateMovement = () => {
|
||||
// Validar seriales si el producto los requiere
|
||||
if (hasSerials.value && !serialsValidation.value.valid) {
|
||||
window.Notify.warning(serialsValidation.value.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preparar datos según el tipo de movimiento
|
||||
const data = {
|
||||
quantity: Number(form.quantity), // Común para todos los tipos
|
||||
notes: form.notes || null
|
||||
};
|
||||
|
||||
// Siempre enviar serial_numbers si el producto requiere seriales
|
||||
if (hasSerials.value) {
|
||||
// Validar que haya seriales
|
||||
if (serialsArray.value.length === 0) {
|
||||
window.Notify.warning('Debe ingresar números de serie para este producto');
|
||||
return;
|
||||
}
|
||||
data.serial_numbers = serialsArray.value;
|
||||
}
|
||||
|
||||
// Campos específicos por tipo
|
||||
if (props.movement.movement_type === 'entry') {
|
||||
data.unit_cost = Number(form.unit_cost);
|
||||
@ -110,9 +167,19 @@ const updateMovement = () => {
|
||||
data.warehouse_to_id = form.destination_warehouse_id;
|
||||
}
|
||||
|
||||
console.log('📤 Datos a enviar al backend:', {
|
||||
movement_id: props.movement.id,
|
||||
data: data,
|
||||
serial_numbers_count: data.serial_numbers?.length || 0,
|
||||
hasSerials: hasSerials.value
|
||||
});
|
||||
|
||||
api.put(apiURL(`movimientos/${props.movement.id}`), {
|
||||
data,
|
||||
onSuccess: () => {
|
||||
onSuccess: (response) => {
|
||||
console.log('✅ Respuesta del backend (completa):', JSON.stringify(response, null, 2));
|
||||
console.log('✅ Tipo de respuesta:', typeof response);
|
||||
console.log('✅ Keys:', Object.keys(response || {}));
|
||||
window.Notify.success('Movimiento actualizado correctamente');
|
||||
emit('updated');
|
||||
closeModal();
|
||||
@ -144,6 +211,21 @@ watch(() => props.show, (isShown) => {
|
||||
form.invoice_reference = props.movement.invoice_reference || '';
|
||||
form.notes = props.movement.notes || '';
|
||||
|
||||
// Cargar números de serie si existen
|
||||
let serialNumbers = [];
|
||||
|
||||
if (props.movement.serial_numbers && props.movement.serial_numbers.length > 0) {
|
||||
serialNumbers = props.movement.serial_numbers;
|
||||
} else if (props.movement.serials && props.movement.serials.length > 0) {
|
||||
serialNumbers = props.movement.serials.map(s => s.serial_number);
|
||||
}
|
||||
|
||||
if (serialNumbers.length > 0) {
|
||||
serialsText.value = serialNumbers.join('\n');
|
||||
} else {
|
||||
serialsText.value = '';
|
||||
}
|
||||
|
||||
// Almacenes según tipo
|
||||
if (props.movement.movement_type === 'entry') {
|
||||
form.destination_warehouse_id = props.movement.warehouse_id || '';
|
||||
@ -153,7 +235,19 @@ watch(() => props.show, (isShown) => {
|
||||
form.origin_warehouse_id = props.movement.origin_warehouse_id || '';
|
||||
form.destination_warehouse_id = props.movement.destination_warehouse_id || '';
|
||||
}
|
||||
console.log('🔍 Debug Movement:', {
|
||||
movement: props.movement,
|
||||
has_inventory: !!props.movement.inventory,
|
||||
track_serials: props.movement?.inventory?.track_serials,
|
||||
serials_relation: props.movement.serials,
|
||||
hasSerials_computed: hasSerials.value,
|
||||
serialNumbers_loaded: serialNumbers,
|
||||
serialsText_final: serialsText.value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Limpiar seriales al cerrar
|
||||
serialsText.value = '';
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
@ -194,6 +288,13 @@ watch(() => props.show, (isShown) => {
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<GoogleIcon name="inventory_2" class="text-gray-500 dark:text-gray-400 text-sm" />
|
||||
<p class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Producto</p>
|
||||
<span
|
||||
v-if="hasSerials"
|
||||
class="ml-auto px-2 py-0.5 text-xs font-semibold bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-full flex items-center gap-1"
|
||||
>
|
||||
<GoogleIcon name="qr_code_scanner" class="text-xs" />
|
||||
Con seriales
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
{{ movement?.inventory?.name || 'N/A' }}
|
||||
@ -222,6 +323,48 @@ watch(() => props.show, (isShown) => {
|
||||
<FormError :message="form.errors?.quantity" />
|
||||
</div>
|
||||
|
||||
<!-- Números de Serie (solo si el producto los tiene) -->
|
||||
<div v-if="hasSerials" class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<GoogleIcon name="qr_code_scanner" class="text-amber-600 dark:text-amber-400" />
|
||||
<label class="text-xs font-semibold text-amber-700 dark:text-amber-300 uppercase">
|
||||
NÚMEROS DE SERIE
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mb-2">
|
||||
Ingresa un número de serie por línea. Debe coincidir con la cantidad ({{ form.quantity }}).
|
||||
</p>
|
||||
<textarea
|
||||
v-model="serialsText"
|
||||
rows="5"
|
||||
placeholder="SN001 SN002 SN003"
|
||||
class="w-full px-3 py-2 border border-amber-300 dark:border-amber-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:ring-2 focus:ring-amber-500 focus:border-amber-500 resize-none"
|
||||
:class="{
|
||||
'border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500': !serialsValidation.valid && serialsText.length > 0
|
||||
}"
|
||||
></textarea>
|
||||
|
||||
<!-- Contador y validación -->
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400">
|
||||
<span class="font-semibold">{{ serialsArray.length }}</span> de {{ form.quantity }} seriales
|
||||
</p>
|
||||
<p
|
||||
v-if="!serialsValidation.valid && serialsText.length > 0"
|
||||
class="text-xs text-red-600 dark:text-red-400 font-medium"
|
||||
>
|
||||
{{ serialsValidation.message }}
|
||||
</p>
|
||||
<p
|
||||
v-else-if="serialsValidation.valid && serialsArray.length > 0"
|
||||
class="text-xs text-green-600 dark:text-green-400 font-medium flex items-center gap-1"
|
||||
>
|
||||
<GoogleIcon name="check_circle" class="text-sm" />
|
||||
Válido
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Costo unitario (solo para entradas) -->
|
||||
<div v-if="movement?.movement_type === 'entry'">
|
||||
<label class="block text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase mb-1.5">
|
||||
|
||||
@ -275,6 +275,35 @@ const TicketDetailMovement = {
|
||||
} else {
|
||||
yPosition += 1;
|
||||
}
|
||||
|
||||
// Números de serie (si existen)
|
||||
const serialNumbers = movementData.serial_numbers ||
|
||||
(movementData.serials ? movementData.serials.map(s => s.serial_number) : []);
|
||||
|
||||
if (serialNumbers && serialNumbers.length > 0) {
|
||||
yPosition += 2;
|
||||
doc.setLineWidth(0.2);
|
||||
doc.setDrawColor(...darkGrayColor);
|
||||
doc.line(leftMargin, yPosition, rightMargin, yPosition);
|
||||
yPosition += 5;
|
||||
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...[0, 0, 0]); // amber
|
||||
doc.text('NÚMEROS DE SERIE:', leftMargin, yPosition);
|
||||
yPosition += 4;
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('courier', 'normal');
|
||||
doc.setTextColor(...blackColor);
|
||||
|
||||
serialNumbers.forEach((serial, index) => {
|
||||
doc.text(`${index + 1}. ${serial}`, leftMargin + 2, yPosition);
|
||||
yPosition += 3.5;
|
||||
});
|
||||
|
||||
yPosition += 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Notas (si existen)
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { apiURL, token } from '@Services/Api';
|
||||
import { page } from '@Services/Page';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
@ -38,34 +37,6 @@ const whatsappService = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enviar mensaje genérico por WhatsApp
|
||||
* @param {Object} data - Datos del mensaje
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async sendMessage({ phone_number, message }) {
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
apiURL('whatsapp/send-message'),
|
||||
{
|
||||
phone_number,
|
||||
message
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token.value}`
|
||||
}
|
||||
}
|
||||
);
|
||||
return data;
|
||||
} catch (error) {
|
||||
const errorData = error.response?.data?.data || error.response?.data;
|
||||
console.error('WhatsApp Error Detail:', errorData);
|
||||
throw errorData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default whatsappService;
|
||||
Loading…
x
Reference in New Issue
Block a user