611 lines
12 KiB
Svelte
611 lines
12 KiB
Svelte
<script>
|
||
import { onMount } from 'svelte';
|
||
|
||
export let user = null;
|
||
|
||
let selectedYear = new Date().getFullYear();
|
||
let fuelData = [];
|
||
let loading = true;
|
||
let error = '';
|
||
let expandedMonths = new Set();
|
||
let currentYear = new Date().getFullYear();
|
||
let currentMonth = new Date().getMonth();
|
||
|
||
// Turkish month names
|
||
const turkishMonths = [
|
||
'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran',
|
||
'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'
|
||
];
|
||
|
||
// Generate available years (current year and 3 years back)
|
||
let availableYears = [];
|
||
for (let i = 0; i < 4; i++) {
|
||
availableYears.push(currentYear - i);
|
||
}
|
||
|
||
let initialLoadDone = false;
|
||
|
||
onMount(async () => {
|
||
await loadFuelData();
|
||
initialLoadDone = true;
|
||
});
|
||
|
||
// Watch for year changes and reload data
|
||
$: if (initialLoadDone && selectedYear) {
|
||
loadFuelData();
|
||
expandedMonths = new Set();
|
||
}
|
||
|
||
async function loadFuelData() {
|
||
if (!user) {
|
||
error = 'Kullanıcı bilgisi bulunamadı.';
|
||
loading = false;
|
||
return;
|
||
}
|
||
|
||
loading = true;
|
||
error = '';
|
||
|
||
try {
|
||
const params = new URLSearchParams({ status: 'approved' });
|
||
if (user.unit_id) {
|
||
params.set('unit_id', user.unit_id);
|
||
} else if (user.id) {
|
||
params.set('manager_id', user.id);
|
||
} else {
|
||
throw new Error('Kullanıcı bilgilerinde eksik alan var.');
|
||
}
|
||
const url = `/api/fuel-slips?${params.toString()}`;
|
||
const response = await fetch(url);
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const allSlips = data.fuelSlips || [];
|
||
|
||
// Filter slips for the selected year and sort by date
|
||
fuelData = allSlips
|
||
.filter(slip => {
|
||
const slipDate = new Date(slip.date);
|
||
return slipDate.getFullYear() === selectedYear;
|
||
})
|
||
.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||
} else {
|
||
error = 'Yakıt verileri yüklenemedi.';
|
||
}
|
||
} catch (err) {
|
||
error = `Bağlantı hatası: ${err.message}`;
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
function toggleMonth(monthIndex) {
|
||
if (expandedMonths.has(monthIndex)) {
|
||
expandedMonths.delete(monthIndex);
|
||
} else {
|
||
expandedMonths.add(monthIndex);
|
||
}
|
||
expandedMonths = expandedMonths;
|
||
}
|
||
|
||
function getMonthData(monthIndex) {
|
||
return fuelData.filter(slip => {
|
||
const slipDate = new Date(slip.date);
|
||
return slipDate.getMonth() === monthIndex;
|
||
});
|
||
}
|
||
|
||
function isMonthInFuture(monthIndex) {
|
||
return selectedYear === currentYear && monthIndex > currentMonth;
|
||
}
|
||
|
||
function getFuelTypeLabel(type) {
|
||
return type === 'benzin' ? '⛽ Benzin' : '🛢️ Motorin';
|
||
}
|
||
|
||
function getMonthTotal(monthIndex) {
|
||
const monthData = getMonthData(monthIndex);
|
||
return monthData.reduce((total, slip) => total + (slip.liters || 0), 0);
|
||
}
|
||
|
||
function getYearTotal() {
|
||
return fuelData.reduce((total, slip) => total + (slip.liters || 0), 0);
|
||
}
|
||
|
||
function formatDate(dateString) {
|
||
return new Date(dateString).toLocaleDateString('tr-TR');
|
||
}
|
||
|
||
function formatLiters(liters) {
|
||
return Number(liters).toFixed(1);
|
||
}
|
||
|
||
function getSlipCount(monthIndex) {
|
||
return getMonthData(monthIndex).length;
|
||
}
|
||
|
||
function getMonthDataSummary() {
|
||
const months = ['Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran', 'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'];
|
||
return months.map((month, index) => ({
|
||
month,
|
||
count: getSlipCount(index),
|
||
total: getMonthTotal(index)
|
||
})).filter(m => m.count > 0);
|
||
}
|
||
|
||
// Reactive statement that rebuilds when fuelData or expandedMonths changes
|
||
$: monthlyData = (() => {
|
||
const months = [];
|
||
|
||
// Determine max month to show (don't show future months)
|
||
const maxMonth = selectedYear === currentYear ? currentMonth : 11;
|
||
|
||
// Show months from maxMonth down to January
|
||
for (let i = maxMonth; i >= 0; i--) {
|
||
const data = getMonthData(i);
|
||
const monthData = {
|
||
month: i,
|
||
data: data,
|
||
total: getMonthTotal(i),
|
||
count: data.length,
|
||
isExpanded: expandedMonths.has(i)
|
||
};
|
||
months.push(monthData);
|
||
}
|
||
|
||
return months;
|
||
})();
|
||
</script>
|
||
|
||
<div class="monthly-fuel-report">
|
||
<div class="report-header">
|
||
<h1 class="report-title">Aylık Yakıt Dökümü</h1>
|
||
<div class="year-selector">
|
||
<label for="year-select">Yıl:</label>
|
||
<select id="year-select" bind:value={selectedYear} on:change={loadFuelData}>
|
||
{#each availableYears as year}
|
||
<option value={year}>{year}</option>
|
||
{/each}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{#if error}
|
||
<div class="error-message">
|
||
{error}
|
||
</div>
|
||
{/if}
|
||
|
||
{#if loading}
|
||
<div class="loading-container">
|
||
<div class="spinner"></div>
|
||
<p>Yükleniyor...</p>
|
||
</div>
|
||
{:else if fuelData.length === 0}
|
||
<div class="empty-state">
|
||
<div class="empty-icon">
|
||
<i class="fa-solid fa-chart-line"></i>
|
||
</div>
|
||
<h3>Veri Bulunamadı</h3>
|
||
<p>{selectedYear} yılı için onaylı yakıt fişi bulunamadı.</p>
|
||
</div>
|
||
{:else}
|
||
<!-- Year Summary -->
|
||
<div class="year-summary card">
|
||
<div class="summary-item">
|
||
<span class="summary-label">Yıl Toplamı:</span>
|
||
<span class="summary-value">{formatLiters(getYearTotal())} Litre</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">Toplam Fiş:</span>
|
||
<span class="summary-value">{fuelData.length} Adet</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Monthly Accordion -->
|
||
<div class="months-container">
|
||
{#each monthlyData as month (month.month)}
|
||
<div class="month-card">
|
||
<div
|
||
class="month-header"
|
||
class:has-data={month.count > 0}
|
||
class:expanded={month.isExpanded}
|
||
on:click={() => toggleMonth(month.month)}
|
||
>
|
||
<div class="month-info">
|
||
<h3 class="month-name">
|
||
{turkishMonths[month.month]} {selectedYear}
|
||
</h3>
|
||
{#if month.count > 0}
|
||
<div class="month-stats">
|
||
<span class="stat-badge">{month.count} Fiş</span>
|
||
<span class="stat-badge">{formatLiters(month.total)}L</span>
|
||
</div>
|
||
{:else}
|
||
<div class="month-stats">
|
||
<span class="stat-badge empty">Veri Yok</span>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<div class="expand-icon">
|
||
<i class="fa-solid fa-chevron-{month.isExpanded ? 'up' : 'down'}"></i>
|
||
</div>
|
||
</div>
|
||
|
||
{#if month.isExpanded && month.count > 0}
|
||
<div class="month-content">
|
||
<div class="fuel-table-container">
|
||
<table class="fuel-table">
|
||
<thead>
|
||
<tr>
|
||
<th>S.No</th>
|
||
<th>Araç Plakası</th>
|
||
<th>Tarih</th>
|
||
<th>Yakıt Cinsi</th>
|
||
<th>Miktar</th>
|
||
<th>Teslim Alan</th>
|
||
<th>Teslim Eden</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{#each month.data.sort((a, b) => new Date(a.date) - new Date(b.date)) as slip, index}
|
||
<tr>
|
||
<td>{index + 1}</td>
|
||
<td class="plate-cell">{slip.vehicle_info?.plate || '-'}</td>
|
||
<td>{formatDate(slip.date)}</td>
|
||
<td>{getFuelTypeLabel(slip.fuel_type)}</td>
|
||
<td class="quantity-cell">{formatLiters(slip.liters)}</td>
|
||
<td>{slip.personnel_info?.rank} {slip.personnel_info?.full_name || '-'}</td>
|
||
<td>{slip.fuel_manager_info?.full_name || '-'}</td>
|
||
</tr>
|
||
{/each}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<style>
|
||
.monthly-fuel-report {
|
||
padding: 0;
|
||
max-width: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.report-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 2rem;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.report-title {
|
||
font-size: 1.8rem;
|
||
font-weight: 700;
|
||
color: var(--text-color);
|
||
margin: 0;
|
||
}
|
||
|
||
.year-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
background: white;
|
||
padding: 0.75rem 1rem;
|
||
border: 1px solid var(--card-border-color);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.year-selector label {
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
}
|
||
|
||
.year-selector select {
|
||
padding: 0.5rem;
|
||
border: 1px solid #E5E7EB;
|
||
border-radius: 6px;
|
||
font-weight: 600;
|
||
background: white;
|
||
cursor: pointer;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.error-message {
|
||
background: #FEE2E2;
|
||
color: #DC2626;
|
||
padding: 1rem;
|
||
border-radius: 8px;
|
||
margin-bottom: 1.5rem;
|
||
border: 1px solid #FECACA;
|
||
}
|
||
|
||
.loading-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 3rem;
|
||
}
|
||
|
||
.spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid #E5E7EB;
|
||
border-top: 4px solid var(--primary-color);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 3rem;
|
||
background: white;
|
||
border-radius: 12px;
|
||
border: 1px solid var(--card-border-color);
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 4rem;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 1rem;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-state h3 {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.empty-state p {
|
||
color: var(--text-secondary);
|
||
margin: 0;
|
||
}
|
||
|
||
.year-summary {
|
||
display: flex;
|
||
gap: 2rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 2rem;
|
||
background: #E0E7FF;
|
||
color: #4338CA;
|
||
border: 1px solid #C7D2FE;
|
||
}
|
||
|
||
.summary-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.summary-label {
|
||
font-size: 0.9rem;
|
||
opacity: 0.9;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.summary-value {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.months-container {
|
||
display: grid;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.month-card {
|
||
background: white;
|
||
border: 1px solid var(--card-border-color);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.month-card:hover {
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.month-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 1.25rem 1.5rem;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition: all 0.2s ease;
|
||
background: #FAFBFC;
|
||
}
|
||
|
||
.month-header:hover {
|
||
background: #F3F4F6;
|
||
}
|
||
|
||
.month-header.has-data {
|
||
background: linear-gradient(135deg, #F8FAFC 0%, #E2E8F0 100%);
|
||
}
|
||
|
||
.month-header.expanded {
|
||
background: var(--primary-color);
|
||
color: white;
|
||
}
|
||
|
||
.month-header.expanded .month-name,
|
||
.month-header.expanded .stat-badge {
|
||
color: white;
|
||
}
|
||
|
||
.month-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.month-name {
|
||
font-size: 1.2rem;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
margin: 0 0 0.5rem 0;
|
||
}
|
||
|
||
.month-stats {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.stat-badge {
|
||
background: rgba(59, 130, 246, 0.1);
|
||
color: #1E40AF;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 20px;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stat-badge.empty {
|
||
background: rgba(156, 163, 175, 0.1);
|
||
color: #6B7280;
|
||
}
|
||
|
||
.expand-icon {
|
||
font-size: 1rem;
|
||
transition: transform 0.2s ease;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.month-header.expanded .expand-icon {
|
||
color: white;
|
||
}
|
||
|
||
.month-content {
|
||
border-top: 1px solid var(--card-border-color);
|
||
background: white;
|
||
}
|
||
|
||
.fuel-table-container {
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.fuel-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.fuel-table th {
|
||
background: #F9FAFB;
|
||
padding: 1rem;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
border-bottom: 2px solid #E5E7EB;
|
||
font-size: 0.8rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.fuel-table td {
|
||
padding: 1rem;
|
||
border-bottom: 1px solid #F3F4F6;
|
||
color: var(--text-color);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.fuel-table tbody tr:hover {
|
||
background: #F9FAFB;
|
||
}
|
||
|
||
.plate-cell {
|
||
font-weight: 600;
|
||
color: var(--primary-color);
|
||
}
|
||
|
||
.quantity-cell {
|
||
font-weight: 600;
|
||
color: #059669;
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 768px) {
|
||
.report-header {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.report-title {
|
||
font-size: 1.5rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.year-selector {
|
||
justify-content: center;
|
||
}
|
||
|
||
.year-summary {
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
text-align: center;
|
||
}
|
||
|
||
.month-header {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.month-name {
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.month-stats {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.fuel-table {
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.fuel-table th,
|
||
.fuel-table td {
|
||
padding: 0.75rem 0.5rem;
|
||
}
|
||
|
||
.stat-badge {
|
||
font-size: 0.7rem;
|
||
padding: 0.2rem 0.6rem;
|
||
}
|
||
}
|
||
|
||
/* Animation */
|
||
.month-content {
|
||
animation: slideDown 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
max-height: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
max-height: 2000px;
|
||
}
|
||
}
|
||
</style>
|