FIX:Intefaz de cobro de multa

This commit is contained in:
Rubi Almora 2026-03-26 11:34:34 -06:00
parent f53d0ff457
commit 04fcb9df08

View File

@ -1,268 +1,520 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref, computed } from "vue";
import { useApi, apiURL } from "@/services/Api.js"; import { useApi, apiURL } from "@/services/Api.js";
import Input from "@Holos/Form/Input.vue";
import QRscan from "./QRscan.vue"; import QRscan from "./QRscan.vue";
/** Eventos */ const emit = defineEmits(["fine-searched", "payment-processed"]);
const emit = defineEmits(["fine-searched"]);
const api = useApi(); const api = useApi();
/** Refs */ // Pestañas
const fineNumber = ref(""); const tabs = [
const fineData = ref({ { id: "qr", label: "Escanear Boleta" },
fecha: "", { id: "folio", label: "Buscar por Folio" },
placa: "", { id: "curp", label: "Buscar por CURP" },
vin: "", ];
licencia: "", const activeTab = ref("qr");
tarjeta: "",
rfc: "",
nombre: "",
monto: "",
});
/** Métodos */ // Estado de búsqueda
const handleSearch = async () => { const folioQuery = ref("");
if (!fineNumber.value.trim()) { const curpQuery = ref("");
Notify.warning("Por favor ingresa o escanea una multa"); const isSearching = ref(false);
const searchResults = ref([]);
const selectedFines = ref([]);
const searchMessage = ref("");
// Simulador QR
const showSimulator = ref(false);
const simulatorToken = ref("");
// Computed
const totalSelected = computed(() =>
selectedFines.value.reduce((sum, f) => sum + parseFloat(f.total_amount || 0), 0)
);
const hasSelection = computed(() => selectedFines.value.length > 0);
// Helpers
const formatDate = (dateStr) => {
if (!dateStr) return "-";
return new Date(dateStr).toLocaleDateString("es-MX");
};
const getConcepts = (fine) => {
if (!fine.charge_concepts?.length) return [];
return fine.charge_concepts.map((c) => c.short_name || c.name || "").filter(Boolean);
};
const isFineSelected = (fine) => selectedFines.value.some((f) => f.id === fine.id);
// Procesado de respuestas
const processSingleResult = (data) => {
const fine = data.model;
searchResults.value = [fine];
if (fine.status === "paid") {
searchMessage.value = "Se encontró 1 multa (ya pagada).";
selectedFines.value = [];
Notify.info("La multa ya ha sido pagada previamente");
} else {
searchMessage.value = "Se encontraron 1 infracciones pendientes.";
selectedFines.value = [fine];
emit("fine-searched", { rawData: data });
}
};
const processMultipleResults = (data) => {
const fines = data.models || [];
searchResults.value = fines;
selectedFines.value = [];
const pending = fines.filter((f) => f.status !== "paid");
searchMessage.value =
pending.length > 0
? `Se encontraron ${pending.length} infracciones pendientes.`
: "No se encontraron infracciones pendientes.";
};
// Búsquedas
const searchByQR = async (qrToken) => {
if (!qrToken?.trim()) return;
isSearching.value = true;
searchResults.value = [];
selectedFines.value = [];
await api.get(apiURL("fines/search"), {
params: { qr_token: qrToken.trim() },
onSuccess: processSingleResult,
onFail: (e) => Notify.error(e.message || "Error al buscar la multa"),
onError: () => Notify.error("Ocurrió un error al buscar la multa"),
});
isSearching.value = false;
};
const searchByFolio = async () => {
if (!folioQuery.value.trim()) {
Notify.warning("Por favor ingresa un folio");
return; return;
} }
isSearching.value = true;
await api.get(apiURL(`fines/search`), { searchResults.value = [];
params: { selectedFines.value = [];
qr_token: fineNumber.value.trim(), await api.get(apiURL("fines/search"), {
}, params: { id: folioQuery.value.trim() },
onSuccess: (data) => { onSuccess: processSingleResult,
const model = data.model; onFail: (e) => Notify.error(e.message || "Error al buscar la multa"),
onError: () => Notify.error("Ocurrió un error al buscar la multa"),
if (model.status === "paid") {
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,
};
return;
}
// Multa pendiente
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: false,
};
emit("fine-searched", {
...fineData.value,
rawData: data,
});
},
onFail: (error) => {
Notify.error(error.message || "Error al buscar la multa");
fineData.value = {
fecha: "",
placa: "",
vin: "",
licencia: "",
tarjeta: "",
rfc: "",
nombre: "",
monto: "",
};
},
onError: (error) => {
console.error("Error al buscar multa:", error);
Notify.error("Ocurrió un error al buscar la multa");
},
}); });
isSearching.value = false;
}; };
const searchByCurp = async () => {
if (!curpQuery.value.trim()) {
Notify.warning("Por favor ingresa un CURP o nombre");
return;
}
isSearching.value = true;
searchResults.value = [];
selectedFines.value = [];
await api.get(apiURL("fines/search"), {
params: { curp: curpQuery.value.trim() },
onSuccess: processMultipleResults,
onFail: (e) => Notify.error(e.message || "Error al buscar multas"),
onError: () => Notify.error("Ocurrió un error al buscar multas"),
});
isSearching.value = false;
};
// Selección
const toggleFineSelection = (fine) => {
if (fine.status === "paid") return;
const idx = selectedFines.value.findIndex((f) => f.id === fine.id);
if (idx >= 0) selectedFines.value.splice(idx, 1);
else selectedFines.value.push(fine);
};
// QR Simulator
const handleQRDetected = (qrToken) => searchByQR(qrToken);
const simulateQRScan = () => {
if (!simulatorToken.value.trim()) {
Notify.warning("Ingresa un QR token para simular");
return;
}
searchByQR(simulatorToken.value.trim());
showSimulator.value = false;
simulatorToken.value = "";
};
// Cobro
const isProcessingPayment = ref(false);
const handlePayment = async () => { const handlePayment = async () => {
if (!fineData.value.monto) { if (!hasSelection.value) return;
Notify.warning("No hay multa cargada para cobrar"); isProcessingPayment.value = true;
return; let successCount = 0;
for (const fine of selectedFines.value) {
await api.post(apiURL(`fines/${fine.id}/mark-as-paid`), {
onSuccess: (data) => {
successCount++;
emit("payment-processed", { fine, paymentData: data });
},
onFail: (e) => Notify.error(e.message || "Error al procesar el pago"),
onError: () => Notify.error("Ocurrió un error al procesar el pago"),
});
} }
isProcessingPayment.value = false;
if (fineData.value.isPaid) { if (successCount > 0) {
Notify.warning("Esta multa ya ha sido pagada previamente"); Notify.success(
return; successCount === 1
? "Multa cobrada exitosamente"
: `${successCount} multas cobradas exitosamente`
);
searchResults.value = [];
selectedFines.value = [];
searchMessage.value = "";
} }
await api.post(apiURL(`fines/${fineData.value.id}/mark-as-paid`), {
onSuccess: (data) => {
Notify.success("Multa cobrada exitosamente");
emit("payment-processed", { ...fineData.value, paymentData: data });
fineData.value.isPaid = true;
},
onFail: (error) => {
Notify.error(
error.message || "Error al procesar el pago de la multa"
);
},
onError: (error) => {
Notify.error("Ocurrió un error al procesar el pago de la multa");
},
});
}; };
const clearForm = () => { // Cambio de pestaña
fineData.value = { const switchTab = (tab) => {
id: null, activeTab.value = tab;
fecha: "", searchResults.value = [];
placa: "", selectedFines.value = [];
vin: "", searchMessage.value = "";
licencia: "", folioQuery.value = "";
tarjeta: "", curpQuery.value = "";
rfc: "", showSimulator.value = false;
nombre: "", simulatorToken.value = "";
monto: "",
};
fineNumber.value = "";
}; };
</script> </script>
<template> <template>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6"> <div class="flex gap-5 p-5 min-h-[calc(100vh-80px)]">
<div class="bg-white rounded-xl p-6 shadow-lg">
<h3 class="text-xl font-semibold mb-4 text-gray-800">Buscar Multa</h3>
<div class="mb-6"> <!-- Panel izquierdo -->
<label class="block text-sm text-gray-600 font-medium mb-2"> <div class="flex-1 flex flex-col gap-4 min-w-0">
Escanear Código QR
</label>
<div
class="w-full h-80 bg-gray-800 rounded-lg flex flex-col items-center justify-center"
>
<!-- QR -->
<QRscan
v-model="fineNumber"
@qr-detected="handleSearch"
/>
</div>
</div>
<div class="mb-5"> <!-- Tarjeta de búsqueda -->
<Input <div class="bg-white rounded-2xl shadow-sm overflow-hidden">
v-model="fineNumber"
id="Número de Multa"
type="text"
placeholder="Ingrese el número de multa"
@keyup.enter="handleSearch"
/>
</div>
<button <!-- Pestañas -->
@click="handleSearch" <div class="flex border-b border-gray-200">
type="button" <button
class="w-full bg-[#7a0b3a] hover:bg-[#68082e] text-white font-medium py-3.5 rounded-lg transition-colors" v-for="tab in tabs"
> :key="tab.id"
Buscar @click="switchTab(tab.id)"
</button> :class="[
</div> 'flex items-center gap-2 px-6 py-4 text-sm font-medium transition-colors border-b-2 -mb-px',
<div class="bg-white rounded-xl p-6 shadow-lg"> activeTab === tab.id
<h3 class="text-xl font-semibold mb-4 text-gray-800"> ? 'border-blue-600 text-blue-600 bg-white'
Detalles de la Multa : 'border-transparent text-gray-500 hover:text-gray-700 hover:bg-gray-50',
</h3> ]"
>
<form @submit.prevent="handlePayment"> <!-- Ícono QR -->
<!-- Fecha y Placa --> <svg v-if="tab.id === 'qr'" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<div class="grid grid-cols-2 gap-4 mb-5"> <rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="3" y="14" width="7" height="7" />
<Input <rect x="5" y="5" width="3" height="3" fill="currentColor" stroke="none" /><rect x="16" y="5" width="3" height="3" fill="currentColor" stroke="none" /><rect x="5" y="16" width="3" height="3" fill="currentColor" stroke="none" />
v-model="fineData.fecha" <path d="M14 14h3v3h-3zM17 17h3v3h-3zM14 20h3" />
id="Fecha de Multa" </svg>
type="text" <!-- Ícono Documento -->
disabled <svg v-else-if="tab.id === 'folio'" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14,2 14,8 20,8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10,9 9,9 8,9" />
<Input v-model="fineData.placa" id="Placa" type="text" disabled /> </svg>
<!-- Ícono Persona -->
<svg v-else xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
</svg>
{{ tab.label }}
</button>
</div> </div>
<!-- VIN y Licencia --> <!-- Contenido: Escanear QR -->
<div class="grid grid-cols-2 gap-4 mb-5"> <div v-if="activeTab === 'qr'" class="p-6">
<Input v-model="fineData.vin" id="VIN" type="text" disabled /> <!-- Escáner de cámara real -->
<Input <div class="w-full h-72 bg-gray-900 rounded-2xl overflow-hidden mb-4">
v-model="fineData.licencia" <QRscan @qr-detected="handleQRDetected" />
id="Licencia" </div>
type="text"
disabled
/>
</div>
<!-- Tarjeta de Circulación y RFC --> <p class="text-gray-500 text-sm text-center mb-4">
<div class="grid grid-cols-2 gap-4 mb-5"> Posicione el código QR de la boleta física de infracción frente al lector.
<Input </p>
v-model="fineData.tarjeta"
id="Tarjeta de Circulación"
type="text"
disabled
/>
<Input v-model="fineData.rfc" id="RFC" type="text" disabled />
</div>
<!-- Nombre --> <!-- Botón simulador -->
<div class="mb-5"> <div class="flex justify-center mb-2">
<Input v-model="fineData.nombre" id="Nombre" type="text" disabled />
</div>
<!-- Línea separadora -->
<hr class="my-6 border-gray-300" />
<!-- Monto a Pagar -->
<div class="mb-5">
<Input
v-model="fineData.monto"
id="Monto a Pagar"
type="text"
disabled
class="text-lg font-semibold"
/>
</div>
<div class="space-y-6">
<div v-if="fineData.isPaid">
<button <button
@click="showSimulator = !showSimulator"
type="button" type="button"
@click="clearForm" class="px-6 py-2.5 bg-gray-900 hover:bg-gray-700 text-white text-sm font-medium rounded-lg transition-colors"
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 otra multa Simular Escaneo de Lector
</button> </button>
</div> </div>
<!-- Botón Cobrar --> <!-- Input simulador -->
<div> <div v-if="showSimulator" class="mt-3 flex gap-2">
<div class="flex-1 flex items-center gap-2 border border-gray-300 rounded-lg px-3 py-2.5 bg-white focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
v-model="simulatorToken"
@keyup.enter="simulateQRScan"
type="text"
placeholder="Ingresa el QR token a simular..."
class="flex-1 text-sm outline-none bg-transparent text-gray-800 placeholder-gray-400"
/>
</div>
<button <button
type="submit" @click="simulateQRScan"
:disabled="fineData.isPaid" :disabled="isSearching"
:class="[ type="button"
'w-full font-medium py-3.5 rounded-lg transition-colors', class="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors"
fineData.isPaid
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-700 hover:bg-green-600 text-white',
]"
> >
Cobrar {{ isSearching ? "..." : "Simular" }}
</button> </button>
</div> </div>
</div> </div>
</form>
<!-- Contenido: Buscar por Folio -->
<div v-else-if="activeTab === 'folio'" class="p-6">
<div class="flex gap-2">
<div class="flex-1 flex items-center gap-2 border border-gray-300 rounded-lg px-3 py-2.5 bg-white focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
v-model="folioQuery"
@keyup.enter="searchByFolio"
type="text"
placeholder="Número de folio..."
class="flex-1 text-sm outline-none bg-transparent text-gray-800 placeholder-gray-400"
/>
</div>
<button
@click="searchByFolio"
:disabled="isSearching"
type="button"
class="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors min-w-[90px]"
>
{{ isSearching ? "Buscando..." : "Buscar" }}
</button>
</div>
</div>
<!-- Contenido: Buscar por CURP / Nombre -->
<div v-else class="p-6">
<div class="flex gap-2">
<div class="flex-1 flex items-center gap-2 border border-gray-300 rounded-lg px-3 py-2.5 bg-white focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500 transition-all">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
v-model="curpQuery"
@keyup.enter="searchByCurp"
type="text"
placeholder="CURP o nombre del infractor..."
class="flex-1 text-sm outline-none bg-transparent text-gray-800 placeholder-gray-400"
/>
</div>
<button
@click="searchByCurp"
:disabled="isSearching"
type="button"
class="px-6 py-2.5 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white text-sm font-semibold rounded-lg transition-colors min-w-[90px]"
>
{{ isSearching ? "Buscando..." : "Buscar" }}
</button>
</div>
</div>
</div>
<!-- Resultados de búsqueda -->
<div v-if="searchResults.length > 0" class="bg-white rounded-2xl shadow-sm p-6">
<h3 class="font-semibold text-gray-800 mb-0.5">Resultados de la búsqueda</h3>
<p class="text-sm text-gray-400 mb-4">{{ searchMessage }}</p>
<div class="space-y-3">
<div
v-for="fine in searchResults"
:key="fine.id"
@click="activeTab === 'curp' ? toggleFineSelection(fine) : null"
:class="[
'rounded-xl border-2 p-4 transition-all',
activeTab === 'curp' && fine.status !== 'paid' ? 'cursor-pointer' : '',
isFineSelected(fine)
? 'border-blue-500 bg-blue-50/20'
: 'border-gray-200 hover:border-gray-300',
fine.status === 'paid' ? 'opacity-60' : '',
]"
>
<!-- Encabezado de tarjeta -->
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<!-- Indicador de selección -->
<div
:class="[
'w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-colors',
isFineSelected(fine)
? 'border-blue-500 bg-blue-500'
: 'border-gray-300 bg-white',
]"
>
<div v-if="isFineSelected(fine)" class="w-2 h-2 bg-white rounded-full" />
</div>
<span class="font-semibold text-gray-800">Folio: {{ fine.id }}</span>
<span
v-if="fine.status === 'paid'"
class="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full"
>
Pagada
</span>
</div>
<span class="font-semibold text-gray-800 text-base">
${{ parseFloat(fine.total_amount || 0).toFixed(2) }}
</span>
</div>
<!-- Grid de datos -->
<div class="grid grid-cols-2 gap-x-6 gap-y-1.5 text-sm mb-2">
<!-- Nombre -->
<div class="flex items-center gap-2 text-gray-600 min-w-0">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
<span class="truncate">{{ fine.name || "-" }}</span>
</div>
<!-- Fecha -->
<div class="flex items-center gap-2 text-gray-600">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<span>{{ formatDate(fine.created_at) }}</span>
</div>
<!-- CURP -->
<div class="flex items-center gap-2 text-gray-600 min-w-0 col-span-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14,2 14,8 20,8"/>
</svg>
<span class="truncate">CURP: {{ fine.curp || "-" }}</span>
</div>
</div>
<!-- Vehículo -->
<div v-if="fine.plate || fine.vin" class="flex items-center gap-2 text-sm text-gray-600 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-gray-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path d="M5 17H3a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v9a2 2 0 0 1-2 2h-2"/><circle cx="7.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/>
</svg>
<span class="text-gray-500">
{{ [fine.plate ? `Placas: ${fine.plate}` : null, fine.vin ? `VIN: ${fine.vin}` : null].filter(Boolean).join(" · ") }}
</span>
</div>
<!-- Conceptos de infracción -->
<div v-if="getConcepts(fine).length" class="pt-2 border-t border-gray-100">
<div class="flex items-start gap-2 mb-1">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5 text-red-400 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<span class="text-xs font-medium text-red-500 uppercase tracking-wide">
{{ getConcepts(fine).length === 1 ? "Infracción" : "Infracciones" }}
</span>
</div>
<ul class="space-y-1 pl-5">
<li
v-for="(concept, i) in getConcepts(fine)"
:key="i"
class="text-sm text-red-500 flex items-start gap-1.5"
>
<span class="text-red-300 mt-1 shrink-0"></span>
<span>{{ concept }}</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Panel derecho: Resumen de Cobro -->
<div class="w-72 shrink-0 bg-[#1a1f2e] rounded-2xl p-5 flex flex-col text-white self-start sticky top-5">
<!-- Título -->
<div class="flex items-center gap-2 font-semibold text-base mb-5">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
</svg>
Resumen de Cobro
</div>
<!-- Estado vacío -->
<div
v-if="!hasSelection"
class="flex flex-col items-center justify-center py-10 gap-3 text-gray-500"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-12 h-12 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
</svg>
<p class="text-center text-xs text-gray-500 leading-relaxed">
Seleccione al menos una infracción para procesar el cobro.
</p>
</div>
<!-- Multas seleccionadas -->
<div v-else class="flex-1 mb-4">
<p class="text-xs text-gray-400 mb-2">
Infracciones seleccionadas ({{ selectedFines.length }})
</p>
<div class="space-y-2">
<div
v-for="fine in selectedFines"
:key="fine.id"
class="bg-[#252b3b] rounded-xl p-3"
>
<div class="flex justify-between items-start mb-2">
<span class="text-sm font-medium text-white">Folio: {{ fine.id }}</span>
<span class="text-sm font-semibold text-white shrink-0 ml-2">
${{ parseFloat(fine.total_amount || 0).toFixed(2) }}
</span>
</div>
<ul class="space-y-1">
<li
v-for="(concept, i) in getConcepts(fine)"
:key="i"
class="flex items-start gap-1.5 text-xs text-gray-400"
>
<span class="text-gray-600 shrink-0 mt-0.5"></span>
<span>{{ concept }}</span>
</li>
<li v-if="!getConcepts(fine).length" class="text-xs text-gray-500 italic">
Sin concepto registrado
</li>
</ul>
</div>
</div>
</div>
<!-- Totales y botón -->
<div class="mt-auto pt-4 border-t border-gray-700">
<div class="flex justify-between text-sm text-gray-400 mb-2">
<span>Subtotal</span>
<span>${{ totalSelected.toFixed(2) }}</span>
</div>
<div class="flex justify-between font-bold text-white text-base mb-4">
<span>Total</span>
<span>${{ totalSelected.toFixed(2) }}</span>
</div>
<button
@click="handlePayment"
:disabled="!hasSelection || isProcessingPayment"
:class="[
'w-full py-3 rounded-xl font-semibold text-sm flex items-center justify-center gap-2 transition-all',
hasSelection && !isProcessingPayment
? 'bg-green-500 hover:bg-green-600 text-white shadow-lg shadow-green-500/20'
: 'bg-[#252b3b] text-gray-500 cursor-not-allowed',
]"
>
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<rect x="2" y="5" width="20" height="14" rx="2"/><line x1="2" y1="10" x2="22" y2="10"/>
</svg>
{{ isProcessingPayment ? "Procesando..." : "Cobrar Total" }}
</button>
</div>
</div> </div>
</div> </div>
</template> </template>