MOD: actualizar módulo de entrega de caja y escaneo de QR

This commit is contained in:
Juan Felipe Zapata Moreno 2025-11-24 16:40:50 -06:00
parent bd6edc59d6
commit 5e46e55d73
4 changed files with 580 additions and 153 deletions

View File

@ -1,114 +1,297 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { apiURL, useApi } from "@/services/Api.js";
import Input from "@Holos/Form/Input.vue"; import Input from "@Holos/Form/Input.vue";
import QRscan from "./QRscan.vue"; import QRscan from "./QRscan.vue";
/** Eventos */ /** Eventos */
const emit = defineEmits(["fine-searched"]); const emit = defineEmits(["cash-cut-found", "cash-cut-delivered"]);
/** Instancias */
const api = useApi();
/** Refs */ /** Refs */
const numero_entrega = ref(""); const qr_token = ref("");
const fineData = ref({ const loading = ref(false);
fecha: "", const cashCutData = ref({
numero_entrega: "", id: "",
nombre: "", closed_by: "",
monto_entrega: "", start_at: "",
end_at: "",
total_amount: 0,
status: "",
received_at: "",
received_by: "",
isReceived: false,
});
const statusTranslations = {
undelivered: "Sin entregar",
received: "Entregado",
};
const statusInSpanish = computed(() => {
return (
statusTranslations[cashCutData.value.status] || cashCutData.value.status
);
});
const formattedStartDate = computed(() => {
if (!cashCutData.value.start_at) return "";
return cashCutData.value.start_at.slice(0, 16);
});
const formattedEndDate = computed(() => {
if (!cashCutData.value.end_at) return "";
return cashCutData.value.end_at.slice(0, 16);
});
const cashierName = computed(() => {
return cashCutData.value.closed_by?.full_name || "N/A";
});
const formattedAmount = computed(() => {
if (!cashCutData.value.total_amount) return "0.00";
return `$${parseFloat(cashCutData.value.total_amount).toLocaleString(
"es-MX",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}
)}`;
}); });
/** Métodos */ /** Métodos */
const handleSearch = () => { const handleQRDetected = async (code) => {
if (!numero_entrega.value.trim()) { qr_token.value = code;
Notify.warning("Por favor ingresa un número de entrega"); await searchCashCut();
};
const searchCashCut = async () => {
if (!qr_token.value.trim()) {
Notify.warning("Por favor escanea un código QR o ingresa el token");
return; return;
} }
fineData.value = { loading.value = true;
fecha: "2025-11-11",
numero_entrega: numero_entrega.value,
nombre: "Juan Pérez García",
monto_entrega: "$1,500.00",
};
emit("fine-searched", fineData.value); await api.get(apiURL(`cash-cuts/find-by-qr-token`), {
params: {
qr_token: qr_token.value,
},
onSuccess: (response) => {
const model = response.model;
if (model.status === "received") {
Notify.info("La caja ya ha sido entregada previamente");
cashCutData.value = {
id: model.id,
closed_by: model.closed_by,
start_at: model.start_at,
end_at: model.end_at,
total_amount: model.total_amount,
status: model.status,
received_at: model.received_at,
received_by: model.received_by,
isReceived: true,
};
return;
}
cashCutData.value = {
id: model.id,
closed_by: model.closed_by,
start_at: model.start_at,
end_at: model.end_at,
total_amount: model.total_amount,
status: model.status,
received_at: model.received_at,
received_by: model.received_by,
isReceived: false,
};
Notify.success("Corte de caja encontrado");
emit("cash-cut-found", cashCutData.value);
},
onFail: (error) => {
Notify.warning(error.message || "No se encontró el corte de caja");
},
onError: (error) => {
Notify.error("Error al buscar el corte de caja");
},
});
loading.value = false;
};
const handleDelivery = async () => {
if (!cashCutData.value.id) {
Notify.warning("No hay corte de caja para entregar");
return;
}
if (cashCutData.value.isReceived) {
Notify.info("El corte de caja ya ha sido entregado");
return;
}
await api.put(apiURL(`cash-cuts/${cashCutData.value.id}/mark-as-received`), {
onSuccess: (data) => {
Notify.success("Corte de caja entregado correctamente");
cashCutData.value.status = "received";
emit("cash-cut-delivered", cashCutData.value);
cashCutData.value.isReceived = true;
cashCutData.value.received_at = data.received_at;
cashCutData.value.received_by = data.received_by;
},
onFail: (error) => {
Notify.warning(error.message || "No se pudo entregar el corte de caja");
},
onError: () => {
Notify.error("Error al entregar el corte de caja");
},
});
loading.value = false;
};
const handleSearch = () => {
searchCashCut();
};
const clearForm = () => {
cashCutData.value = {
id: null,
closed_by: null,
start_at: "",
end_at: "",
total_amount: 0,
status: "",
received_at: "",
received_by: "",
isReceived: false,
};
qr_token.value = "";
}; };
</script> </script>
<template> <template>
<div class="space-y-6 p-6"> <div class="space-y-6 p-6">
<div class="bg-white rounded-xl p-6 shadow-lg"> <div class="bg-white rounded-xl p-6 shadow-lg">
<h3 class="text-xl font-semibold mb-4 text-gray-800">Buscar Multa</h3> <h3 class="text-xl font-semibold mb-4 text-gray-800">
Buscar Corte de Caja
</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Scanner QR -->
<div class="mb-6"> <div class="mb-6">
<label class="block text-sm text-gray-600 font-medium mb-2"> <label class="block text-sm text-gray-600 font-medium mb-2">
Escanear Código QR Escanear Código QR
</label> </label>
<div <div class="w-full h-80 bg-gray-800 rounded-lg overflow-hidden">
class="w-full h-64 bg-gray-800 rounded-lg overflow-hidden" <QRscan v-model="qr_token" @qr-detected="handleQRDetected" />
>
<!-- QR -->
<QRscan
v-model:numero_entrega="numero_entrega"
@fine-searched="handleSearch"
/>
</div> </div>
</div> </div>
<!-- Input manual -->
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div class="mb-5"> <div class="mb-5">
<Input <Input
v-model="numero_entrega" v-model="qr_token"
id="Número de entrega" id="Token QR"
type="text" type="text"
placeholder="Ingrese el número de entrega" placeholder="Ingrese el token del QR"
@keyup.enter="handleSearch" @keyup.enter="handleSearch"
/> />
</div> </div>
<button <button
@click="handleSearch" @click="handleSearch"
:disabled="loading"
type="button" type="button"
class="w-full bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors" class="w-full bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors disabled:opacity-50"
> >
Buscar {{ loading ? "Buscando..." : "Buscar" }}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-white rounded-xl p-6 shadow-lg"> <!-- Formulario con datos del corte -->
<form @submit.prevent="handlePayment"> <div v-if="cashCutData.id" class="bg-white rounded-xl p-6 shadow-lg">
<!-- Fecha --> <h3 class="text-xl font-semibold mb-4 text-gray-800">
<div class="grid grid-cols-2 gap-4 mb-5"> Datos del Corte de Caja
<Input </h3>
v-model="fineData.fecha"
id="Fecha de entrega"
type="date"
/>
</div>
<!-- Nombre --> <div class="grid grid-cols-2 gap-4 mb-5">
<div class="mb-5"> <Input
<Input v-model="fineData.nombre" id="Nombre del cajero" type="text"/> :model-value="formattedStartDate"
</div> id="Fecha de inicio"
type="datetime-local"
disabled
/>
<Input
:model-value="formattedEndDate"
id="Fecha de cierre"
type="datetime-local"
disabled
/>
</div>
<!-- Monto a Pagar --> <div class="mb-5">
<div class="mb-5"> <Input :model-value="cashierName" id="Cajero" type="text" disabled />
<Input </div>
v-model="fineData.monto_entrega"
id="Monto a entregar"
type="text"
class="text-lg font-semibold"
/>
</div>
<!-- Botón Cobrar --> <div class="mb-5">
<button <Input
type="submit" :model-value="formattedAmount"
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors" id="Monto total"
> type="text"
Entregar caja class="text-lg font-semibold"
</button> disabled
</form> />
</div>
<div class="mb-5">
<Input
:model-value="statusInSpanish"
id="Estado"
type="text"
disabled
/>
</div>
<div class="space-y-6">
<div v-if="cashCutData.isReceived">
<button
type="button"
@click="clearForm"
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
Limpiar y buscar otro corte
</button>
</div>
<!-- Botón recibir -->
<div>
<button
type="submit"
:disabled="cashCutData.isReceived"
:class="[
'w-full font-medium py-3.5 rounded-lg transition-colors',
cashCutData.isReceived
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-700 hover:bg-green-600 text-white',
]"
>
Entregar Corte de Caja
</button>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -26,7 +26,7 @@ const fineData = ref({
/** Métodos */ /** Métodos */
const handleSearch = async () => { const handleSearch = async () => {
if (!fineNumber.value.trim()) { if (!fineNumber.value.trim()) {
window.Notify.warning("Por favor ingresa o escanea una multa"); Notify.warning("Por favor ingresa o escanea una multa");
return; return;
} }
@ -38,7 +38,7 @@ const handleSearch = async () => {
const model = data.model; const model = data.model;
if (model.status === "paid") { if (model.status === "paid") {
window.Notify.info("La multa ya ha sido pagada previamente"); Notify.info("La multa ya ha sido pagada previamente");
fineData.value = { fineData.value = {
id: model.id, id: model.id,
@ -76,7 +76,7 @@ const handleSearch = async () => {
}); });
}, },
onFail: (error) => { onFail: (error) => {
window.Notify.error(error.message || "Error al buscar la multa"); Notify.error(error.message || "Error al buscar la multa");
fineData.value = { fineData.value = {
fecha: "", fecha: "",
placa: "", placa: "",
@ -90,37 +90,37 @@ const handleSearch = async () => {
}, },
onError: (error) => { onError: (error) => {
console.error("Error al buscar multa:", error); console.error("Error al buscar multa:", error);
window.Notify.error("Ocurrió un error al buscar la multa"); Notify.error("Ocurrió un error al buscar la multa");
}, },
}); });
}; };
const handlePayment = async () => { const handlePayment = async () => {
if (!fineData.value.monto) { if (!fineData.value.monto) {
window.Notify.warning("No hay multa cargada para cobrar"); Notify.warning("No hay multa cargada para cobrar");
return; return;
} }
if (fineData.value.isPaid) { if (fineData.value.isPaid) {
window.Notify.warning("Esta multa ya ha sido pagada previamente"); Notify.warning("Esta multa ya ha sido pagada previamente");
return; return;
} }
await api.post(apiURL(`fines/${fineData.value.id}/mark-as-paid`), { await api.post(apiURL(`fines/${fineData.value.id}/mark-as-paid`), {
onSuccess: (data) => { onSuccess: (data) => {
window.Notify.success("Multa cobrada exitosamente"); Notify.success("Multa cobrada exitosamente");
emit("payment-processed", { ...fineData.value, paymentData: data }); emit("payment-processed", { ...fineData.value, paymentData: data });
fineData.value.isPaid = true; fineData.value.isPaid = true;
}, },
onFail: (error) => { onFail: (error) => {
window.Notify.error( Notify.error(
error.message || "Error al procesar el pago de la multa" error.message || "Error al procesar el pago de la multa"
); );
}, },
onError: (error) => { onError: (error) => {
window.Notify.error("Ocurrió un error al procesar el pago de la multa"); Notify.error("Ocurrió un error al procesar el pago de la multa");
}, },
}); });
}; };

View File

@ -1,75 +1,221 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { apiURL, useApi } from "@/services/Api.js";
import Input from "@Holos/Form/Input.vue"; import Input from "@Holos/Form/Input.vue";
import Selectable from "@Holos/Form/Selectable.vue";
/** Eventos */ /** Eventos */
const emit = defineEmits(["membership-paid"]); const emit = defineEmits(["membership-paid"]);
/** Instancias */
const api = useApi();
/** Refs */ /** Refs */
const searchForm = ref({ const loading = ref(false);
membershipNumber: "", const curp = ref("");
curp: "",
});
const memberData = ref({ const memberData = ref({
image: null, id: null,
name: "",
curp: "", curp: "",
tutorName: "", name: "",
tutor: null,
photo: "",
photo_url: "",
memberships: [],
}); });
const paymentData = ref({ const paymentData = ref({
service: [], membershipId: null,
quantity: 1, quantity: 1,
totalAmount: "", totalAmount: 0,
}); });
const serviceOptions = ref([ /** Computed */
{ id: 1, description: "Membresía Mensual" }, const activeMembership = computed(() => {
{ id: 2, description: "Membresía Trimestral" }, if (
{ id: 3, description: "Membresía Semestral" }, !memberData.value.memberships ||
{ id: 4, description: "Membresía Anual" }, memberData.value.memberships.length === 0
]); ) {
return null;
}
return memberData.value.memberships[0];
});
const expiresAt = computed(() => {
if (!activeMembership.value || !activeMembership.value.expires_at)
return null;
const date = new Date(activeMembership.value.expires_at);
return date.toLocaleDateString("es-MX", {
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
const formattedAmount = computed(() => {
if (!paymentData.value.totalAmount) return "$0.00";
return `$${parseFloat(paymentData.value.totalAmount).toLocaleString("es-MX", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
});
/** Métodos */ /** Métodos */
const handleSearch = () => { const handleSearch = async () => {
if (!searchForm.value.membershipNumber && !searchForm.value.curp) { if (!curp.value.trim()) {
alert("Por favor ingresa al menos un criterio de búsqueda"); Notify.warning("Por favor ingresa un CURP");
return; return;
} }
console.log("Buscando miembro:", searchForm.value); loading.value = true;
// Simular datos encontrados await api.get(apiURL("members/search"), {
memberData.value = { params: {
name: "Juan Pérez García", curp: curp.value,
curp: "PEGJ900101HDFRNN09", },
tutorName: "María López Hernández", onSuccess: (response) => {
image: null, const model = response.model;
};
};
const handlePayment = () => { memberData.value = {
if (!paymentData.value.service) { id: model.id,
alert("Por favor selecciona un servicio"); curp: model.curp,
return; name: model.name,
} tutor: model.tutor,
photo: model.photo,
photo_url: model.photo_url,
memberships: model.memberships || [],
};
console.log("Procesando pago de membresía:", { if (memberData.value.memberships.length === 0) {
member: memberData.value, Notify.warning("El miembro no tiene membresías activas");
payment: paymentData.value, } else {
Notify.success("Miembro encontrado");
// Pre-cargar datos del pago
if (activeMembership.value) {
paymentData.value.membershipId = activeMembership.value.id;
calculateTotal();
}
}
},
onFail: (error) => {
Notify.warning(error.message || "No se encontró el miembro con ese CURP");
clearMemberData();
},
onError: () => {
Notify.error("Error al buscar el miembro");
clearMemberData();
},
}); });
emit("membership-paid", { loading.value = false;
member: memberData.value,
payment: paymentData.value,
});
}; };
const calculateTotal = () => { const calculateTotal = () => {
paymentData.value.totalAmount = "0.00"; if (!activeMembership.value) {
paymentData.value.totalAmount = 0;
return;
}
const unitCost = parseFloat(
activeMembership.value.charge_concept.unit_cost_peso || 0
);
const quantity = parseInt(paymentData.value.quantity) || 1;
paymentData.value.totalAmount = unitCost * quantity;
};
const handlePayment = async () => {
if (!memberData.value.id) {
Notify.warning("Primero debes buscar un miembro");
return;
}
if (!activeMembership.value) {
Notify.warning("El miembro no tiene membresías activas para pagar");
return;
}
if (paymentData.value.totalAmount <= 0) {
Notify.warning("El monto debe ser mayor a 0");
return;
}
loading.value = true;
await api.put(apiURL(`members/${memberData.value.id}/charge-membership`), {
data: {
charge_concept_id: activeMembership.value.charge_concept.id,
quantity: paymentData.value.quantity,
},
onSuccess: (response) => {
Notify.success("Pago de membresía procesado correctamente");
// Actualizar la membresía con la nueva info
if (response.membership) {
memberData.value.memberships = [response.membership];
}
emit("membership-paid", {
member: response.member,
membership: response.membership,
payment: response.payment,
});
clearMemberData();
},
onFail: (error) => {
Notify.warning(error.message || "No se pudo procesar el pago");
},
onError: () => {
Notify.error("Error al procesar el pago");
},
});
loading.value = false;
};
const isMembershipActive = computed(() => {
if (!activeMembership.value || !activeMembership.value.expires_at)
return false;
const expirationDate = new Date(activeMembership.value.expires_at);
const now = new Date();
return expirationDate > now;
});
// Computed para mostrar días restantes
const daysUntilExpiration = computed(() => {
if (!activeMembership.value || !activeMembership.value.expires_at)
return null;
const expirationDate = new Date(activeMembership.value.expires_at);
const now = new Date();
const diffTime = expirationDate - now;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays > 0 ? diffDays : 0;
});
const clearMemberData = () => {
memberData.value = {
id: null,
curp: "",
name: "",
tutor: null,
photo: "",
photo_url: "",
memberships: [],
};
paymentData.value = {
membershipId: null,
quantity: 1,
totalAmount: 0,
};
curp.value = "";
}; };
</script> </script>
@ -79,36 +225,33 @@ const calculateTotal = () => {
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6"> <div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6">
<h3 class="text-xl font-semibold mb-6 text-gray-800">Buscar miembro</h3> <h3 class="text-xl font-semibold mb-6 text-gray-800">Buscar miembro</h3>
<form @submit.prevent="handleSearch"> <form @submit.prevent="handleSearch">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
<!-- Número de Membresía -->
<Input
v-model="searchForm.membershipNumber"
id="Número de Membresía"
type="text"
placeholder=""
/>
<!-- CURP --> <!-- CURP -->
<Input <Input
v-model="searchForm.curp" v-model="curp"
id="CURP" id="CURP"
type="text" type="text"
placeholder="" placeholder="Ej: AMWO0020923"
:disabled="loading"
/> />
<!-- Botón Buscar --> <!-- Botón Buscar -->
<button <button
type="submit" type="submit"
class="py-3 px-8 rounded-lg transition-colors h-[42px] bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium" :disabled="loading"
class="py-3 px-8 rounded-lg transition-colors h-[42px] bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium disabled:opacity-50"
> >
Buscar {{ loading ? "Buscando..." : "Buscar" }}
</button> </button>
</div> </div>
</form> </form>
</div> </div>
<!-- Información del Miembro --> <!-- Información del Miembro -->
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6"> <div
v-if="memberData.id"
class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg mb-6"
>
<h3 class="text-xl font-semibold mb-6 text-gray-800"> <h3 class="text-xl font-semibold mb-6 text-gray-800">
Información del Miembro Información del Miembro
</h3> </h3>
@ -120,9 +263,16 @@ const calculateTotal = () => {
Fotografía Fotografía
</label> </label>
<div <div
class="w-full h-64 bg-gray-200 rounded-lg flex items-center justify-center" class="w-full h-80 bg-gray-200 rounded-lg flex items-center justify-center overflow-hidden"
> >
<img
v-if="memberData.photo_url"
:src="memberData.photo_url"
:alt="memberData.name"
class="w-80 h-80 object-cover"
/>
<svg <svg
v-else
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-24 w-24 text-gray-400" class="h-24 w-24 text-gray-400"
fill="none" fill="none"
@ -141,60 +291,154 @@ const calculateTotal = () => {
<!-- Datos del miembro --> <!-- Datos del miembro -->
<div class="space-y-5"> <div class="space-y-5">
<Input v-model="memberData.name" id="Nombre" type="text" disabled /> <Input
:model-value="memberData.name"
<Input v-model="memberData.curp" id="CURP" type="text" disabled /> id="Nombre"
type="text"
disabled
/>
<Input <Input
v-model="memberData.tutorName" :model-value="memberData.curp"
id="CURP"
type="text"
disabled
/>
<Input
:model-value="memberData.tutor || 'Sin tutor'"
id="Nombre Tutor" id="Nombre Tutor"
type="text" type="text"
disabled disabled
/> />
<!-- Información de membresía -->
<div v-if="activeMembership" class="space-y-3">
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-gray-600">Tipo de membresía:</p>
<p class="text-lg font-semibold text-gray-800">
{{ activeMembership.charge_concept.name }}
</p>
</div>
<div v-if="expiresAt" class="p-4 bg-yellow-50 rounded-lg">
<p class="text-sm text-gray-600">Expira:</p>
<p class="text-lg font-semibold text-gray-800">{{ expiresAt }}</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Realizar Cobro de Membresía --> <!-- Realizar Cobro de Membresía -->
<div class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg"> <div
v-if="memberData.id && activeMembership"
class="bg-white rounded-xl p-6 m-3 max-w-auto shadow-lg"
>
<h3 class="text-xl font-semibold mb-6 text-gray-800"> <h3 class="text-xl font-semibold mb-6 text-gray-800">
Realizar Cobro de Membresía Realizar Cobro de Membresía
</h3> </h3>
<form @submit.prevent="handlePayment"> <form @submit.prevent="handlePayment">
<div class="grid grid-cols-3 gap-4 mb-6"> <div class="grid grid-cols-3 gap-4 mb-6">
<!-- Servicio a Pagar --> <!-- Servicio -->
<Selectable <div>
v-model="paymentData.service" <Input
label="description" :model-value="activeMembership.charge_concept.name"
title="Servicio a pagar" id="Servicio"
:options="serviceOptions" type="text"
/> disabled
/>
<p class="text-xs text-gray-500 mt-1">
Costo unitario: ${{
activeMembership.charge_concept.unit_cost_peso
}}
</p>
</div>
<!-- Cantidad --> <!-- Cantidad -->
<Input <Input
v-model="paymentData.quantity" v-model.number="paymentData.quantity"
id="Cantidad (días)" id="Cantidad"
type="number" type="number"
min="1" min="1"
:disabled="loading"
@input="calculateTotal" @input="calculateTotal"
/> />
<!-- Monto Total --> <!-- Monto Total -->
<Input <Input
v-model="paymentData.totalAmount" :model-value="formattedAmount"
id="Monto Total" id="Monto Total"
type="text" type="text"
disabled disabled
/> />
</div> </div>
<!-- Botón Cobrar --> <!-- Información de membresía -->
<div v-if="activeMembership" class="space-y-3">
<!-- Badge de estado de membresía -->
<div
:class="[
'p-4 rounded-lg border-2',
isMembershipActive
? 'bg-green-50 border-green-500'
: 'bg-red-50 border-red-500',
]"
>
<p
class="text-sm font-semibold"
:class="isMembershipActive ? 'text-green-700' : 'text-red-700'"
>
{{
isMembershipActive
? "✓ Membresía activa"
: "✗ Membresía vencida"
}}
</p>
<p
v-if="isMembershipActive && daysUntilExpiration"
class="text-xs mt-1"
:class="
daysUntilExpiration <= 7 ? 'text-orange-600' : 'text-green-600'
"
>
{{
daysUntilExpiration === 1
? "Vence mañana"
: `Vence en ${daysUntilExpiration} días`
}}
</p>
</div>
<div class="p-4 bg-blue-50 rounded-lg">
<p class="text-sm text-gray-600">Tipo de membresía:</p>
<p class="text-lg font-semibold text-gray-800">
{{ activeMembership.charge_concept.name }}
</p>
</div>
<div v-if="expiresAt" class="p-4 bg-yellow-50 rounded-lg">
<p class="text-sm text-gray-600">
{{ isMembershipActive ? "Expira:" : "Expiró:" }}
</p>
<p class="text-lg font-semibold text-gray-800">{{ expiresAt }}</p>
</div>
</div>
<!-- Botón de pago -->
<button <button
type="submit" type="submit"
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors" :disabled="loading || !activeMembership || isMembershipActive"
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
Pagar membresía {{
loading
? "Procesando..."
: isMembershipActive
? "Membresía activa - No requiere pago"
: "Renovar membresía"
}}
</button> </button>
</form> </form>
</div> </div>

View File

@ -3,14 +3,14 @@ import { ref } from 'vue';
import { QrcodeStream } from 'vue-qrcode-reader'; import { QrcodeStream } from 'vue-qrcode-reader';
/** Props y Emits */ /** Props y Emits */
defineProps({ const props = defineProps({
fineNumber: { modelValue: {
type: String, type: String,
default: '' default: ''
} }
}); });
const emit = defineEmits(['update:fineNumber', 'fine-searched']); const emit = defineEmits(['update:modelValue', 'qr-detected']);
/** Refs */ /** Refs */
const error = ref(''); const error = ref('');
@ -34,13 +34,13 @@ function onDetect(detectedCodes) {
const code = detectedCodes[0].rawValue; const code = detectedCodes[0].rawValue;
// Emitir el código escaneado al componente padre // Emitir el código escaneado al componente padre
emit('update:fineNumber', code); emit('update:modelValue', code);
// Pausar el escaneo después de detectar // Pausar el escaneo después de detectar
isScanning.value = false; isScanning.value = false;
// Emitir evento para buscar la multa // Emitir evento con el código detectado
emit('fine-searched'); emit('qr-detected', code);
// Reactivar el escaneo después de 2 segundos // Reactivar el escaneo después de 2 segundos
setTimeout(() => { setTimeout(() => {