ADD: modificación modulo concepto, multa y descuento
This commit is contained in:
parent
3029d425fe
commit
bd6edc59d6
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)',
|
||||
|
||||
@ -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>
|
||||
|
||||
227
src/pages/App/Concept/Modal/Edit.vue
Normal file
227
src/pages/App/Concept/Modal/Edit.vue
Normal 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>
|
||||
21
src/pages/App/Discount/Module.js
Normal file
21
src/pages/App/Discount/Module.js
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user