MOD: actualizar módulo de entrega de caja y escaneo de QR
This commit is contained in:
parent
bd6edc59d6
commit
5e46e55d73
@ -1,114 +1,297 @@
|
||||
<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 QRscan from "./QRscan.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(["fine-searched"]);
|
||||
const emit = defineEmits(["cash-cut-found", "cash-cut-delivered"]);
|
||||
|
||||
/** Instancias */
|
||||
const api = useApi();
|
||||
|
||||
/** Refs */
|
||||
const numero_entrega = ref("");
|
||||
const fineData = ref({
|
||||
fecha: "",
|
||||
numero_entrega: "",
|
||||
nombre: "",
|
||||
monto_entrega: "",
|
||||
const qr_token = ref("");
|
||||
const loading = ref(false);
|
||||
const cashCutData = ref({
|
||||
id: "",
|
||||
closed_by: "",
|
||||
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 */
|
||||
const handleSearch = () => {
|
||||
if (!numero_entrega.value.trim()) {
|
||||
Notify.warning("Por favor ingresa un número de entrega");
|
||||
const handleQRDetected = async (code) => {
|
||||
qr_token.value = code;
|
||||
await searchCashCut();
|
||||
};
|
||||
|
||||
const searchCashCut = async () => {
|
||||
if (!qr_token.value.trim()) {
|
||||
Notify.warning("Por favor escanea un código QR o ingresa el token");
|
||||
return;
|
||||
}
|
||||
|
||||
fineData.value = {
|
||||
fecha: "2025-11-11",
|
||||
numero_entrega: numero_entrega.value,
|
||||
nombre: "Juan Pérez García",
|
||||
monto_entrega: "$1,500.00",
|
||||
loading.value = true;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
emit("fine-searched", fineData.value);
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6 p-6">
|
||||
<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">
|
||||
<!-- Scanner QR -->
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm text-gray-600 font-medium mb-2">
|
||||
Escanear Código QR
|
||||
</label>
|
||||
<div
|
||||
class="w-full h-64 bg-gray-800 rounded-lg overflow-hidden"
|
||||
>
|
||||
<!-- QR -->
|
||||
<QRscan
|
||||
v-model:numero_entrega="numero_entrega"
|
||||
@fine-searched="handleSearch"
|
||||
/>
|
||||
<div class="w-full h-80 bg-gray-800 rounded-lg overflow-hidden">
|
||||
<QRscan v-model="qr_token" @qr-detected="handleQRDetected" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input manual -->
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="numero_entrega"
|
||||
id="Número de entrega"
|
||||
v-model="qr_token"
|
||||
id="Token QR"
|
||||
type="text"
|
||||
placeholder="Ingrese el número de entrega"
|
||||
placeholder="Ingrese el token del QR"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="handleSearch"
|
||||
:disabled="loading"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<form @submit.prevent="handlePayment">
|
||||
<!-- Fecha -->
|
||||
<!-- Formulario con datos del corte -->
|
||||
<div v-if="cashCutData.id" class="bg-white rounded-xl p-6 shadow-lg">
|
||||
<h3 class="text-xl font-semibold mb-4 text-gray-800">
|
||||
Datos del Corte de Caja
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-5">
|
||||
<Input
|
||||
v-model="fineData.fecha"
|
||||
id="Fecha de entrega"
|
||||
type="date"
|
||||
:model-value="formattedStartDate"
|
||||
id="Fecha de inicio"
|
||||
type="datetime-local"
|
||||
disabled
|
||||
/>
|
||||
<Input
|
||||
:model-value="formattedEndDate"
|
||||
id="Fecha de cierre"
|
||||
type="datetime-local"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Nombre -->
|
||||
<div class="mb-5">
|
||||
<Input v-model="fineData.nombre" id="Nombre del cajero" type="text"/>
|
||||
<Input :model-value="cashierName" id="Cajero" type="text" disabled />
|
||||
</div>
|
||||
|
||||
<!-- Monto a Pagar -->
|
||||
<div class="mb-5">
|
||||
<Input
|
||||
v-model="fineData.monto_entrega"
|
||||
id="Monto a entregar"
|
||||
:model-value="formattedAmount"
|
||||
id="Monto total"
|
||||
type="text"
|
||||
class="text-lg font-semibold"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Botón Cobrar -->
|
||||
<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"
|
||||
class="w-full bg-green-700 hover:bg-green-600 text-white font-medium py-3.5 rounded-lg transition-colors"
|
||||
: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 caja
|
||||
Entregar Corte de Caja
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -26,7 +26,7 @@ const fineData = ref({
|
||||
/** Métodos */
|
||||
const handleSearch = async () => {
|
||||
if (!fineNumber.value.trim()) {
|
||||
window.Notify.warning("Por favor ingresa o escanea una multa");
|
||||
Notify.warning("Por favor ingresa o escanea una multa");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ const handleSearch = async () => {
|
||||
const model = data.model;
|
||||
|
||||
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 = {
|
||||
id: model.id,
|
||||
@ -76,7 +76,7 @@ const handleSearch = async () => {
|
||||
});
|
||||
},
|
||||
onFail: (error) => {
|
||||
window.Notify.error(error.message || "Error al buscar la multa");
|
||||
Notify.error(error.message || "Error al buscar la multa");
|
||||
fineData.value = {
|
||||
fecha: "",
|
||||
placa: "",
|
||||
@ -90,37 +90,37 @@ const handleSearch = async () => {
|
||||
},
|
||||
onError: (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 () => {
|
||||
if (!fineData.value.monto) {
|
||||
window.Notify.warning("No hay multa cargada para cobrar");
|
||||
Notify.warning("No hay multa cargada para cobrar");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fineData.value.isPaid) {
|
||||
window.Notify.warning("Esta multa ya ha sido pagada previamente");
|
||||
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");
|
||||
Notify.success("Multa cobrada exitosamente");
|
||||
|
||||
emit("payment-processed", { ...fineData.value, paymentData: data });
|
||||
|
||||
fineData.value.isPaid = true;
|
||||
},
|
||||
onFail: (error) => {
|
||||
window.Notify.error(
|
||||
Notify.error(
|
||||
error.message || "Error al procesar el pago de la multa"
|
||||
);
|
||||
},
|
||||
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");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,75 +1,221 @@
|
||||
<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 Selectable from "@Holos/Form/Selectable.vue";
|
||||
|
||||
/** Eventos */
|
||||
const emit = defineEmits(["membership-paid"]);
|
||||
|
||||
/** Instancias */
|
||||
const api = useApi();
|
||||
|
||||
/** Refs */
|
||||
const searchForm = ref({
|
||||
membershipNumber: "",
|
||||
curp: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
const curp = ref("");
|
||||
|
||||
const memberData = ref({
|
||||
image: null,
|
||||
name: "",
|
||||
id: null,
|
||||
curp: "",
|
||||
tutorName: "",
|
||||
name: "",
|
||||
tutor: null,
|
||||
photo: "",
|
||||
photo_url: "",
|
||||
memberships: [],
|
||||
});
|
||||
|
||||
const paymentData = ref({
|
||||
service: [],
|
||||
membershipId: null,
|
||||
quantity: 1,
|
||||
totalAmount: "",
|
||||
totalAmount: 0,
|
||||
});
|
||||
|
||||
const serviceOptions = ref([
|
||||
{ id: 1, description: "Membresía Mensual" },
|
||||
{ id: 2, description: "Membresía Trimestral" },
|
||||
{ id: 3, description: "Membresía Semestral" },
|
||||
{ id: 4, description: "Membresía Anual" },
|
||||
]);
|
||||
/** Computed */
|
||||
const activeMembership = computed(() => {
|
||||
if (
|
||||
!memberData.value.memberships ||
|
||||
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 */
|
||||
const handleSearch = () => {
|
||||
if (!searchForm.value.membershipNumber && !searchForm.value.curp) {
|
||||
alert("Por favor ingresa al menos un criterio de búsqueda");
|
||||
const handleSearch = async () => {
|
||||
if (!curp.value.trim()) {
|
||||
Notify.warning("Por favor ingresa un CURP");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Buscando miembro:", searchForm.value);
|
||||
loading.value = true;
|
||||
|
||||
await api.get(apiURL("members/search"), {
|
||||
params: {
|
||||
curp: curp.value,
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
const model = response.model;
|
||||
|
||||
// Simular datos encontrados
|
||||
memberData.value = {
|
||||
name: "Juan Pérez García",
|
||||
curp: "PEGJ900101HDFRNN09",
|
||||
tutorName: "María López Hernández",
|
||||
image: null,
|
||||
};
|
||||
id: model.id,
|
||||
curp: model.curp,
|
||||
name: model.name,
|
||||
tutor: model.tutor,
|
||||
photo: model.photo,
|
||||
photo_url: model.photo_url,
|
||||
memberships: model.memberships || [],
|
||||
};
|
||||
|
||||
const handlePayment = () => {
|
||||
if (!paymentData.value.service) {
|
||||
alert("Por favor selecciona un servicio");
|
||||
return;
|
||||
if (memberData.value.memberships.length === 0) {
|
||||
Notify.warning("El miembro no tiene membresías activas");
|
||||
} else {
|
||||
Notify.success("Miembro encontrado");
|
||||
|
||||
// Pre-cargar datos del pago
|
||||
if (activeMembership.value) {
|
||||
paymentData.value.membershipId = activeMembership.value.id;
|
||||
calculateTotal();
|
||||
}
|
||||
|
||||
console.log("Procesando pago de membresía:", {
|
||||
member: memberData.value,
|
||||
payment: paymentData.value,
|
||||
}
|
||||
},
|
||||
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", {
|
||||
member: memberData.value,
|
||||
payment: paymentData.value,
|
||||
});
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
@ -79,36 +225,33 @@ const calculateTotal = () => {
|
||||
<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>
|
||||
<form @submit.prevent="handleSearch">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||
<!-- Número de Membresía -->
|
||||
<Input
|
||||
v-model="searchForm.membershipNumber"
|
||||
id="Número de Membresía"
|
||||
type="text"
|
||||
placeholder=""
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 items-end">
|
||||
<!-- CURP -->
|
||||
<Input
|
||||
v-model="searchForm.curp"
|
||||
v-model="curp"
|
||||
id="CURP"
|
||||
type="text"
|
||||
placeholder=""
|
||||
placeholder="Ej: AMWO0020923"
|
||||
:disabled="loading"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
: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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
Información del Miembro
|
||||
</h3>
|
||||
@ -120,9 +263,16 @@ const calculateTotal = () => {
|
||||
Fotografía
|
||||
</label>
|
||||
<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
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-24 w-24 text-gray-400"
|
||||
fill="none"
|
||||
@ -141,60 +291,154 @@ const calculateTotal = () => {
|
||||
|
||||
<!-- Datos del miembro -->
|
||||
<div class="space-y-5">
|
||||
<Input v-model="memberData.name" id="Nombre" type="text" disabled />
|
||||
|
||||
<Input v-model="memberData.curp" id="CURP" type="text" disabled />
|
||||
<Input
|
||||
:model-value="memberData.name"
|
||||
id="Nombre"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Input
|
||||
v-model="memberData.tutorName"
|
||||
:model-value="memberData.curp"
|
||||
id="CURP"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Input
|
||||
:model-value="memberData.tutor || 'Sin tutor'"
|
||||
id="Nombre Tutor"
|
||||
type="text"
|
||||
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>
|
||||
|
||||
<!-- 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">
|
||||
Realizar Cobro de Membresía
|
||||
</h3>
|
||||
|
||||
<form @submit.prevent="handlePayment">
|
||||
<div class="grid grid-cols-3 gap-4 mb-6">
|
||||
<!-- Servicio a Pagar -->
|
||||
<Selectable
|
||||
v-model="paymentData.service"
|
||||
label="description"
|
||||
title="Servicio a pagar"
|
||||
:options="serviceOptions"
|
||||
<!-- Servicio -->
|
||||
<div>
|
||||
<Input
|
||||
:model-value="activeMembership.charge_concept.name"
|
||||
id="Servicio"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Costo unitario: ${{
|
||||
activeMembership.charge_concept.unit_cost_peso
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Cantidad -->
|
||||
<Input
|
||||
v-model="paymentData.quantity"
|
||||
id="Cantidad (días)"
|
||||
v-model.number="paymentData.quantity"
|
||||
id="Cantidad"
|
||||
type="number"
|
||||
min="1"
|
||||
:disabled="loading"
|
||||
@input="calculateTotal"
|
||||
/>
|
||||
|
||||
<!-- Monto Total -->
|
||||
<Input
|
||||
v-model="paymentData.totalAmount"
|
||||
:model-value="formattedAmount"
|
||||
id="Monto Total"
|
||||
type="text"
|
||||
disabled
|
||||
/>
|
||||
</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
|
||||
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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -3,14 +3,14 @@ import { ref } from 'vue';
|
||||
import { QrcodeStream } from 'vue-qrcode-reader';
|
||||
|
||||
/** Props y Emits */
|
||||
defineProps({
|
||||
fineNumber: {
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:fineNumber', 'fine-searched']);
|
||||
const emit = defineEmits(['update:modelValue', 'qr-detected']);
|
||||
|
||||
/** Refs */
|
||||
const error = ref('');
|
||||
@ -34,13 +34,13 @@ function onDetect(detectedCodes) {
|
||||
const code = detectedCodes[0].rawValue;
|
||||
|
||||
// Emitir el código escaneado al componente padre
|
||||
emit('update:fineNumber', code);
|
||||
emit('update:modelValue', code);
|
||||
|
||||
// Pausar el escaneo después de detectar
|
||||
isScanning.value = false;
|
||||
|
||||
// Emitir evento para buscar la multa
|
||||
emit('fine-searched');
|
||||
// Emitir evento con el código detectado
|
||||
emit('qr-detected', code);
|
||||
|
||||
// Reactivar el escaneo después de 2 segundos
|
||||
setTimeout(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user