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>
|
<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>
|
||||||
|
|||||||
@ -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");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user