MOD: agregar gestión de visibilidad en el formulario de entrega y mejorar la interfaz de usuario en la sección de checkout

This commit is contained in:
Juan Felipe Zapata Moreno 2025-12-22 16:15:12 -06:00
parent b9d2f69390
commit 523b0972d6
6 changed files with 289 additions and 14 deletions

View File

@ -10,6 +10,7 @@ services:
- /var/www/comal-pagos.frontend/node_modules - /var/www/comal-pagos.frontend/node_modules
networks: networks:
- comal-pagos-network - comal-pagos-network
mem_limit: 512m
networks: networks:
comal-pagos-network: comal-pagos-network:

View File

@ -11,6 +11,9 @@ const emit = defineEmits(["cash-cut-found", "cash-cut-delivered"]);
/** Instancias */ /** Instancias */
const api = useApi(); const api = useApi();
/** Control de visibilidad del formulario */
const isFormOpen = ref(false);
/** Refs */ /** Refs */
const qr_token = ref(""); const qr_token = ref("");
const loading = ref(false); const loading = ref(false);
@ -181,12 +184,33 @@ const clearForm = () => {
</script> </script>
<template> <template>
<div class="space-y-6 p-6"> <div class="mx-4 mt-4 mb-6">
<div class="bg-white rounded-xl p-6 shadow-lg"> <!-- Botón para abrir el formulario -->
<h3 class="text-xl font-semibold mb-4 text-gray-800"> <button
Buscar Corte de Caja v-if="!isFormOpen"
</h3> @click="isFormOpen = true"
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> class="w-full bg-primary hover:bg-primary/90 text-white font-semibold py-3 px-6 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg flex items-center justify-center space-x-2"
>
<span class="text-2xl">+</span>
<span>Buscar y Entregar Corte de Caja</span>
</button>
<!-- Formulario expandible -->
<div v-if="isFormOpen" class="space-y-6">
<div class="bg-white rounded-xl p-6 shadow-lg">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800">
Buscar Corte de Caja
</h3>
<button
@click="isFormOpen = false"
class="text-gray-400 hover:text-gray-600 transition-colors"
title="Cerrar"
>
<span class="text-2xl">&times;</span>
</button>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Scanner QR --> <!-- 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">
@ -279,7 +303,8 @@ const clearForm = () => {
<!-- Botón recibir --> <!-- Botón recibir -->
<div> <div>
<button <button
type="submit" type="button"
@click="handleDelivery"
:disabled="cashCutData.isReceived" :disabled="cashCutData.isReceived"
:class="[ :class="[
'w-full font-medium py-3.5 rounded-lg transition-colors', 'w-full font-medium py-3.5 rounded-lg transition-colors',
@ -292,6 +317,7 @@ const clearForm = () => {
</button> </button>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -23,8 +23,8 @@ function paintBoundingBox(detectedCodes, ctx) {
boundingBox: { x, y, width, height } boundingBox: { x, y, width, height }
} = detectedCode; } = detectedCode;
ctx.lineWidth = 2; ctx.lineWidth = 4;
ctx.strokeStyle = '#7a0b3a'; // Color del tema ctx.strokeStyle = '#10b981';
ctx.strokeRect(x, y, width, height); ctx.strokeRect(x, y, width, height);
} }
} }

View File

