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:
577
src/lib/components/MonthlyFuelReportContent.svelte
Normal file
577
src/lib/components/MonthlyFuelReportContent.svelte
Normal 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>
|
||||
Reference in New Issue
Block a user