584 lines
22 KiB
PHP
Executable File
584 lines
22 KiB
PHP
Executable File
@extends('layouts.app')
|
|
|
|
@section('title', 'Calendario')
|
|
|
|
@section('content')
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<h2 class="mb-4">
|
|
<i class="bi bi-calendar3 text-primary"></i> Calendario de Ventas y Gastos
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Selector de mes -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<form method="GET" class="d-flex gap-2 flex-wrap">
|
|
<select name="year" class="form-select" style="width: auto;" onchange="this.form.submit()">
|
|
@php($years = range(2035, 2026))
|
|
@foreach($years as $y)
|
|
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}</option>
|
|
@endforeach
|
|
</select>
|
|
<select name="month" class="form-select" style="width: auto;" onchange="this.form.submit()">
|
|
<option value="">Todos los meses</option>
|
|
@foreach(['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'] as $m)
|
|
<option value="{{ $m }}" {{ request('month') == $m ? 'selected' : '' }}>{{ $m }}</option>
|
|
@endforeach
|
|
</select>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 text-end">
|
|
@if($currentMonth)
|
|
<span class="badge bg-primary fs-6">
|
|
<i class="bi bi-calendar"></i> {{ $currentMonth->name }} {{ $currentMonth->year }}
|
|
</span>
|
|
<span class="badge bg-{{ $currentMonth->status === 'open' ? 'success' : ($currentMonth->status === 'closed' ? 'warning' : 'info') }}">
|
|
{{ ucfirst($currentMonth->status) }}
|
|
</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
@if($currentMonth)
|
|
<!-- Resumen del mes -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="card stat-card primary">
|
|
<div class="card-body">
|
|
<h6 class="text-muted">Ventas del Usuario</h6>
|
|
<h4 class="text-primary">${{ number_format($currentMonth->dailySales()->sum('user_sales'), 2) }}</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card stat-card success">
|
|
<div class="card-body">
|
|
<h6 class="text-muted">Ventas del Sistema</h6>
|
|
<h4 class="text-success">${{ number_format($currentMonth->dailySales()->sum('system_sales'), 2) }}</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card stat-card warning">
|
|
<div class="card-body">
|
|
<h6 class="text-muted">Total Gastos</h6>
|
|
<h4 class="text-warning">${{ number_format($currentMonth->expenses()->sum('amount'), 2) }}</h4>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendario FullCalendar -->
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div id="calendar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leyenda -->
|
|
<div class="row mt-3">
|
|
<div class="col-12">
|
|
<div class="d-flex gap-3 flex-wrap">
|
|
<span class="badge bg-light text-dark border p-2">
|
|
<i class="bi bi-square text-secondary"></i> Sin datos
|
|
</span>
|
|
<span class="badge bg-success-subtle text-success border p-2">
|
|
<i class="bi bi-square text-success"></i> Con ventas
|
|
</span>
|
|
<span class="badge bg-danger-subtle text-danger border p-2">
|
|
<i class="bi bi-square text-danger"></i> Con gastos
|
|
</span>
|
|
<span class="badge bg-warning-subtle text-warning border p-2">
|
|
<i class="bi bi-square text-warning"></i> Con ambos
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="alert alert-info">
|
|
<h5><i class="bi bi-info-circle"></i> No hay un mes seleccionado</h5>
|
|
<p>Selecciona un mes del dropdown o crea uno nuevo.</p>
|
|
<a href="{{ route('months.index') }}" class="btn btn-primary">Ver Meses</a>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Modal para capturar ventas -->
|
|
<div class="modal fade" id="dayModal" tabindex="-1" aria-labelledby="dayModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-primary text-white">
|
|
<h5 class="modal-title" id="dayModalLabel">
|
|
<i class="bi bi-calendar-plus"></i> Capturar Día
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form method="POST" id="dayForm">
|
|
@csrf
|
|
<div class="modal-body">
|
|
<input type="hidden" name="month_id" id="formMonthId" value="{{ $currentMonth->id ?? '' }}">
|
|
<input type="hidden" name="date" id="modalDate">
|
|
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle"></i> Fecha: <strong id="modalDateDisplay"></strong>
|
|
</div>
|
|
|
|
<!-- Ventas -->
|
|
<h6 class="border-bottom pb-2 mb-3">
|
|
<i class="bi bi-currency-dollar text-success"></i> Ventas
|
|
</h6>
|
|
|
|
<div class="mb-3">
|
|
<label for="user_sales" class="form-label">Ventas del Usuario</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" class="form-control" id="user_sales" name="user_sales"
|
|
value="0" step="0.01" min="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="system_sales" class="form-label">Ventas del Sistema</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" class="form-control" id="system_sales" name="system_sales"
|
|
value="0" step="0.01" min="0">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gastos -->
|
|
<h6 class="border-bottom pb-2 mb-3 mt-4">
|
|
<i class="bi bi-receipt text-danger"></i> Gastos
|
|
</h6>
|
|
|
|
<div class="mb-3">
|
|
<label for="expense_description" class="form-label">Descripción del Gasto</label>
|
|
<input type="text" class="form-control" id="expense_description" name="expense_description"
|
|
placeholder="Opcional">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="expense_amount" class="form-label">Monto del Gasto</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text">$</span>
|
|
<input type="number" class="form-control" id="expense_amount" name="expense_amount"
|
|
value="0" step="0.01" min="0">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="expense_type" class="form-label">Quincena del Gasto</label>
|
|
<select class="form-select" id="expense_type" name="expense_type">
|
|
<option value="q1">1ra Quincena (1-15)</option>
|
|
<option value="q2">2da Quincena (16-31)</option>
|
|
<option value="mensual">Gasto Mensual (se divide en 2)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-save"></i> Guardar
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal para ver detalles del día -->
|
|
<div class="modal fade" id="dayDetailModal" tabindex="-1" aria-labelledby="dayDetailModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-info text-white">
|
|
<h5 class="modal-title" id="dayDetailModalLabel">
|
|
<i class="bi bi-eye"></i> Detalles del Día
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="mb-3"><strong>Fecha:</strong> <span id="detailDate"></span></p>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<div class="card bg-light">
|
|
<div class="card-body text-center">
|
|
<h6 class="text-muted">Ventas Usuario</h6>
|
|
<h5 class="text-success" id="detailUserSales">$0.00</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="card bg-light">
|
|
<div class="card-body text-center">
|
|
<h6 class="text-muted">Ventas Sistema</h6>
|
|
<h5 class="text-info" id="detailSystemSales">$0.00</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card bg-light">
|
|
<div class="card-body">
|
|
<h6 class="text-muted">Gastos del Día</h6>
|
|
<div id="detailExpenses">
|
|
<p class="text-muted mb-0">No hay gastos registrados</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
|
|
<button type="button" class="btn btn-primary" id="editDayBtn">
|
|
<i class="bi bi-pencil"></i> Editar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Data for calendar events
|
|
const dailySales = @json($dailySales ?? []);
|
|
const expenses = @json($expenses ?? []);
|
|
const year = {{ $year }};
|
|
const currentMonthId = {{ $currentMonth->id ?? 0 }};
|
|
|
|
console.log('Month ID:', currentMonthId);
|
|
console.log('DailySales raw:', dailySales);
|
|
console.log('DailySales type:', typeof dailySales);
|
|
console.log('Expenses:', expenses);
|
|
|
|
// Map data to calendar events
|
|
const events = [];
|
|
|
|
// Add sales events - iterate over object values
|
|
Object.keys(dailySales).forEach(date => {
|
|
const saleData = dailySales[date];
|
|
if (saleData && saleData.user_sales > 0) {
|
|
events.push({
|
|
title: 'V: $' + parseFloat(saleData.user_sales).toLocaleString(),
|
|
start: date,
|
|
className: 'bg-success',
|
|
extendedProps: {
|
|
type: 'sale',
|
|
user_sales: saleData.user_sales,
|
|
system_sales: saleData.system_sales
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add expense events
|
|
Object.keys(expenses).forEach(date => {
|
|
const expData = expenses[date];
|
|
if (expData && expData.amount > 0) {
|
|
events.push({
|
|
title: 'G: $' + parseFloat(expData.amount).toLocaleString(),
|
|
start: date,
|
|
className: 'bg-danger',
|
|
extendedProps: {
|
|
type: 'expense',
|
|
amount: expData.amount,
|
|
description: expData.description
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log('Events:', events);
|
|
|
|
const calendarEl = document.getElementById('calendar');
|
|
|
|
// FullCalendar initialization
|
|
const calendar = new FullCalendar.Calendar(calendarEl, {
|
|
initialView: 'dayGridMonth',
|
|
initialDate: year + '-04-01',
|
|
locale: 'es',
|
|
headerToolbar: {
|
|
left: 'prev,next today',
|
|
center: 'title',
|
|
right: 'dayGridMonth,listMonth'
|
|
},
|
|
buttonText: {
|
|
today: 'Hoy',
|
|
month: 'Mes',
|
|
list: 'Lista'
|
|
},
|
|
dayMaxEvents: true,
|
|
eventDisplay: 'block',
|
|
events: events,
|
|
dateClick: function(info) {
|
|
// Verify month exists
|
|
const monthId = {{ $currentMonth->id ?? 0 }};
|
|
if (!monthId || monthId === 0) {
|
|
console.log('No hay mes seleccionado');
|
|
return;
|
|
}
|
|
openDayModal(info.dateStr);
|
|
},
|
|
eventClick: function(info) {
|
|
showDayDetails(info.event.startStr);
|
|
},
|
|
eventDidMount: function(info) {
|
|
// Add custom styling based on data
|
|
const date = info.event.startStr;
|
|
if (dailySales[date] && expenses[date]) {
|
|
info.el.style.background = 'linear-gradient(135deg, #f39c12 0%, #e74c3c 100%)';
|
|
} else if (dailySales[date]) {
|
|
info.el.style.background = 'linear-gradient(135deg, #27ae60 0%, #2ecc71 100%)';
|
|
} else if (expenses[date]) {
|
|
info.el.style.background = 'linear-gradient(135deg, #e74c3c 0%, #c0392b 100%)';
|
|
}
|
|
}
|
|
});
|
|
|
|
calendar.render();
|
|
|
|
// Initialize selected date variable
|
|
window.selectedDate = null;
|
|
|
|
// Functions
|
|
window.openDayModal = function(dateStr) {
|
|
const modalEl = document.getElementById('dayModal');
|
|
if (!modalEl) return;
|
|
|
|
const modal = new bootstrap.Modal(modalEl);
|
|
|
|
const modalDateEl = document.getElementById('modalDate');
|
|
const modalDateDisplayEl = document.getElementById('modalDateDisplay');
|
|
|
|
if (modalDateEl && modalDateDisplayEl) {
|
|
modalDateEl.value = dateStr;
|
|
modalDateDisplayEl.textContent = formatDate(dateStr);
|
|
}
|
|
|
|
// Check if there's existing data
|
|
const userSalesEl = document.getElementById('user_sales');
|
|
const systemSalesEl = document.getElementById('system_sales');
|
|
const expenseDescEl = document.getElementById('expense_description');
|
|
const expenseAmountEl = document.getElementById('expense_amount');
|
|
|
|
if (userSalesEl && systemSalesEl) {
|
|
// Buscar coincidencia parcial
|
|
let saleData = dailySales[dateStr];
|
|
if (!saleData) {
|
|
// Buscar en claves
|
|
for (const key in dailySales) {
|
|
if (key.includes(dateStr) || dateStr.includes(key)) {
|
|
saleData = dailySales[key];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (saleData) {
|
|
userSalesEl.value = saleData.user_sales;
|
|
systemSalesEl.value = saleData.system_sales;
|
|
} else {
|
|
userSalesEl.value = 0;
|
|
systemSalesEl.value = 0;
|
|
}
|
|
}
|
|
|
|
if (expenseDescEl && expenseAmountEl) {
|
|
let expData = expenses[dateStr];
|
|
if (!expData) {
|
|
for (const key in expenses) {
|
|
if (key.includes(dateStr) || dateStr.includes(key)) {
|
|
expData = expenses[key];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (expData) {
|
|
expenseDescEl.value = expData.description || '';
|
|
expenseAmountEl.value = expData.amount;
|
|
} else {
|
|
expenseDescEl.value = '';
|
|
expenseAmountEl.value = 0;
|
|
}
|
|
}
|
|
|
|
modal.show();
|
|
};
|
|
|
|
window.showDayDetails = function(dateStr) {
|
|
const modal = new bootstrap.Modal(document.getElementById('dayDetailModal'));
|
|
document.getElementById('detailDate').textContent = formatDate(dateStr);
|
|
|
|
// Buscar en todas las claves
|
|
let foundKey = null;
|
|
for (const key in dailySales) {
|
|
if (key.includes(dateStr) || dateStr.includes(key)) {
|
|
foundKey = key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (foundKey) {
|
|
// Found
|
|
document.getElementById('detailUserSales').textContent = '$' + dailySales[foundKey].user_sales.toLocaleString();
|
|
document.getElementById('detailSystemSales').textContent = '$' + dailySales[foundKey].system_sales.toLocaleString();
|
|
} else {
|
|
document.getElementById('detailUserSales').textContent = '$0.00';
|
|
document.getElementById('detailSystemSales').textContent = '$0.00';
|
|
}
|
|
|
|
if (expenses[dateStr]) {
|
|
document.getElementById('detailExpenses').innerHTML = `
|
|
<p class="mb-1"><strong>${expenses[dateStr].description || 'Gasto'}</strong></p>
|
|
<h5 class="text-danger mb-0">$${expenses[dateStr].amount.toLocaleString()}</h5>
|
|
`;
|
|
} else {
|
|
document.getElementById('detailExpenses').innerHTML = '<p class="text-muted mb-0">No hay gastos registrados</p>';
|
|
}
|
|
|
|
// Set up edit button
|
|
document.getElementById('editDayBtn').onclick = function() {
|
|
window.selectedDate = dateStr;
|
|
modal.hide();
|
|
setTimeout(() => openDayModal(dateStr), 300);
|
|
};
|
|
|
|
modal.show();
|
|
};
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return 'Sin fecha';
|
|
const parts = dateStr.split('-');
|
|
if (parts.length === 3) {
|
|
const year = parseInt(parts[0]);
|
|
const month = parseInt(parts[1]) - 1;
|
|
const day = parseInt(parts[2]);
|
|
const date = new Date(year, month, day);
|
|
return date.toLocaleDateString('es-ES', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
}
|
|
return dateStr;
|
|
}
|
|
|
|
// Form submission handler
|
|
document.getElementById('dayForm').addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
// Get values directly from DOM elements
|
|
const monthIdEl = document.getElementById('formMonthId');
|
|
const dateEl = document.getElementById('modalDate');
|
|
const userSalesEl = document.getElementById('user_sales');
|
|
const systemSalesEl = document.getElementById('system_sales');
|
|
const expenseAmountEl = document.getElementById('expense_amount');
|
|
const expenseDescEl = document.getElementById('expense_description');
|
|
const expenseTypeEl = document.getElementById('expense_type');
|
|
|
|
let monthId = monthIdEl ? monthIdEl.value : '';
|
|
let date = dateEl ? dateEl.value : '';
|
|
|
|
// Use window.selectedDate as fallback
|
|
if (!date && window.selectedDate) date = window.selectedDate;
|
|
|
|
// Try fallback
|
|
if (!monthId) monthId = '{{ $currentMonth->id ?? 0 }}';
|
|
|
|
const userSales = userSalesEl ? (parseFloat(userSalesEl.value) || 0) : 0;
|
|
const systemSales = systemSalesEl ? (parseFloat(systemSalesEl.value) || 0) : 0;
|
|
const expenseAmount = expenseAmountEl ? (parseFloat(expenseAmountEl.value) || 0) : 0;
|
|
const expenseDesc = expenseDescEl ? expenseDescEl.value : '';
|
|
const expenseType = expenseTypeEl ? expenseTypeEl.value : 'q1';
|
|
|
|
// Save data
|
|
if (!monthId || !date) {
|
|
return;
|
|
}
|
|
|
|
const data = {
|
|
month_id: monthId,
|
|
date: date,
|
|
user_sales: userSales,
|
|
system_sales: systemSales,
|
|
expense_description: expenseDesc,
|
|
expense_amount: expenseAmount,
|
|
expense_type: expenseType
|
|
};
|
|
|
|
// Send data via fetch
|
|
fetch('{{ route("calendar.day.store") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify(data)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
window.location.reload();
|
|
} else {
|
|
console.error('Error saving:', data.message);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|
|
|
|
@push('styles')
|
|
<style>
|
|
.fc {
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.fc .fc-toolbar-title {
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.fc .fc-daygrid-day-number {
|
|
padding: 8px;
|
|
}
|
|
|
|
.fc-event {
|
|
padding: 2px 4px;
|
|
font-size: 0.75rem;
|
|
border-radius: 4px;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.fc .fc-col-header-cell-cushion {
|
|
padding: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.fc-daygrid-day.has-data {
|
|
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
}
|
|
|
|
.fc-daygrid-day.has-expense {
|
|
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
|
|
}
|
|
|
|
.fc-daygrid-day.has-both {
|
|
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
|
}
|
|
|
|
.day-cell-clickable {
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.day-cell-clickable:hover {
|
|
background: #e3f2fd !important;
|
|
}
|
|
</style>
|
|
@endpush |