@ -216,6 +216,15 @@ export default {
confirm:'Confirmar', confirm:'Confirmar',
copyright:'Todos los derechos reservados.', copyright:'Todos los derechos reservados.',
contact:'Contacto', contact:'Contacto',
checkout: {
concept: "Concepto",
amount: "Monto total",
date: "Fecha",
user: "Usuario",
list: {
empty: "No hay cortes de caja registrados"
}
},
create: 'Crear', create: 'Crear',
created: 'Registro creado', created: 'Registro creado',
created_at: 'Fecha creación', created_at: 'Fecha creación',

View File

@ -1,9 +1,227 @@
<script setup> <script setup>
import PageHeader from "@Holos/PageHeader.vue"; import { onMounted, reactive, ref } from "vue";
import Checkout from '@App/CheckoutDelivery.vue'; import { useSearcher, apiURL } from "@Services/Api";
import { transl } from "./Module";
import PageHeader from "@Holos/PageHeader.vue";
import Checkout from "@App/CheckoutDelivery.vue";
import Table from "@Holos/Table.vue";
const models = ref({
data: [],
total: 0,
});
const filters = reactive({
type: "",
});
const expandedRows = ref(new Set());
const toggleRow = (id) => {
if (expandedRows.value.has(id)) {
expandedRows.value.delete(id);
} else {
expandedRows.value.add(id);
}
};
const isRowExpanded = (id) => {
return expandedRows.value.has(id);
};
const getConceptsSummary = (model) => {
if (!model.details || model.details.length === 0) return "-";
const allConcepts = model.details
.flatMap(
(detail) =>
detail.payment?.details?.map((pd) => pd.charge_concept?.name) || []
)
.filter(Boolean);
const uniqueConcepts = [...new Set(allConcepts)];
if (uniqueConcepts.length === 0) return "-";
if (uniqueConcepts.length === 1) return uniqueConcepts[0];
return `${uniqueConcepts[0]} y ${uniqueConcepts.length - 1} más...`;
};
const searcher = useSearcher({
url: apiURL("cash-cuts"),
filters,
onSuccess: (data) => {
models.value = data.models || { data: [] };
},
});
onMounted(() => {
searcher.search();
});
</script> </script>
<template> <template>
<PageHeader title="Entrega de Caja" /> <PageHeader title="Entrega de Caja" />
<Checkout /> <Checkout />
</template> <div class="mx-4 mb-4">
<div class="bg-white rounded-lg shadow-md p-4 mb-4">
<div class="flex items-center gap-4">
<label class="text-sm font-medium text-gray-700"
>Filtrar por tipo:</label
>
<select
v-model="filters.type"
@change="searcher.search()"
class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Todos</option>
<option value="membership">Membresías</option>
<option value="fine">Multas</option>
</select>
</div>
</div>
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<Table
:items="models"
:processing="searcher.processing"
@send-pagination="(page) => searcher.pagination(page)"
>
<template #head>
<th
class="px-6 py-3 text-left text-sm font-semibold"
v-text="transl('concept')"
/>
<th
class="px-6 py-3 text-left text-sm font-semibold"
v-text="transl('amount')"
/>
<th
class="px-6 py-3 text-left text-sm font-semibold"
v-text="transl('user')"
/>
<th
class="px-6 py-3 text-left text-sm font-semibold"
v-text="transl('date')"
/>
</template>
<template #body="{ items }">
<template v-for="model in items" :key="model.id">
<!-- Fila principal -->
<tr
class="border-b border-gray-200 hover:bg-gray-50 transition-colors cursor-pointer"
@click="toggleRow(model.id)"
>
<td class="px-6 py-4 text-sm text-gray-700">
<div class="flex items-center gap-2">
<svg
class="w-4 h-4 transition-transform duration-200"
:class="{ 'rotate-90': isRowExpanded(model.id) }"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<span>{{ getConceptsSummary(model) }}</span>
</div>
</td>
<td class="px-6 py-4 text-sm text-gray-700">
{{ model.closed_by?.full_name || "-" }}
</td>
<td class="px-6 py-4 text-sm text-gray-700">
${{ parseFloat(model.total_amount || 0).toFixed(2) }}
</td>
<td class="px-6 py-4 text-sm text-gray-700">
{{
model.end_at
? new Date(model.end_at).toLocaleDateString()
: "-"
}}
</td>
</tr>
<!-- Fila expandida con detalles -->
<tr v-if="isRowExpanded(model.id)" class="bg-gray-50">
<td colspan="4" class="px-6 py-4">
<div
class="bg-white rounded-lg shadow-sm p-4 border border-gray-200"
>
<h4 class="font-semibold text-gray-800 mb-3">
Detalle del corte de caja
</h4>
<div
v-if="model.details && model.details.length > 0"
class="space-y-3"
>
<div
v-for="detail in model.details"
:key="detail.id"
class="border-b border-gray-200 pb-3 last:border-b-0"
>
<div
v-for="paymentDetail in detail.payment?.details || []"
:key="paymentDetail.id"
class="flex justify-between items-center py-2"
>
<div class="flex-1">
<p class="font-medium text-gray-700">
{{ paymentDetail.charge_concept?.name || "-" }}
</p>
<p class="text-xs text-gray-500">
Cantidad: {{ paymentDetail.quantity || 1 }}
</p>
</div>
<div class="text-right">
<p class="font-semibold text-gray-800">
${{
parseFloat(paymentDetail.amount || 0).toFixed(2)
}}
</p>
</div>
</div>
</div>
<div
class="flex justify-between items-center pt-3 border-t-2 border-gray-300"
>
<div>
<p class="font-bold text-gray-800">Total del corte</p>
<p class="text-xs text-gray-500">
Fecha:
{{
model.end_at
? new Date(model.end_at).toLocaleDateString()
: "-"
}}
</p>
</div>
<p class="text-xl font-bold text-blue-600">
${{ parseFloat(model.total_amount || 0).toFixed(2) }}
</p>
</div>
</div>
<div v-else class="text-center text-gray-500 py-4">
No hay detalles disponibles
</div>
</div>
</td>
</tr>
</template>
</template>
<template #empty>
<td colspan="4" class="px-6 py-8 text-center text-gray-500 text-sm">
{{ transl("list.empty") }}
</td>
</template>
</Table>
</div>
</div>
</template>

View File

@ -0,0 +1,21 @@
import { lang } from '@Lang/i18n';
import { hasPermission } from '@Plugins/RolePermission.js';
// Ruta API
const apiTo = (name, params = {}) => route(`checkout.${name}`, params)
// Ruta visual
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `checkout.${name}`, params, query })
// Obtener traducción del componente
const transl = (str) => lang(`checkout.${str}`)
// Determina si un usuario puede hacer algo no en base a los permisos
const can = (permission) => hasPermission(`checkout.${permission}`)
export {
can,
viewTo,
apiTo,
transl
}