207 lines
7.4 KiB
Vue
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>
|