Justificacion y reportes
This commit is contained in:
parent
a24218187b
commit
c4179ef709
@ -211,6 +211,7 @@ export default {
|
|||||||
enabled:'Habilitado',
|
enabled:'Habilitado',
|
||||||
endDate:'Fecha Fin',
|
endDate:'Fecha Fin',
|
||||||
event:'Evento',
|
event:'Evento',
|
||||||
|
event_name: 'Nombre del evento',
|
||||||
files: {
|
files: {
|
||||||
excel: 'Archivo excel',
|
excel: 'Archivo excel',
|
||||||
select: 'Seleccionar archivo'
|
select: 'Seleccionar archivo'
|
||||||
@ -232,6 +233,7 @@ export default {
|
|||||||
icon:'Icono',
|
icon:'Icono',
|
||||||
import: 'Importar',
|
import: 'Importar',
|
||||||
items: 'Elementos',
|
items: 'Elementos',
|
||||||
|
location: 'Ubicación',
|
||||||
maternal:'Apellido materno',
|
maternal:'Apellido materno',
|
||||||
message:'Mensaje',
|
message:'Mensaje',
|
||||||
menu:'Menú',
|
menu:'Menú',
|
||||||
@ -249,6 +251,7 @@ export default {
|
|||||||
seeAll:'Ver todas',
|
seeAll:'Ver todas',
|
||||||
},
|
},
|
||||||
omitted:'Omitida',
|
omitted:'Omitida',
|
||||||
|
participants_count: 'Cantidad de participantes',
|
||||||
password:'Contraseña',
|
password:'Contraseña',
|
||||||
passwordConfirmation:'Confirmar contraseña',
|
passwordConfirmation:'Confirmar contraseña',
|
||||||
passwordCurrent:'Contraseña actual',
|
passwordCurrent:'Contraseña actual',
|
||||||
|
|||||||
@ -1,20 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { api } from '@Services/Api';
|
||||||
|
import { apiTo } from './Module';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import VueApexCharts from 'vue3-apexcharts';
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
|
||||||
// Datos del dashboard
|
// Datos del dashboard
|
||||||
const dashboardData = ref({
|
const dashboardData = ref({
|
||||||
annualBudget: 50000,
|
totalBudget: 0,
|
||||||
justifiedExpenses: 32500,
|
justifiedExpenses: 0,
|
||||||
remainingBalance: 17500,
|
remainingBalance: 0,
|
||||||
expenseDistribution: [
|
expenseDistribution: []
|
||||||
{ name: 'Catering', value: 45, color: '#3B82F6' },
|
|
||||||
{ name: 'Marketing', value: 25, color: '#EF4444' },
|
|
||||||
{ name: 'Viáticos', value: 15, color: '#F97316' },
|
|
||||||
{ name: 'Otros', value: 10, color: '#6B7280' },
|
|
||||||
{ name: 'Renta de Equipo', value: 5, color: '#10B981' }
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configuración del gráfico de pie
|
// Configuración del gráfico de pie
|
||||||
@ -85,15 +81,6 @@ const chartSeries = computed(() => {
|
|||||||
return dashboardData.value.expenseDistribution.map(item => item.value);
|
return dashboardData.value.expenseDistribution.map(item => item.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Función para formatear moneda
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return new Intl.NumberFormat('es-MX', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD',
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para calcular porcentaje
|
// Función para calcular porcentaje
|
||||||
const calculatePercentage = (value, total) => {
|
const calculatePercentage = (value, total) => {
|
||||||
@ -102,17 +89,20 @@ const calculatePercentage = (value, total) => {
|
|||||||
|
|
||||||
// Computed para el porcentaje de gastos
|
// Computed para el porcentaje de gastos
|
||||||
const expensesPercentage = computed(() => {
|
const expensesPercentage = computed(() => {
|
||||||
return calculatePercentage(dashboardData.value.justifiedExpenses, dashboardData.value.annualBudget);
|
return calculatePercentage(dashboardData.value.justifiedExpenses, dashboardData.value.totalBudget);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Computed para el porcentaje de saldo restante
|
// Computed para el porcentaje de saldo restante
|
||||||
const remainingPercentage = computed(() => {
|
const remainingPercentage = computed(() => {
|
||||||
return calculatePercentage(dashboardData.value.remainingBalance, dashboardData.value.annualBudget);
|
return calculatePercentage(dashboardData.value.remainingBalance, dashboardData.value.totalBudget);
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Aquí podrías cargar datos reales desde tu API
|
api.get(apiTo('dashboard'), {
|
||||||
console.log('Dashboard de eventos cargado');
|
onSuccess: (r) => {
|
||||||
|
dashboardData.value = r.dashboard;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -133,9 +123,9 @@ onMounted(() => {
|
|||||||
<div class="bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
<div class="bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Presupuesto Anual</h3>
|
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Total</h3>
|
||||||
<p class="mt-2 text-3xl font-bold text-blue-600 dark:text-blue-400">
|
<p class="mt-2 text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
{{ formatCurrency(dashboardData.annualBudget) }}
|
${{ dashboardData.totalBudget }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@ -152,7 +142,7 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Gastos Justificados</h3>
|
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Gastos Justificados</h3>
|
||||||
<p class="mt-2 text-3xl font-bold text-green-600 dark:text-green-400">
|
<p class="mt-2 text-3xl font-bold text-green-600 dark:text-green-400">
|
||||||
{{ formatCurrency(dashboardData.justifiedExpenses) }}
|
${{ dashboardData.justifiedExpenses }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-primary-dt/50">
|
<p class="mt-1 text-xs text-gray-500 dark:text-primary-dt/50">
|
||||||
{{ expensesPercentage }}% del presupuesto
|
{{ expensesPercentage }}% del presupuesto
|
||||||
@ -172,7 +162,7 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Saldo Restante</h3>
|
<h3 class="text-sm font-medium text-gray-500 dark:text-primary-dt/70">Saldo Restante</h3>
|
||||||
<p class="mt-2 text-3xl font-bold text-amber-600 dark:text-amber-400">
|
<p class="mt-2 text-3xl font-bold text-amber-600 dark:text-amber-400">
|
||||||
{{ formatCurrency(dashboardData.remainingBalance) }}
|
${{ dashboardData.remainingBalance }}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-1 text-xs text-gray-500 dark:text-primary-dt/50">
|
<p class="mt-1 text-xs text-gray-500 dark:text-primary-dt/50">
|
||||||
{{ remainingPercentage }}% del presupuesto
|
{{ remainingPercentage }}% del presupuesto
|
||||||
@ -194,10 +184,10 @@ onMounted(() => {
|
|||||||
<!-- Título de la sección -->
|
<!-- Título de la sección -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-primary-dt">
|
<h2 class="text-xl font-semibold text-gray-800 dark:text-primary-dt">
|
||||||
Distribución de Gastos por Concepto
|
Gastos por Caja Chica
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">
|
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">
|
||||||
Porcentaje de gastos por categoría
|
Distribución de gastos por caja chica
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -218,7 +208,7 @@ onMounted(() => {
|
|||||||
<div class="lg:w-80">
|
<div class="lg:w-80">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h3 class="text-lg font-medium text-gray-800 dark:text-primary-dt mb-4">
|
<h3 class="text-lg font-medium text-gray-800 dark:text-primary-dt mb-4">
|
||||||
Detalle por Categoría
|
Detalle por Caja Chica
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -240,7 +230,7 @@ onMounted(() => {
|
|||||||
{{ item.value }}%
|
{{ item.value }}%
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 dark:text-primary-dt/50">
|
<div class="text-xs text-gray-500 dark:text-primary-dt/50">
|
||||||
{{ formatCurrency((item.value / 100) * dashboardData.justifiedExpenses) }}
|
${{ ((item.value / 100) * dashboardData.justifiedExpenses).toFixed(2) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -257,7 +247,7 @@ onMounted(() => {
|
|||||||
{{ dashboardData.expenseDistribution.length }}
|
{{ dashboardData.expenseDistribution.length }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-500 dark:text-primary-dt/70">
|
<div class="text-sm text-gray-500 dark:text-primary-dt/70">
|
||||||
Categorías
|
Cajas Chicas
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@ -1,45 +1,70 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { api, useForm } from '@Services/Api';
|
||||||
|
import { apiTo, viewTo } from './Module';
|
||||||
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import PageHeader from '@Holos/PageHeader.vue';
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue';
|
||||||
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import Selectable from '@Holos/Form/Selectable.vue';
|
||||||
|
import Textarea from '@Holos/Form/Textarea.vue';
|
||||||
|
import PrimaryButton from '@Holos/Button/Primary.vue';
|
||||||
|
|
||||||
// Estado del formulario
|
/** Definiciones */
|
||||||
const formData = ref({
|
const router = useRouter();
|
||||||
eventName: '',
|
|
||||||
place: '',
|
/** Propiedades */
|
||||||
|
const form = useForm({
|
||||||
|
event_name: '',
|
||||||
|
location: '',
|
||||||
description: '',
|
description: '',
|
||||||
company: 'Empresa A',
|
participants_count: '',
|
||||||
participants: '',
|
|
||||||
provider: '',
|
|
||||||
assignedAmount: 5000,
|
|
||||||
cost: '',
|
cost: '',
|
||||||
spent: '',
|
|
||||||
observations: '',
|
observations: '',
|
||||||
justificationFile: null
|
company_id: '',
|
||||||
|
petty_cash_id: '',
|
||||||
|
files: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Opciones de empresas
|
const companies = ref([]);
|
||||||
const companies = ref([
|
const pettyCashes = ref([]);
|
||||||
'Empresa A',
|
const availableBudget = ref('');
|
||||||
'Empresa B',
|
|
||||||
'Empresa C',
|
// Función para cargar cajas chicas por empresa
|
||||||
'Empresa D'
|
const loadPettyCashesByCompany = () => {
|
||||||
]);
|
if (form.company_id?.id) {
|
||||||
|
api.catalog({
|
||||||
|
'pettyCash:byCompany': form.company_id.id
|
||||||
|
}, {
|
||||||
|
onSuccess: (r) => {
|
||||||
|
pettyCashes.value = r['pettyCash:byCompany'] ?? [];
|
||||||
|
// Limpiar selección de caja chica al cambiar empresa
|
||||||
|
form.petty_cash_id = '';
|
||||||
|
availableBudget.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch para cargar cajas chicas cuando cambie la empresa
|
||||||
|
watch(() => form.company_id, loadPettyCashesByCompany, { deep: true });
|
||||||
|
|
||||||
|
// Watch para actualizar el presupuesto disponible cuando cambie la caja chica
|
||||||
|
watch(() => form.petty_cash_id, (newValue) => {
|
||||||
|
availableBudget.value = newValue?.available_budget ?? 0;
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
// Computed para calcular la diferencia
|
|
||||||
const difference = computed(() => {
|
|
||||||
const assigned = parseFloat(formData.value.assignedAmount) || 0;
|
|
||||||
const spent = parseFloat(formData.value.spent) || 0;
|
|
||||||
return assigned - spent;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Función para manejar la subida de archivos
|
// Función para manejar la subida de archivos
|
||||||
const handleFileUpload = (event) => {
|
const handleFileUpload = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (file.type === 'application/pdf') {
|
if (file.type === 'application/pdf') {
|
||||||
formData.value.justificationFile = file;
|
form.files = file;
|
||||||
} else {
|
} else {
|
||||||
alert('Por favor, selecciona un archivo PDF válido');
|
Notify.error('Por favor, selecciona un archivo PDF válido');
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -47,7 +72,7 @@ const handleFileUpload = (event) => {
|
|||||||
|
|
||||||
// Función para eliminar archivo
|
// Función para eliminar archivo
|
||||||
const removeFile = () => {
|
const removeFile = () => {
|
||||||
formData.value.justificationFile = null;
|
form.files = null;
|
||||||
const fileInput = document.getElementById('justification-file');
|
const fileInput = document.getElementById('justification-file');
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
@ -55,239 +80,144 @@ const removeFile = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Función para enviar justificación
|
// Función para enviar justificación
|
||||||
const submitJustification = () => {
|
function submit() {
|
||||||
// Validación básica
|
form.transform(data => ({
|
||||||
if (!formData.value.eventName || !formData.value.place || !formData.value.description) {
|
...data,
|
||||||
alert('Por favor, completa todos los campos obligatorios');
|
company_id: data.company_id?.id,
|
||||||
return;
|
petty_cash_id: data.petty_cash_id?.id
|
||||||
|
})).post(apiTo('store-expense-justification'), {
|
||||||
|
onSuccess: () => {
|
||||||
|
Notify.success('Justificación enviada correctamente');
|
||||||
|
router.push(viewTo({ name: 'reports' }));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!formData.value.justificationFile) {
|
/** Ciclos */
|
||||||
alert('Por favor, sube un archivo de justificación');
|
onMounted(() => {
|
||||||
return;
|
api.catalog({
|
||||||
}
|
'company:all': null
|
||||||
|
}, {
|
||||||
console.log('Enviar justificación:', formData.value);
|
onSuccess: (r) => companies.value = r['company:all'] ?? []
|
||||||
|
});
|
||||||
// Aquí implementarías la lógica para enviar
|
});
|
||||||
alert('Justificación enviada correctamente');
|
|
||||||
|
|
||||||
// Limpiar formulario
|
|
||||||
formData.value = {
|
|
||||||
eventName: '',
|
|
||||||
place: '',
|
|
||||||
description: '',
|
|
||||||
company: 'Empresa A',
|
|
||||||
participants: '',
|
|
||||||
provider: '',
|
|
||||||
assignedAmount: 5000,
|
|
||||||
cost: '',
|
|
||||||
spent: '',
|
|
||||||
observations: '',
|
|
||||||
justificationFile: null
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para formatear moneda
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return new Intl.NumberFormat('es-MX', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD'
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 max-w-auto mx-auto">
|
<PageHeader
|
||||||
<!-- Header -->
|
title="Justificación de Gastos"
|
||||||
<div class="flex items-start justify-between gap-4">
|
>
|
||||||
<div>
|
<RouterLink :to="viewTo({ name: 'index' })">
|
||||||
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-primary-dt">Justificación de Gastos</h1>
|
<IconButton
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">Documenta y justifica los gastos realizados en eventos</p>
|
class="text-white"
|
||||||
</div>
|
icon="arrow_back"
|
||||||
|
:title="$t('return')"
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<div class="w-full pb-2">
|
||||||
|
<p class="text-justify text-sm">Documenta y justifica los gastos realizados en eventos</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card principal -->
|
<!-- Card principal -->
|
||||||
<section class="mt-6 bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
<section class="mt-6 bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
||||||
|
|
||||||
<!-- Formulario -->
|
<!-- Formulario -->
|
||||||
<form @submit.prevent="submitJustification" class="space-y-6">
|
<form @submit.prevent="submit" class="space-y-6">
|
||||||
|
|
||||||
<!-- Detalles del Evento -->
|
<!-- Detalles del Evento -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
<!-- Nombre del Evento -->
|
<Input
|
||||||
<div class="md:col-span-1">
|
v-model="form.event_name"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="event_name"
|
||||||
Nombre del Evento *
|
title="Nombre del Evento"
|
||||||
</label>
|
:onError="form.errors.event_name"
|
||||||
<input
|
|
||||||
v-model="formData.eventName"
|
|
||||||
type="text"
|
|
||||||
required
|
required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
/>
|
||||||
placeholder="Ingresa el nombre del evento"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lugar -->
|
<Input
|
||||||
<div class="md:col-span-1">
|
v-model="form.location"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="location"
|
||||||
Lugar *
|
title="Lugar"
|
||||||
</label>
|
:onError="form.errors.location"
|
||||||
<input
|
|
||||||
v-model="formData.place"
|
|
||||||
type="text"
|
|
||||||
required
|
required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
/>
|
||||||
placeholder="Ingresa el lugar del evento"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Descripción -->
|
<Textarea
|
||||||
<div>
|
v-model="form.description"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="description"
|
||||||
Descripción y Detalles *
|
title="Descripción y Detalles"
|
||||||
</label>
|
:onError="form.errors.description"
|
||||||
<textarea
|
|
||||||
v-model="formData.description"
|
|
||||||
required
|
required
|
||||||
rows="4"
|
/>
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent resize-vertical dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
|
||||||
placeholder="Describe los detalles del evento y los gastos realizados"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Detalles Financieros y Participantes -->
|
<!-- Detalles Financieros y Participantes -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
<!-- Empresa -->
|
<Selectable
|
||||||
<div>
|
v-model="form.company_id"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="company_id"
|
||||||
Empresa
|
title="Empresa"
|
||||||
</label>
|
:onError="form.errors.company_id"
|
||||||
<select
|
:options="companies"
|
||||||
v-model="formData.company"
|
required
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
/>
|
||||||
>
|
|
||||||
<option v-for="company in companies" :key="company" :value="company">
|
|
||||||
{{ company }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Número de Participantes -->
|
<Input
|
||||||
<div>
|
v-model.number="form.participants_count"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="participants_count"
|
||||||
Número de Participantes
|
title="Número de Participantes"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="formData.participants"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
:onError="form.errors.participants_count"
|
||||||
placeholder="0"
|
/>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Proveedor -->
|
<Input
|
||||||
<div>
|
v-model.number="form.cost"
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
id="cost"
|
||||||
Proveedor
|
title="Costo"
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.provider"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
|
||||||
placeholder="Nombre del proveedor"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Monto Asignado -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
|
||||||
Monto Asignado (USD)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="formData.assignedAmount"
|
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
:onError="form.errors.cost"
|
||||||
placeholder="0.00"
|
/>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Costo -->
|
<!-- Caja Chica y Presupuesto Disponible -->
|
||||||
<div>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
|
||||||
Costo (USD)
|
<Selectable
|
||||||
</label>
|
v-model="form.petty_cash_id"
|
||||||
<input
|
id="petty_cash_id"
|
||||||
v-model.number="formData.cost"
|
title="Caja Chica"
|
||||||
|
:onError="form.errors.petty_cash_id"
|
||||||
|
:options="pettyCashes"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
v-model="availableBudget"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
id="available_budget"
|
||||||
step="0.01"
|
title="Presupuesto Disponible"
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
disabled
|
||||||
placeholder="0.00"
|
/>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gastado -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
|
||||||
Gastado (USD)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="formData.spent"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
|
||||||
placeholder="0.00"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resumen financiero -->
|
|
||||||
<div v-if="formData.assignedAmount > 0 || formData.spent > 0" class="p-4 bg-gray-50 rounded-lg dark:bg-primary/5">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-primary-dt/70">Monto Asignado</div>
|
|
||||||
<div class="text-lg font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
{{ formatCurrency(formData.assignedAmount) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-primary-dt/70">Gastado</div>
|
|
||||||
<div class="text-lg font-bold text-red-600 dark:text-red-400">
|
|
||||||
{{ formatCurrency(formData.spent) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-primary-dt/70">Diferencia</div>
|
|
||||||
<div class="text-lg font-bold" :class="difference >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
|
||||||
{{ formatCurrency(difference) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Observaciones -->
|
<!-- Observaciones -->
|
||||||
<div>
|
<Textarea
|
||||||
<label class="block text-sm font-medium text-gray-700 dark:text-primary-dt mb-2">
|
v-model="form.observations"
|
||||||
Observaciones
|
id="observations"
|
||||||
</label>
|
title="Observaciones"
|
||||||
<textarea
|
:onError="form.errors.observations"
|
||||||
v-model="formData.observations"
|
/>
|
||||||
rows="3"
|
|
||||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:border-transparent resize-vertical dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt"
|
|
||||||
placeholder="Agrega observaciones adicionales si es necesario"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subida de archivo -->
|
<!-- Subida de archivo -->
|
||||||
<div>
|
<div>
|
||||||
@ -321,12 +251,12 @@ const formatCurrency = (amount) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Archivo seleccionado -->
|
<!-- Archivo seleccionado -->
|
||||||
<div v-if="formData.justificationFile" class="mt-3 p-3 bg-green-50 border border-green-200 rounded-lg dark:bg-green-900/20 dark:border-green-800">
|
<div v-if="form.files" class="mt-3 p-3 bg-green-50 border border-green-200 rounded-lg dark:bg-green-900/20 dark:border-green-800">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<GoogleIcon class="text-green-600 dark:text-green-400 text-xl" name="picture_as_pdf" />
|
<GoogleIcon class="text-green-600 dark:text-green-400 text-xl" name="picture_as_pdf" />
|
||||||
<span class="text-sm font-medium text-green-800 dark:text-green-300">
|
<span class="text-sm font-medium text-green-800 dark:text-green-300">
|
||||||
{{ formData.justificationFile.name }}
|
{{ form.files.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -344,17 +274,16 @@ const formatCurrency = (amount) => {
|
|||||||
|
|
||||||
<!-- Botón de envío -->
|
<!-- Botón de envío -->
|
||||||
<div class="flex justify-end pt-6 border-t border-gray-100 dark:border-primary/20">
|
<div class="flex justify-end pt-6 border-t border-gray-100 dark:border-primary/20">
|
||||||
<button
|
<PrimaryButton
|
||||||
type="submit"
|
type="submit"
|
||||||
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-[#7c3aed] hover:bg-[#6d28d9] text-white font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-[#7c3aed] focus:ring-offset-2 transition-colors"
|
:processing="form.processing"
|
||||||
>
|
>
|
||||||
<GoogleIcon class="text-white text-xl" name="send" />
|
<GoogleIcon class="text-white text-xl mr-2" name="send" />
|
||||||
Enviar Justificación
|
Enviar Justificación
|
||||||
</button>
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
109
src/pages/Admin/Events/Modals/Show.vue
Normal file
109
src/pages/Admin/Events/Modals/Show.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { getDateTime } from '@Controllers/DateController';
|
||||||
|
|
||||||
|
import Header from '@Holos/Modal/Elements/Header.vue';
|
||||||
|
import ShowModal from '@Holos/Modal/Show.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Eventos */
|
||||||
|
const emit = defineEmits([
|
||||||
|
'close',
|
||||||
|
'reload'
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Propiedades */
|
||||||
|
const model = ref(null);
|
||||||
|
|
||||||
|
/** Referencias */
|
||||||
|
const modalRef = ref(null);
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
function close() {
|
||||||
|
model.value = null;
|
||||||
|
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exposiciones */
|
||||||
|
defineExpose({
|
||||||
|
open: (data) => {
|
||||||
|
model.value = data;
|
||||||
|
modalRef.value.open();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ShowModal
|
||||||
|
ref="modalRef"
|
||||||
|
@close="close"
|
||||||
|
>
|
||||||
|
<div v-if="model">
|
||||||
|
<Header
|
||||||
|
:title="model.event_name"
|
||||||
|
:subtitle="model.location"
|
||||||
|
>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<div class="flex w-full justify-center items-center">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-6xl text-primary"
|
||||||
|
name="event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
<div class="flex w-full p-4">
|
||||||
|
<GoogleIcon
|
||||||
|
class="text-xl text-success"
|
||||||
|
name="event"
|
||||||
|
/>
|
||||||
|
<div class="pl-3">
|
||||||
|
<p class="font-bold text-lg leading-none pb-2">
|
||||||
|
{{ $t('details') }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('event_name') }}: </b>
|
||||||
|
{{ model.event_name }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('location') }}: </b>
|
||||||
|
{{ model.location }}
|
||||||
|
</p>
|
||||||
|
<p v-if="model.description">
|
||||||
|
<b>{{ $t('description') }}: </b>
|
||||||
|
{{ model.description }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('participants_count') }}: </b>
|
||||||
|
{{ model.participants_count ?? '-' }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('cost') }}: </b>
|
||||||
|
${{ model.cost ?? '0.00' }}
|
||||||
|
</p>
|
||||||
|
<p v-if="model.observations">
|
||||||
|
<b>{{ $t('observations') }}: </b>
|
||||||
|
{{ model.observations }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('company') }}: </b>
|
||||||
|
{{ model.company?.name ?? '-' }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('petty_cash.title') }}: </b>
|
||||||
|
{{ model.petty_cash?.name ?? '-' }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('created_at') }}: </b>
|
||||||
|
{{ getDateTime(model.created_at) }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<b>{{ $t('updated_at') }}: </b>
|
||||||
|
{{ getDateTime(model.updated_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ShowModal>
|
||||||
|
</template>
|
||||||
21
src/pages/Admin/Events/Module.js
Normal file
21
src/pages/Admin/Events/Module.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { lang } from '@Lang/i18n';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission.js';
|
||||||
|
|
||||||
|
// Ruta API
|
||||||
|
const apiTo = (name, params = {}) => route(`admin.events.${name}`, params)
|
||||||
|
|
||||||
|
// Ruta visual
|
||||||
|
const viewTo = ({ name = '', params = {}, query = {} }) => view({ name: `admin.events.${name}`, params, query })
|
||||||
|
|
||||||
|
// Obtener traducción del componente
|
||||||
|
const transl = (str) => lang(`events.${str}`)
|
||||||
|
|
||||||
|
// Determina si un usuario puede hacer algo no en base a los permisos
|
||||||
|
const can = (permission) => hasPermission(`events.${permission}`)
|
||||||
|
|
||||||
|
export {
|
||||||
|
can,
|
||||||
|
viewTo,
|
||||||
|
apiTo,
|
||||||
|
transl
|
||||||
|
}
|
||||||
@ -1,204 +1,102 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useSearcher } from '@Services/Api';
|
||||||
|
import { hasPermission } from '@Plugins/RolePermission';
|
||||||
|
import { getDate } from '@Controllers/DateController';
|
||||||
|
import { can, apiTo, viewTo, transl } from './Module'
|
||||||
|
|
||||||
|
import IconButton from '@Holos/Button/Icon.vue'
|
||||||
|
import DestroyView from '@Holos/Modal/Template/Destroy.vue';
|
||||||
|
import SearcherHead from '@Holos/Searcher.vue';
|
||||||
|
import Table from '@Holos/NewTable.vue';
|
||||||
|
import ShowView from './Modals/Show.vue';
|
||||||
|
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import Searcher from '@Holos/Searcher.vue';
|
||||||
|
import Adding from '@Holos/Button/ButtonRh.vue';
|
||||||
|
|
||||||
// Datos de ejemplo de gastos
|
/** Propiedades */
|
||||||
const expenses = ref([
|
const models = ref([]);
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
date: '2024-08-15',
|
|
||||||
concept: 'Catering para evento X',
|
|
||||||
reason: 'Servicio de comida para 50 asistentes.',
|
|
||||||
amount: 1500.00,
|
|
||||||
justificationFile: 'catering_evento_x.pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
date: '2024-08-14',
|
|
||||||
concept: 'Renta de equipo de audio',
|
|
||||||
reason: 'Micrófonos y bocinas para el auditorio.',
|
|
||||||
amount: 800.00,
|
|
||||||
justificationFile: 'renta_audio.pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
date: '2024-08-10',
|
|
||||||
concept: 'Viáticos para conferencista',
|
|
||||||
reason: 'Vuelo y hospedaje para Dr. Smith.',
|
|
||||||
amount: 1250.00,
|
|
||||||
justificationFile: 'viaticos_dr_smith.pdf'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Computed para calcular el total
|
/** Referencias */
|
||||||
const totalExpenses = computed(() => {
|
const showModal = ref(false);
|
||||||
return expenses.value.reduce((total, expense) => {
|
const destroyModal = ref(false);
|
||||||
return total + expense.amount;
|
|
||||||
}, 0);
|
/** Métodos */
|
||||||
|
const searcher = useSearcher({
|
||||||
|
url: apiTo('index'),
|
||||||
|
onSuccess: (r) => models.value = r.models,
|
||||||
|
onError: () => models.value = []
|
||||||
});
|
});
|
||||||
|
|
||||||
// Función para ver PDF
|
/** Ciclos */
|
||||||
const viewPDF = (fileName) => {
|
onMounted(() => {
|
||||||
console.log('Ver PDF:', fileName);
|
searcher.search();
|
||||||
// Aquí implementarías la lógica para abrir el PDF
|
});
|
||||||
// Por ejemplo: window.open(`/pdfs/${fileName}`, '_blank');
|
|
||||||
alert(`Abriendo archivo: ${fileName}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para formatear moneda
|
|
||||||
const formatCurrency = (amount) => {
|
|
||||||
return new Intl.NumberFormat('es-MX', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD'
|
|
||||||
}).format(amount);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para formatear fecha
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('es-MX', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para exportar reporte
|
|
||||||
const exportReport = () => {
|
|
||||||
console.log('Exportar reporte:', expenses.value);
|
|
||||||
// Aquí implementarías la lógica para exportar el reporte
|
|
||||||
alert('Reporte exportado correctamente');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Función para filtrar por fecha
|
|
||||||
const filterByDate = () => {
|
|
||||||
// Aquí podrías implementar filtros por fecha
|
|
||||||
console.log('Filtrar por fecha');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 max-w-auto mx-auto">
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-primary-dt">Reporte de Gastos de Eventos</h1>
|
<h1 class="text-3xl font-extrabold text-gray-900 dark:text-primary-dt">{{ $t('reports.title') }}</h1>
|
||||||
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">Consulta y gestiona los gastos realizados en eventos</p>
|
<p class="mt-1 text-sm text-gray-500 dark:text-primary-dt/70">{{ $t('reports.description') }}
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button
|
|
||||||
@click="filterByDate"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#2563eb] focus:border-transparent dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt dark:hover:bg-primary/10"
|
|
||||||
>
|
|
||||||
<GoogleIcon class="text-gray-600 dark:text-primary-dt text-xl" name="filter_list" />
|
|
||||||
Filtrar
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="exportReport"
|
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-[#2563eb] hover:bg-[#1e40af] text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-[#2563eb] focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<GoogleIcon class="text-white text-xl" name="download" />
|
|
||||||
Exportar
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card principal -->
|
<!-- Search Card -->
|
||||||
<section class="mt-6 bg-white rounded-lg shadow-sm p-6 dark:bg-primary-d dark:border-primary/20 dark:text-primary-dt">
|
<div class="pt-2 w-full">
|
||||||
|
<Searcher @search="(x) => searcher.search(x)">
|
||||||
<!-- Tabla de gastos -->
|
</Searcher>
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr class="border-b border-gray-100 dark:border-primary/20">
|
|
||||||
<th class="text-left py-3 px-4 font-semibold text-gray-800 dark:text-primary-dt">FECHA</th>
|
|
||||||
<th class="text-left py-3 px-4 font-semibold text-gray-800 dark:text-primary-dt">CONCEPTO</th>
|
|
||||||
<th class="text-left py-3 px-4 font-semibold text-gray-800 dark:text-primary-dt">RAZÓN</th>
|
|
||||||
<th class="text-right py-3 px-4 font-semibold text-gray-800 dark:text-primary-dt">MONTO</th>
|
|
||||||
<th class="text-left py-3 px-4 font-semibold text-gray-800 dark:text-primary-dt">JUSTIFICANTE</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="expense in expenses"
|
|
||||||
:key="expense.id"
|
|
||||||
class="border-b border-gray-50 dark:border-primary/10 hover:bg-gray-50 dark:hover:bg-primary/5"
|
|
||||||
>
|
|
||||||
<!-- Fecha -->
|
|
||||||
<td class="py-4 px-4">
|
|
||||||
<div class="text-sm text-gray-900 dark:text-primary-dt">
|
|
||||||
{{ formatDate(expense.date) }}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Concepto -->
|
<!-- List Card -->
|
||||||
<td class="py-4 px-4">
|
<div class="pt-2 w-full">
|
||||||
<div class="font-medium text-gray-900 dark:text-primary-dt">
|
<Table :items="models" :processing="searcher.processing" @send-pagination="(page) => searcher.pagination(page)">
|
||||||
{{ expense.concept }}
|
<template #head>
|
||||||
</div>
|
<th v-text="$t('created_at')" />
|
||||||
</td>
|
<th v-text="$t('event_name')" />
|
||||||
|
<th v-text="$t('cost')" />
|
||||||
|
<th v-text="$t('petty_cash.title')" />
|
||||||
|
<th class="w-32 text-center" v-text="$t('actions')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Razón -->
|
<template #body="{ items }">
|
||||||
<td class="py-4 px-4">
|
<tr v-for="model in items" class="table-row">
|
||||||
<div class="text-sm text-gray-600 dark:text-primary-dt/70">
|
<td>{{ model.created_at ? getDate(model.created_at) : '-' }}</td>
|
||||||
{{ expense.reason }}
|
<td>{{ model.event_name }}</td>
|
||||||
</div>
|
<td>
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Monto -->
|
|
||||||
<td class="py-4 px-4 text-right">
|
|
||||||
<div class="text-lg font-bold text-[#2563eb] dark:text-primary-dt">
|
<div class="text-lg font-bold text-[#2563eb] dark:text-primary-dt">
|
||||||
{{ formatCurrency(expense.amount) }}
|
${{ model.cost }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>{{ model.petty_cash?.name }}</td>
|
||||||
<!-- Justificante -->
|
<td>
|
||||||
<td class="py-4 px-4">
|
<div class="table-actions">
|
||||||
<button
|
<IconButton icon="visibility" :title="$t('crud.show')" @click="showModal.open(model)"
|
||||||
@click="viewPDF(expense.justificationFile)"
|
outline />
|
||||||
class="text-blue-600 hover:text-blue-800 underline text-sm font-medium dark:text-blue-400 dark:hover:text-blue-300 transition-colors"
|
<IconButton icon="delete" :title="$t('crud.destroy')"
|
||||||
>
|
@click="destroyModal.open(model)" outline />
|
||||||
Ver PDF
|
</div>
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</template>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Resumen de gastos -->
|
<template #empty>
|
||||||
<div class="mt-6 p-4 bg-gray-50 rounded-lg dark:bg-primary/5">
|
<td colspan="4" class="py-12 text-center">
|
||||||
<div class="flex items-center justify-between">
|
<div class="text-gray-500 dark:text-primary-dt/70">
|
||||||
<div class="flex items-center gap-2">
|
<GoogleIcon name="receipt_long" class="w-12 h-12 mx-auto mb-4 text-gray-400" />
|
||||||
<GoogleIcon class="text-gray-600 dark:text-primary-dt text-xl" name="account_balance_wallet" />
|
<p class="text-lg font-medium">No se encontraron justificaciones</p>
|
||||||
<span class="text-lg font-medium text-gray-800 dark:text-primary-dt">Total de Gastos:</span>
|
<p class="text-sm mt-1">Intenta ajustar los filtros de búsqueda</p>
|
||||||
</div>
|
|
||||||
<div class="text-2xl font-bold text-[#2563eb] dark:text-primary-dt">
|
|
||||||
{{ formatCurrency(totalExpenses) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
|
||||||
<!-- Footer con estadísticas -->
|
<ShowView ref="showModal" />
|
||||||
<div class="mt-6 border-t border-gray-100 pt-4 flex items-center justify-between dark:border-primary/20">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-primary-dt/70">
|
|
||||||
Mostrando {{ expenses.length }} gastos registrados
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<DestroyView ref="destroyModal" subtitle="event_name"
|
||||||
<div class="flex items-center gap-2 text-sm">
|
:to="(expenseJustification) => apiTo('destroy-expense-justification', { expenseJustification })" @update="searcher.search()" />
|
||||||
<span class="w-3 h-3 bg-blue-400 rounded-full"></span>
|
|
||||||
<span class="text-gray-600 dark:text-primary-dt/70">Total: {{ expenses.length }} registros</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<span class="w-3 h-3 bg-green-400 rounded-full"></span>
|
|
||||||
<span class="text-gray-600 dark:text-primary-dt/70">Justificados: {{ expenses.length }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user