ADD: modificación modulo concepto, multa y descuento

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-20 16:03:37 -06:00
parent 3029d425fe
commit bd6edc59d6
7 changed files with 645 additions and 271 deletions

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, ref } from "vue";
import { onMounted, ref, computed } from "vue";
import { useForm, api, apiURL } from "@Services/Api";
import Input from "@Holos/Form/Input.vue";
@ -23,11 +23,41 @@ const form = useForm({
max_amount_peso: "",
unit_cost_uma: "",
unit_cost_peso: "",
charge_type: null,
});
const addresses = ref([]);
const units = ref([]);
const chargeTypeId = computed(() => {
return typeof form.charge_type === 'object' && form.charge_type?.id
? form.charge_type.id
: form.charge_type;
});
const chargeTypesOptions = ref([
{ id: 'uma_range', name: 'UMA Range' },
{ id: 'peso_range', name: 'Peso Range' },
{ id: 'uma_fixed', name: 'UMA unitario' },
{ id: 'peso_fixed', name: 'Peso unitario' },
]);
const showUmaFields = computed(() => {
return chargeTypeId.value === 'uma_range' || chargeTypeId.value === 'uma_fixed';
});
const showPesoFields = computed(() => {
return chargeTypeId.value === 'peso_range' || chargeTypeId.value === 'peso_fixed';
});
const isRangeType = computed(() => {
return chargeTypeId.value === 'uma_range' || chargeTypeId.value === 'peso_range';
});
const isFixedType = computed(() => {
return chargeTypeId.value === 'uma_fixed' || chargeTypeId.value === 'peso_fixed';
});
/** Cargar cosas */
onMounted(async () => {
api.get(apiURL('directions'), {
@ -45,7 +75,7 @@ onMounted(async () => {
/** Métodos */
const handleSubmit = () => {
const transformedData = {
const baseData = {
direction_id: typeof form.direction === 'object' ? form.direction.id : Number(form.direction_id),
unit_id: form.unit ? (typeof form.unit === 'object' ? form.unit.id : Number(form.unit)) : null,
short_name: form.short_name,
@ -53,15 +83,29 @@ const handleSubmit = () => {
legal_instrument: form.legal_instrument,
article: form.article,
content: form.content,
min_amount_uma: form.min_amount_uma ? Number(form.min_amount_uma) : null,
max_amount_uma: form.max_amount_uma ? Number(form.max_amount_uma) : null,
min_amount_peso: form.min_amount_peso ? Number(form.min_amount_peso) : null,
max_amount_peso: form.max_amount_peso ? Number(form.max_amount_peso) : null,
unit_cost_uma: form.unit_cost_uma ? Number(form.unit_cost_uma) : null,
unit_cost_peso: form.unit_cost_peso ? Number(form.unit_cost_peso) : null,
charge_type: chargeTypeId.value,
};
emit('concept-created', transformedData);
// Agregar campos condicionales solo si tienen valor
if (showUmaFields.value && isRangeType.value) {
if (form.min_amount_uma) baseData.min_amount_uma = Number(form.min_amount_uma);
if (form.max_amount_uma) baseData.max_amount_uma = Number(form.max_amount_uma);
}
if (showPesoFields.value && isRangeType.value) {
if (form.min_amount_peso) baseData.min_amount_peso = Number(form.min_amount_peso);
if (form.max_amount_peso) baseData.max_amount_peso = Number(form.max_amount_peso);
}
if (showUmaFields.value && isFixedType.value && form.unit_cost_uma) {
baseData.unit_cost_uma = Number(form.unit_cost_uma);
}
if (showPesoFields.value && isFixedType.value && form.unit_cost_peso) {
baseData.unit_cost_peso = Number(form.unit_cost_peso);
}
emit('concept-created', baseData);
};
</script>
@ -123,7 +167,18 @@ const handleSubmit = () => {
:onError="form.errors.content"
required
/>
<div class="mb-5 grid grid-cols-2 gap-4 py-2">
<div class="mb-5 py-2">
<Selectable
v-model="form.charge_type"
label="name"
value="id"
:title="$t('concept.chargeType')"
:options="chargeTypesOptions"
required
/>
</div>
<div v-if="showUmaFields && isRangeType" class="mb-5 grid grid-cols-2 gap-4 py-2">
<Input
v-model="form.min_amount_uma"
:id="$t('concept.minimumAmountUma')"
@ -139,7 +194,7 @@ const handleSubmit = () => {
:onError="form.errors.max_amount_uma"
/>
</div>
<div class="mb-5 grid grid-cols-2 gap-4 py-2">
<div v-if="showPesoFields && isRangeType" class="mb-5 grid grid-cols-2 gap-4 py-2">
<Input
v-model="form.min_amount_peso"
:id="$t('concept.minimumAmountPeso')"
@ -155,32 +210,37 @@ const handleSubmit = () => {
:onError="form.errors.max_amount_peso"
/>
</div>
<hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-4 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<div class="mb-5 grid grid-cols-3 gap-4 py-2">
<Selectable
v-model="form.unit"
label="name"
value="id"
:title="$t('concept.sizeUnit')"
:options="units"
/>
<Input
v-model="form.unit_cost_uma"
:id="$t('concept.costUnitUma')"
type="number"
step="0.01"
:onError="form.errors.unit_cost_uma"
/>
<Input
v-model="form.unit_cost_peso"
:id="$t('concept.costUnitPeso')"
type="number"
step="0.01"
:onError="form.errors.unit_cost_peso"
/>
<Selectable
v-model="form.unit"
label="name"
value="id"
:title="$t('concept.sizeUnit')"
:options="units"
required
/>
<div v-if="isFixedType" >
<hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-4 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<div class="mb-5 grid grid-cols-3 gap-4 py-2">
<Input
v-if="showUmaFields"
v-model="form.unit_cost_uma"
:id="$t('concept.costUnitUma')"
type="number"
step="0.01"
:onError="form.errors.unit_cost_uma"
/>
<Input
v-if="showPesoFields"
v-model="form.unit_cost_peso"
:id="$t('concept.costUnitPeso')"
type="number"
step="0.01"
:onError="form.errors.unit_cost_peso"
/>
</div>
</div>
<div class="mb-3 p-7 flex justify-center">
<button

View File

@ -1,18 +1,21 @@
<script setup>
import { ref } from "vue";
import { api, apiURL } from "@Services/Api";
import Input from "@Holos/Form/Input.vue";
/** Eventos */
const emit = defineEmits(['discount-authorized']);
const emit = defineEmits(["discount-authorized"]);
/** Refs */
const searchForm = ref({
fineNumber: "",
placa: "",
curp: ""
curp: "",
});
const discountData = ref({
id: null,
fecha: "",
placa: "",
vin: "",
@ -22,41 +25,219 @@ const discountData = ref({
nombre: "",
montoOriginal: "",
pagoMinimo: "",
nuevoMonto: ""
nuevoMonto: "",
descuentoAplicado: "",
// Solo los valores numéricos necesarios para cálculos
total_amount: 0,
min_total: 0,
discount: 0,
isPaid: false,
hasDiscount: false,
});
/** Métodos */
const handleSearch = () => {
if (!searchForm.value.fineNumber && !searchForm.value.placa && !searchForm.value.curp) {
alert("Por favor ingresa al menos un criterio de búsqueda");
const handleSearch = async () => {
if (
!searchForm.value.fineNumber &&
!searchForm.value.placa &&
!searchForm.value.curp
) {
window.Notify.warning("Por favor ingresa al menos un criterio de búsqueda");
return;
}
console.log("Buscando multa:", searchForm.value);
await api.get(apiURL(`fines/search`), {
params: {
qr_token: searchForm.value.fineNumber.trim() || undefined,
plate: searchForm.value.placa.trim() || undefined,
curp: searchForm.value.curp.trim() || undefined,
},
onSuccess: (data) => {
const model = data.model;
const hasDiscount = model.discount && parseFloat(model.discount) > 0;
const isPaid =
model.payments &&
model.payments.length > 0 &&
model.payments[0].status === "paid";
// Simular datos encontrados (temporal)
// Verificar si ya está pagada
if (isPaid) {
window.Notify.info("La multa ya ha sido pagada previamente");
discountData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
placa: model.plate || "",
vin: model.vin || "",
licencia: model.license || "",
tarjeta: model.circulation || "",
rfc: model.rfc || "",
nombre: model.name || "",
montoOriginal: `$${data.total_amount.toFixed(2)}`,
pagoMinimo: `$${data.min_total.toFixed(2)}`,
nuevoMonto: "",
total_amount: data.total_amount,
min_total: data.min_total,
discount: parseFloat(model.discount) || 0,
isPaid: true,
hasDiscount: hasDiscount,
};
return;
}
if (hasDiscount) {
window.Notify.info(
"La multa ya tiene un descuento aplicado previamente"
);
// Multa pendiente
discountData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
placa: model.plate || "",
vin: model.vin || "",
licencia: model.license || "",
tarjeta: model.circulation || "",
rfc: model.rfc || "",
nombre: model.name || "",
montoOriginal: `$${data.total_amount.toFixed(2)}`,
pagoMinimo: `$${data.min_total.toFixed(2)}`,
descuentoAplicado: `$${parseFloat(model.discount).toFixed(2)}`,
nuevoMonto: data.total_to_pay.toFixed(2),
total_amount: data.total_amount,
min_total: data.min_total,
discount: parseFloat(model.discount),
isPaid: false,
hasDiscount: true,
};
return;
}
discountData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
placa: model.plate || "",
vin: model.vin || "",
licencia: model.license || "",
tarjeta: model.circulation || "",
rfc: model.rfc || "",
nombre: model.name || "",
montoOriginal: `$${data.total_amount.toFixed(2)}`,
pagoMinimo: `$${data.min_total.toFixed(2)}`,
nuevoMonto: data.min_total.toFixed(2),
total_amount: data.total_amount,
min_total: data.min_total,
discount: 0,
isPaid: false,
hasDiscount: false,
};
window.Notify.success("Multa encontrada");
},
onFail: (error) => {
window.Notify.error(error.message || "Multa no encontrada");
clearDiscountData();
},
onError: (error) => {
console.error("Error al buscar multa:", error);
window.Notify.error("Ocurrió un error al buscar la multa");
clearDiscountData();
},
});
};
const handleAuthorize = async () => {
if (!discountData.value.id) {
window.Notify.warning("No hay multa cargada para autorizar");
return;
}
if (discountData.value.hasDiscount) {
window.Notify.error("Esta multa ya tiene un descuento autorizado");
return;
}
if (!discountData.value.nuevoMonto) {
window.Notify.warning("Por favor ingresa el nuevo monto autorizado");
return;
}
const nuevoMontoNumber = parseFloat(discountData.value.nuevoMonto);
if (nuevoMontoNumber < discountData.value.min_total) {
window.Notify.error(
`El monto no puede ser menor al pago mínimo de $${discountData.value.min_total.toFixed(
2
)}`
);
return;
}
if (nuevoMontoNumber > discountData.value.total_amount) {
window.Notify.error(
`El monto no puede ser mayor al original de $${discountData.value.total_amount.toFixed(
2
)}`
);
return;
}
const descuento = discountData.value.total_amount - nuevoMontoNumber;
await api.put(apiURL(`fines/${discountData.value.id}/discount`), {
data: {
discount: descuento,
},
onSuccess: (response) => {
window.Notify.success("Descuento autorizado exitosamente");
emit("discount-authorized", {
fine_id: discountData.value.id,
authorized_amount: nuevoMontoNumber,
discount: descuento,
response: response,
});
clearDiscountData();
clearSearchForm();
},
onFail: (error) => {
window.Notify.error(error.message || "Error al autorizar el descuento");
},
onError: (error) => {
console.error("Error al autorizar descuento:", error);
window.Notify.error("Ocurrió un error al autorizar el descuento");
},
});
};
const clearDiscountData = () => {
discountData.value = {
fecha: "2025-11-11",
placa: "ABC-123-DEF",
vin: "1HGBH41JXMN109186",
licencia: "LIC123456",
tarjeta: "TC987654",
rfc: "XAXX010101000",
nombre: "Juan Pérez García",
montoOriginal: "$1,500.00",
pagoMinimo: "$750.00",
nuevoMonto: ""
id: null,
fecha: "",
placa: "",
vin: "",
licencia: "",
tarjeta: "",
rfc: "",
nombre: "",
montoOriginal: "",
pagoMinimo: "",
descuentoAplicado: "",
nuevoMonto: "",
total_amount: 0,
min_total: 0,
discount: 0,
isPaid: false,
hasDiscount: false,
};
};
const handleAuthorize = () => {
if (!discountData.value.nuevoMonto) {
alert("Por favor ingresa el nuevo monto autorizado");
return;
}
console.log("Autorizando descuento:", discountData.value);
emit('discount-authorized', discountData.value);
const clearSearchForm = () => {
searchForm.value = {
fineNumber: "",
placa: "",
curp: "",
};
};
</script>
@ -70,31 +251,16 @@ const handleAuthorize = () => {
<form @submit.prevent="handleSearch">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
<!-- Número de Multa -->
<Input
v-model="searchForm.fineNumber"
id="Número de Multa"
type="text"
placeholder=""
/>
<!-- Placa -->
<Input
v-model="searchForm.placa"
id="Placa"
type="text"
placeholder=""
/>
<Input v-model="searchForm.placa" id="Placa" type="text" />
<!-- CURP -->
<Input
v-model="searchForm.curp"
id="CURP"
type="text"
placeholder=""
/>
<Input v-model="searchForm.curp" id="CURP" type="text" />
<!-- Botón Buscar -->
<button
type="submit"
class="py-3 px-8 rounded-lg transition-colors h-[42px] bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium"
@ -112,7 +278,6 @@ const handleAuthorize = () => {
</h3>
<form @submit.prevent="handleAuthorize">
<!-- Fecha y Placa -->
<div class="grid grid-cols-2 gap-4 mb-5">
<Input
v-model="discountData.fecha"
@ -120,22 +285,11 @@ const handleAuthorize = () => {
type="text"
disabled
/>
<Input
v-model="discountData.placa"
id="Placa"
type="text"
disabled
/>
<Input v-model="discountData.placa" id="Placa" type="text" disabled />
</div>
<!-- VIN y Licencia -->
<div class="grid grid-cols-2 gap-4 mb-5">
<Input
v-model="discountData.vin"
id="VIN"
type="text"
disabled
/>
<Input v-model="discountData.vin" id="VIN" type="text" disabled />
<Input
v-model="discountData.licencia"
id="Licencia"
@ -144,7 +298,6 @@ const handleAuthorize = () => {
/>
</div>
<!-- Tarjeta de Circulación y RFC -->
<div class="grid grid-cols-2 gap-4 mb-5">
<Input
v-model="discountData.tarjeta"
@ -152,15 +305,9 @@ const handleAuthorize = () => {
type="text"
disabled
/>
<Input
v-model="discountData.rfc"
id="RFC"
type="text"
disabled
/>
<Input v-model="discountData.rfc" id="RFC" type="text" disabled />
</div>
<!-- Nombre -->
<div class="mb-5">
<Input
v-model="discountData.nombre"
@ -170,10 +317,8 @@ const handleAuthorize = () => {
/>
</div>
<!-- Línea separadora -->
<hr class="my-6 border-gray-300" />
<!-- Monto Original y Pago Mínimo -->
<div class="grid grid-cols-2 gap-4 mb-5">
<Input
v-model="discountData.montoOriginal"
@ -189,22 +334,71 @@ const handleAuthorize = () => {
/>
</div>
<!-- Nuevo Monto a Cobrar (editable) -->
<div v-if="discountData.hasDiscount" class="mb-5">
<Input
v-model="discountData.descuentoAplicado"
id="Descuento Ya Aplicado"
type="text"
disabled
/>
</div>
<div class="mb-6">
<Input
v-model="discountData.nuevoMonto"
id="Nuevo Monto a Cobrar (Autorizado)"
type="text"
type="number"
step="0.01"
:min="discountData.min_total"
:max="discountData.total_amount"
placeholder="Ingrese el nuevo monto autorizado"
:disabled="discountData.isPaid || discountData.hasDiscount"
required
/>
</div>
<!-- Botón Autorizar -->
<!-- Alertas -->
<div
v-if="discountData.isPaid"
class="mb-5 p-4 bg-blue-50 border border-blue-200 rounded-lg"
>
<p class="text-blue-800 font-semibold">
Esta multa ya ha sido pagada previamente
</p>
</div>
<div
v-if="discountData.hasDiscount && !discountData.isPaid"
class="mb-5 p-4 bg-yellow-50 border border-yellow-200 rounded-lg"
>
<p class="text-yellow-800 font-semibold">
Esta multa ya tiene un descuento autorizado de
{{ discountData.descuentoAplicado }}
</p>
<p class="text-yellow-700 text-sm mt-1">
Monto con descuento: ${{
(discountData.total_amount - discountData.discount).toFixed(2)
}}
</p>
</div>
<button
type="submit"
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors"
:disabled="discountData.isPaid || !discountData.id"
:class="[
'w-full font-medium py-3.5 rounded-lg transition-colors',
discountData.isPaid || discountData.hasDiscount || !discountData.id
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-700 hover:bg-green-600 text-white',
]"
>
Autorizar descuento
{{
discountData.isPaid
? "Multa ya pagada"
: discountData.hasDiscount
? "Descuento ya aplicado"
: "Autorizar Descuento"
}}
</button>
</form>
</div>

View File

@ -32,34 +32,31 @@ const handleSearch = async () => {
await api.get(apiURL(`fines/search`), {
params: {
id: fineNumber.value.trim(),
qr_token: fineNumber.value.trim(),
},
onSuccess: (data) => {
const model = data.model;
if (model.payments && model.payments.length > 0) {
const payment = model.payments[0];
if (model.status === "paid") {
window.Notify.info("La multa ya ha sido pagada previamente");
if (payment.status === "paid") {
window.Notify.info("La multa ya ha sido pagada previamente");
fineData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
placa: model.plate || "",
vin: model.vin || "",
licencia: model.license || "",
tarjeta: model.circulation || "",
rfc: model.rfc || "",
nombre: model.name || "",
monto: `$${data.total_to_pay.toFixed(2)}`,
isPaid: true,
};
fineData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
placa: model.plate || "",
vin: model.vin || "",
licencia: model.license || "",
tarjeta: model.circulation || "",
rfc: model.rfc || "",
nombre: model.name || "",
monto: `$${data.total_to_pay.toFixed(2)}`,
isPaid: true,
};
return;
}
return;
}
// Multa pendiente
fineData.value = {
id: model.id,
fecha: new Date(model.created_at).toLocaleDateString("es-MX"),
@ -104,24 +101,18 @@ const handlePayment = async () => {
return;
}
if (fineData.value.isPaid) {
window.Notify.warning("Esta multa ya ha sido pagada previamente");
return;
}
await api.post(apiURL(`fines/${fineData.value.id}/mark-as-paid`), {
onSuccess: (data) => {
window.Notify.success("Multa cobrada exitosamente");
emit("payment-processed", { ...fineData.value, paymentData: data });
fineData.value = {
id: null,
fecha: "",
placa: "",
vin: "",
licencia: "",
tarjeta: "",
rfc: "",
nombre: "",
monto: "",
};
fineNumber.value = "";
fineData.value.isPaid = true;
},
onFail: (error) => {
window.Notify.error(

View File

@ -116,6 +116,7 @@ export default {
legal: 'Instrumento Legal',
article: 'Articulado',
description: 'Contenido',
chargeType: 'Tipo de Cobro',
minimumAmountUma: 'Monto Mínimo (UMAs)',
maximumAmountUma: 'Monto Máximo (UMAs)',
minimumAmountPeso: 'Monto Mínimo (Pesos)',

View File

@ -9,10 +9,7 @@ import PageHeader from "@Holos/PageHeader.vue";
import ConceptForm from "@App/ConceptSection.vue";
import DestroyView from "@Holos/Modal/Template/Destroy.vue";
import ModalController from "@Controllers/ModalController.js";
import Editview from "@Holos/Modal/Edit.vue";
import Input from "@Holos/Form/Input.vue";
import Textarea from "@Holos/Form/Textarea.vue";
import Selectable from "@Holos/Form/Selectable.vue";
import EditModal from "./Modal/Edit.vue";
/** Controladores */
const Modal = new ModalController();
@ -71,35 +68,9 @@ const handleConceptCreated = async (conceptData) => {
});
};
const handleConceptUpdated = async () => {
const transformedData = {
direction_id: typeof modelModal.value.direction === 'object' ? modelModal.value.direction.id : Number(modelModal.value.direction_id),
unit_id: modelModal.value.unit ? (typeof modelModal.value.unit === 'object' ? modelModal.value.unit.id : Number(modelModal.value.unit)) : null,
short_name: modelModal.value.short_name,
name: modelModal.value.name,
legal_instrument: modelModal.value.legal_instrument,
article: modelModal.value.article,
content: modelModal.value.content,
min_amount_uma: modelModal.value.min_amount_uma ? Number(modelModal.value.min_amount_uma) : null,
max_amount_uma: modelModal.value.max_amount_uma ? Number(modelModal.value.max_amount_uma) : null,
min_amount_peso: modelModal.value.min_amount_peso ? Number(modelModal.value.min_amount_peso) : null,
max_amount_peso: modelModal.value.max_amount_peso ? Number(modelModal.value.max_amount_peso) : null,
unit_cost_uma: modelModal.value.unit_cost_uma ? Number(modelModal.value.unit_cost_uma) : null,
unit_cost_peso: modelModal.value.unit_cost_peso ? Number(modelModal.value.unit_cost_peso) : null,
};
const form = useForm(transformedData);
form.put(apiTo("update", { charge_concept: modelModal.value.id }), {
onSuccess: () => {
Notify.success(transl("edit.onSuccess"));
searcher.refresh();
Modal.switchEditModal();
},
onError: () => {
Notify.error(transl("edit.onError"));
},
});
const handleConceptUpdated = () => {
searcher.refresh();
Modal.switchEditModal();
};
</script>
<template>
@ -181,104 +152,13 @@ const handleConceptUpdated = async () => {
}
"
/>
<Editview
<EditModal
:show="editModal"
:title="transl('edit.title')"
:model="modelModal"
:addresses="addresses"
:units="units"
@close="Modal.switchEditModal"
@update="handleConceptUpdated"
>
<div class="p-4 space-y-4">
<Selectable
v-model="modelModal.direction"
label="name"
value="id"
:title="$t('concept.direction')"
:options="addresses"
required
/>
<Input
v-model="modelModal.short_name"
:id="$t('concept.shortName')"
type="text"
required
/>
<Input
v-model="modelModal.name"
:id="$t('concept.name')"
type="text"
required
/>
<Input
v-model="modelModal.legal_instrument"
:id="$t('concept.legal')"
type="text"
required
/>
<Input
v-model="modelModal.article"
:id="$t('concept.article')"
type="text"
required
/>
<Textarea
v-model="modelModal.content"
:id="$t('concept.description')"
required
/>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.min_amount_uma"
:id="$t('concept.minimumAmountUma')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.max_amount_uma"
:id="$t('concept.maximumAmountUma')"
type="number"
step="0.01"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.min_amount_peso"
:id="$t('concept.minimumAmountPeso')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.max_amount_peso"
:id="$t('concept.maximumAmountPeso')"
type="number"
step="0.01"
/>
</div>
<hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-2 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<Selectable
v-model="modelModal.unit"
label="name"
value="id"
:title="$t('concept.sizeUnit')"
:options="units"
/>
<div class="grid grid-cols-2 gap-4">
<Input
v-model="modelModal.unit_cost_uma"
:id="$t('concept.costUnitUma')"
type="number"
step="0.01"
/>
<Input
v-model="modelModal.unit_cost_peso"
:id="$t('concept.costUnitPeso')"
type="number"
step="0.01"
/>
</div>
</div>
</Editview>
@updated="handleConceptUpdated"
/>
</div>
</template>

View File

@ -0,0 +1,227 @@
<script setup>
import { computed } from "vue";
import { useForm } from "@Services/Api";
import { apiTo, transl } from "../Module";
import Editview from "@Holos/Modal/Edit.vue";
import Input from "@Holos/Form/Input.vue";
import Textarea from "@Holos/Form/Textarea.vue";
import Selectable from "@Holos/Form/Selectable.vue";
/** Props */
const props = defineProps({
show: Boolean,
model: Object,
addresses: Array,
units: Array,
});
/** Eventos */
const emit = defineEmits(['close', 'updated']);
const chargeTypesOptions = [
{ id: 'uma_range', name: 'UMA Range' },
{ id: 'peso_range', name: 'Peso Range' },
{ id: 'uma_fixed', name: 'UMA unitario' },
{ id: 'peso_fixed', name: 'Peso unitario' },
];
const chargeTypeObject = computed({
get() {
if (!props.model.charge_type) return null;
if (typeof props.model.charge_type === 'object') {
return props.model.charge_type;
}
return chargeTypesOptions.find(opt => opt.id === props.model.charge_type) || null;
},
set(value) {
props.model.charge_type = value;
}
});
const editChargeTypeId = computed(() => {
if (!props.model.charge_type) return null;
return typeof props.model.charge_type === 'object' && props.model.charge_type?.id
? props.model.charge_type.id
: props.model.charge_type;
});
const editShowUmaFields = computed(() => {
return editChargeTypeId.value === 'uma_range' || editChargeTypeId.value === 'uma_fixed';
});
const editShowPesoFields = computed(() => {
return editChargeTypeId.value === 'peso_range' || editChargeTypeId.value === 'peso_fixed';
});
const editIsRangeType = computed(() => {
return editChargeTypeId.value === 'uma_range' || editChargeTypeId.value === 'peso_range';
});
const editIsFixedType = computed(() => {
return editChargeTypeId.value === 'uma_fixed' || editChargeTypeId.value === 'peso_fixed';
});
/** Metodos */
const handleUpdate = async () => {
const transformedData = {
direction_id: typeof props.model.direction === 'object' ? props.model.direction.id : Number(props.model.direction_id),
unit_id: props.model.unit ? (typeof props.model.unit === 'object' ? props.model.unit.id : Number(props.model.unit)) : null,
short_name: props.model.short_name,
name: props.model.name,
legal_instrument: props.model.legal_instrument,
article: props.model.article,
content: props.model.content,
charge_type: editChargeTypeId.value,
// Campos UMA para tipos de rango
min_amount_uma: (editShowUmaFields.value && editIsRangeType.value && props.model.min_amount_uma) ? Number(props.model.min_amount_uma) : null,
max_amount_uma: (editShowUmaFields.value && editIsRangeType.value && props.model.max_amount_uma) ? Number(props.model.max_amount_uma) : null,
// Campos Peso para tipos de rango
min_amount_peso: (editShowPesoFields.value && editIsRangeType.value && props.model.min_amount_peso) ? Number(props.model.min_amount_peso) : null,
max_amount_peso: (editShowPesoFields.value && editIsRangeType.value && props.model.max_amount_peso) ? Number(props.model.max_amount_peso) : null,
// Campos de costo unitario para tipos fijos
unit_cost_uma: (editShowUmaFields.value && editIsFixedType.value && props.model.unit_cost_uma) ? Number(props.model.unit_cost_uma) : null,
unit_cost_peso: (editShowPesoFields.value && editIsFixedType.value && props.model.unit_cost_peso) ? Number(props.model.unit_cost_peso) : null,
};
const form = useForm(transformedData);
form.put(apiTo("update", { charge_concept: props.model.id }), {
onSuccess: () => {
Notify.success(transl("edit.onSuccess"));
emit('updated');
},
onError: () => {
Notify.error(transl("edit.onError"));
},
});
};
</script>
<template>
<Editview
:show="show"
:title="transl('edit.title')"
@close="emit('close')"
@update="handleUpdate"
>
<div class="p-4 space-y-4">
<Selectable
v-model="model.direction"
label="name"
value="id"
:title="$t('concept.direction')"
:options="addresses"
required
/>
<Input
v-model="model.short_name"
:id="$t('concept.shortName')"
type="text"
required
/>
<Input
v-model="model.name"
:id="$t('concept.name')"
type="text"
required
/>
<Input
v-model="model.legal_instrument"
:id="$t('concept.legal')"
type="text"
required
/>
<Input
v-model="model.article"
:id="$t('concept.article')"
type="text"
required
/>
<Textarea
v-model="model.content"
:id="$t('concept.description')"
required
/>
<!-- Selector de tipo de cargo -->
<Selectable
v-model="chargeTypeObject"
label="name"
:title="$t('concept.chargeType')"
:options="chargeTypesOptions"
required
/>
<div v-if="editShowUmaFields && editIsRangeType" class="grid grid-cols-2 gap-4">
<Input
v-model="model.min_amount_uma"
:id="$t('concept.minimumAmountUma')"
type="number"
step="0.01"
required
/>
<Input
v-model="model.max_amount_uma"
:id="$t('concept.maximumAmountUma')"
type="number"
step="0.01"
required
/>
</div>
<div v-if="editShowPesoFields && editIsRangeType" class="grid grid-cols-2 gap-4">
<Input
v-model="model.min_amount_peso"
:id="$t('concept.minimumAmountPeso')"
type="number"
step="0.01"
required
/>
<Input
v-model="model.max_amount_peso"
:id="$t('concept.maximumAmountPeso')"
type="number"
step="0.01"
required
/>
</div>
<Selectable
v-model="model.unit"
label="name"
value="id"
:title="$t('concept.sizeUnit')"
:options="units"
required
/>
<div v-if="editIsFixedType">
<hr class="my-4 border-gray-300" />
<h4 class="text-lg font-semibold mb-2 text-gray-700">
{{ $t('concept.tabulator') }}
</h4>
<div class="grid grid-cols-2 gap-4">
<Input
v-if="editShowUmaFields"
v-model="model.unit_cost_uma"
:id="$t('concept.costUnitUma')"
type="number"
step="0.01"
required
/>
<Input
v-if="editShowPesoFields"
v-model="model.unit_cost_peso"
:id="$t('concept.costUnitPeso')"
type="number"
step="0.01"
required
/>
</div>
</div>
</div>
</Editview>
</template>

View File

@ -0,0 +1,21 @@
import { lang } from '@Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js';
// Ruta API
const apiTo = (name, params = {}) => route(`discount.${name}`, params)
// Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `discount.${name}`, params, query })
// Obtener traducción del componente
const transl = (str) => lang(`discount.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`discount.${permission}`)
export {
can,
viewTo,
apiTo,
transl
}