207 lines
7.4 KiB
Vue

<script setup>
import { ref, onMounted, watch } from 'vue';
import { useSearcher, useApi } from '@Services/Api';
import { apiTo } from './Module';
import Table from '@Holos/Table.vue';
import IconButton from '@Holos/Button/Icon.vue';
import PageHeader from '@Holos/PageHeader.vue';
import ShowModal from './Modal/Show.vue';
import ModalController from '@Controllers/ModalController.js';
/** Controladores */
const Modal = new ModalController();
const api = useApi();
const showModal = ref(Modal.showModal);
const modelModal = ref(Modal.modelModal);
/** Estado */
const models = ref({ data: [], total: 0, current_page: 1, last_page: 1 });
const statusFilter = ref('');
const statusConfig = {
pending: { label: 'Pendiente', cls: 'bg-yellow-100 text-yellow-800 border-yellow-200' },
processing: { label: 'En proceso', cls: 'bg-blue-100 text-blue-800 border-blue-200' },
completed: { label: 'Completada', cls: 'bg-green-100 text-green-800 border-green-200' },
rejected: { label: 'Rechazada', cls: 'bg-red-100 text-red-800 border-red-200' },
};
const statusTabs = [
{ value: '', label: 'Todas' },
{ value: 'pending', label: 'Pendientes' },
{ value: 'processing', label: 'En proceso' },
{ value: 'completed', label: 'Completadas'},
{ value: 'rejected', label: 'Rechazadas' },
];
/** Buscador */
const searcher = useSearcher({
url: apiTo(),
filters: {},
onSuccess: (data) => {
models.value = data.models;
},
});
const doSearch = (page = 1) => {
const filters = { page };
if (statusFilter.value) filters.status = statusFilter.value;
searcher.search('', filters);
};
onMounted(() => doSearch());
watch(statusFilter, () => doSearch(1));
/** Paginación */
const pages = () => {
const pages = [];
for (let i = 1; i <= models.value.last_page; i++) {
pages.push(i);
}
return pages;
};
/** Abrir detalle — carga info completa */
const openDetail = (model) => {
Modal.switchShowModal(model);
api.get(apiTo(model.id), {
onSuccess: (data) => {
Modal.modelModal.value = data.model;
}
});
};
/** Actualiza fila en la lista al recibir cambio del modal */
const handleUpdated = (updatedModel) => {
const idx = models.value.data.findIndex(m => m.id === updatedModel.id);
if (idx > -1) {
models.value.data[idx] = updatedModel;
}
};
const formatDate = (iso) => {
if (!iso) return '—';
return new Date(iso).toLocaleDateString('es-MX', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
};
const formatAmount = (amount) => {
if (!amount) return '—';
return new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format(amount);
};
</script>
<template>
<PageHeader title="Solicitudes de Factura" />
<div class="mx-4 mb-4 space-y-4">
<!-- Filtros de estado -->
<div class="flex flex-wrap gap-2">
<button
v-for="tab in statusTabs"
:key="tab.value"
class="px-4 py-1.5 rounded-full text-sm font-medium border transition-colors"
:class="statusFilter === tab.value
? 'bg-primary text-white border-primary'
: 'bg-white text-gray-600 border-gray-300 hover:border-primary hover:text-primary'"
@click="statusFilter = tab.value"
>
{{ tab.label }}
</button>
</div>
<!-- Tabla -->
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<Table
:items="models"
:processing="searcher.processing"
>
<template #head>
<th class="px-4 py-3 text-left text-sm font-semibold">#</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Ciudadano</th>
<th class="px-4 py-3 text-left text-sm font-semibold">RFC</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Monto pagado</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Estado</th>
<th class="px-4 py-3 text-left text-sm font-semibold">Fecha solicitud</th>
<th class="px-4 py-3 text-center text-sm font-semibold w-20">Acción</th>
</template>
<template #body="{ items }">
<tr
v-for="model in items"
:key="model.id"
class="border-b border-gray-100 hover:bg-gray-50 transition-colors cursor-pointer"
@click="openDetail(model)"
>
<td class="px-4 py-3 text-sm text-gray-500 font-mono">{{ model.id }}</td>
<td class="px-4 py-3 text-sm text-gray-800 font-medium">
{{ model.name || model.payment?.model?.name || '—' }}
</td>
<td class="px-4 py-3 text-sm text-gray-700 font-mono">{{ model.rfc }}</td>
<td class="px-4 py-3 text-sm text-gray-700">
{{ formatAmount(model.payment?.total_amount) }}
</td>
<td class="px-4 py-3">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border"
:class="statusConfig[model.status]?.cls ?? 'bg-gray-100 text-gray-700'"
>
{{ statusConfig[model.status]?.label ?? model.status }}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-600">
{{ formatDate(model.request_at) }}
</td>
<td class="px-4 py-3 flex justify-center" @click.stop>
<IconButton
icon="visibility"
title="Ver detalle"
outline
@click="openDetail(model)"
/>
</td>
</tr>
</template>
<template #empty>
<td colspan="7" class="px-6 py-10 text-center text-gray-400 text-sm">
No hay solicitudes de factura con los filtros seleccionados.
</td>
</template>
</Table>
</div>
<!-- Paginación personalizada -->
<div
v-if="models.last_page > 1"
class="flex justify-end flex-wrap gap-1"
>
<button
v-for="p in pages()"
:key="p"
class="px-3 py-1 text-sm border rounded transition-colors"
:class="models.current_page === p
? 'bg-primary text-white border-primary'
: 'bg-white text-gray-600 border-gray-300 hover:border-primary'"
@click="doSearch(p)"
>
{{ p }}
</button>
</div>
</div>
<!-- Modal de detalle -->
<ShowModal
:show="showModal"
:model="modelModal"
:loading="api.processing"
@close="Modal.switchShowModal()"
@updated="handleUpdated"
/>
</template>