feat: Add Aylık Yakıt Dökümü (Monthly Fuel Report) feature for goods managers

- Add new sidebar menu item "Aylık Yakıt Dökümü" with fa-list-ol icon
- Implement year selection dropdown with current year and 3 previous years
- Create collapsible accordion interface for monthly fuel data
- Display approved fuel slips grouped by month with expandable sections
- Show S.No, Araç Plakası, Tarih, Yakıt Cinsi, Miktar, Teslim Alan, Teslim Eden columns
- Filter data by selected year and goods manager ID
- Sort data chronologically within each month
- Hide future months from display
- Include year summary statistics and empty state handling
- Responsive design matching existing UI patterns
- Turkish month names and localization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-06 20:26:19 +03:00
parent 2a922c735e
commit b978902c7f
3 changed files with 808 additions and 2 deletions

View File

@@ -0,0 +1,577 @@
<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);
}
onMount(async () => {
await loadFuelData();
});
async function loadFuelData() {
if (!user) {
error = 'Kullanıcı bilgisi bulunamadı.';
loading = false;
return;
}
loading = true;
error = '';
try {
// Get approved fuel slips for this goods manager for the selected year
const startDate = `${selectedYear}-01-01`;
const endDate = `${selectedYear}-12-31`;
const url = `/api/fuel-slips?manager_id=${user.id}&status=approved`;
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)); // Sort by date descending
console.log(`✅ Loaded ${fuelData.length} fuel slips for year ${selectedYear}`);
} else {
error = 'Yakıt verileri yüklenemedi.';
}
} catch (err) {
console.error('Load fuel data error:', err);
error = 'Bağlantı hatası.';
} finally {
loading = false;
}
}
function toggleMonth(monthIndex) {
if (expandedMonths.has(monthIndex)) {
expandedMonths.delete(monthIndex);
} else {
expandedMonths.add(monthIndex);
}
expandedMonths = expandedMonths; // Trigger reactivity
}
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;
}
$: monthlyData = Array.from({length: 12}, (_, i) => ({
month: i,
data: getMonthData(i),
total: getMonthTotal(i),
count: getSlipCount(i),
isExpanded: expandedMonths.has(i)
}));
</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)}
{#if !isMonthInFuture(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>
{/if}
{/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: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.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>