Files
ytp-glm/src/routes/goods-manager/+page.svelte
2025-11-05 19:51:37 +03:00

816 lines
17 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<svelte:head>
<style>
body {
background: #F2F3F7 !important;
}
</style>
</svelte:head>
<script>
import { onMount, onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import { io } from 'socket.io-client';
let user = null;
let assignedSlips = [];
let loading = true;
let error = '';
let successMessage = '';
let showApprovalModal = false;
let showRejectionModal = false;
let selectedSlip = null;
let socket = null;
// Form değişkenleri
let approvalNotes = '';
let rejectionNotes = '';
onMount(async () => {
// Artık mal sorumlu işlemleri dashboard içinde SPA olarak çalışıyor
goto('/dashboard');
return;
// Socket.IO bağlantısı
socket = io('http://localhost:3000');
// Yeni fiş atandığında bildirim
socket.on('fuel-slip-assigned', (data) => {
if (data.goods_manager_id === user.id) {
loadAssignedSlips();
successMessage = 'Yeni yakıt fişi atandı!';
setTimeout(() => successMessage = '', 3000);
}
});
// Fiş durumu güncellendiğinde listeyi yenile
socket.on('fuel-slip-updated', (data) => {
if (data.goods_manager_id === user.id) {
loadAssignedSlips();
}
});
await loadAssignedSlips();
});
async function loadAssignedSlips() {
try {
const response = await fetch(`/api/fuel-slips?manager_id=${user.id}&status=pending`);
if (response.ok) {
const data = await response.json();
assignedSlips = data.fuelSlips || [];
} else {
error = 'Atanan fişler yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load assigned slips error:', err);
} finally {
loading = false;
}
}
function openApprovalModal(slip) {
selectedSlip = slip;
approvalNotes = '';
showApprovalModal = true;
error = '';
successMessage = '';
}
function openRejectionModal(slip) {
selectedSlip = slip;
rejectionNotes = '';
showRejectionModal = true;
error = '';
successMessage = '';
}
function closeModals() {
showApprovalModal = false;
showRejectionModal = false;
selectedSlip = null;
approvalNotes = '';
rejectionNotes = '';
}
async function handleApproveSlip() {
if (!selectedSlip) return;
try {
const response = await fetch('/api/fuel-slips', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedSlip.id,
status: 'approved',
approval_notes: approvalNotes
}),
});
const data = await response.json();
if (response.ok) {
successMessage = 'Fiş başarıyla onaylandı!';
await loadAssignedSlips();
closeModals();
} else {
error = data.message || 'Fiş onaylanamadı.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Approve slip error:', err);
}
}
async function handleRejectSlip() {
if (!selectedSlip || !rejectionNotes.trim()) {
error = 'Red gerekçesi zorunludur.';
return;
}
try {
const response = await fetch('/api/fuel-slips', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedSlip.id,
status: 'rejected',
approval_notes: rejectionNotes
}),
});
const data = await response.json();
if (response.ok) {
successMessage = 'Fiş başarıyla reddedildi!';
await loadAssignedSlips();
closeModals();
} else {
error = data.message || 'Fiş reddedilemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Reject slip error:', err);
}
}
function getFuelTypeIcon(type) {
return type === 'benzin' ? '⛽' : '🛢️';
}
function getPriorityClass(liters) {
if (liters > 100) return 'priority-high';
if (liters > 50) return 'priority-medium';
return 'priority-low';
}
function handleLogout() {
localStorage.removeItem('user');
goto('/');
}
function goBack() {
goto('/dashboard');
}
// Cleanup
onDestroy(() => {
if (socket) {
socket.disconnect();
}
});
</script>
<div class="goods-manager-page">
<div class="page-header">
<div class="header-left">
<button class="btn btn-secondary" on:click={goBack}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5"/>
<path d="M12 19l-7-7 7-7"/>
</svg>
Geri
</button>
<h1 class="page-title">Atanan Yakıt Fişleri</h1>
<div class="stats-badge">
<span class="count">{assignedSlips.length}</span>
<span>Bekleyen Fiş</span>
</div>
</div>
<div class="header-right">
<span class="user-info">👤 {user?.full_name}</span>
<button class="btn btn-inactive" on:click={handleLogout}>
Çıkış
</button>
</div>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if successMessage}
<div class="success-message">
{successMessage}
</div>
{/if}
{#if loading}
<div class="loading-container">
<div class="spinner"></div>
<p>Yükleniyor...</p>
</div>
{:else if assignedSlips.length === 0}
<div class="empty-state">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11H3v10h6V11z"/>
<path d="M21 11h-6v10h6V11z"/>
<path d="M14 3v4h-4V3"/>
<path d="M17 7V3h-4v4"/>
<path d="M7 7V3H3v4"/>
<path d="M21 7v-4h-4v4"/>
</svg>
</div>
<h3>Onay Bekleyen Fiş Yok</h3>
<p>Size atanan yeni yakıt fişleri olmadığında burada görünecekler.</p>
</div>
{:else}
<div class="slips-container">
{#each assignedSlips as slip (slip.id)}
<div class="slip-card card {getPriorityClass(slip.liters)}">
<div class="slip-header">
<div class="slip-info">
<h3 class="slip-title">
{getFuelTypeIcon(slip.fuel_type)} {slip.liters}L {slip.fuel_type === 'benzin' ? 'Benzin' : 'Motorin'}
</h3>
<p class="slip-date">{new Date(slip.date).toLocaleDateString('tr-TR')}</p>
</div>
<div class="priority-indicator">
{#if slip.liters > 100}
<span class="priority-badge high"><i class="fas fa-exclamation-triangle"></i> Yüksek Öncelik</span>
{:else if slip.liters > 50}
<span class="priority-badge medium"><i class="fas fa-exclamation-circle"></i> Orta Öncelik</span>
{:else}
<span class="priority-badge low"><i class="fas fa-info-circle"></i> Düşük Öncelik</span>
{/if}
</div>
</div>
<div class="slip-details">
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">🚗 Araç:</span>
<span class="detail-value">{slip.vehicle_info.brand} {slip.vehicle_info.model}</span>
</div>
<div class="detail-item">
<span class="detail-label">📝 Plaka:</span>
<span class="detail-value">{slip.vehicle_info.plate}</span>
</div>
<div class="detail-item">
<span class="detail-label">🏢 Birlik:</span>
<span class="detail-value">{slip.unit_name}</span>
</div>
<div class="detail-item">
<span class="detail-label">👤 Personel:</span>
<span class="detail-value">{slip.personnel_info.rank} {slip.personnel_info.full_name}</span>
</div>
<div class="detail-item">
<span class="detail-label">📊 KM:</span>
<span class="detail-value">{slip.km.toLocaleString('tr-TR')} km</span>
</div>
<div class="detail-item">
<span class="detail-label">⏰ Oluşturma:</span>
<span class="detail-value">{new Date(slip.created_at).toLocaleString('tr-TR')}</span>
</div>
</div>
{#if slip.notes}
<div class="notes-section">
<span class="detail-label">📄 Notlar:</span>
<p class="notes-text">{slip.notes}</p>
</div>
{/if}
</div>
<div class="slip-actions">
<button class="btn btn-success" on:click={() => openApprovalModal(slip)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
Onayla
</button>
<button class="btn btn-danger" on:click={() => openRejectionModal(slip)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
Reddet
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Onay Modal -->
{#if showApprovalModal}
<div class="modal-overlay" on:click={closeModals}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>✅ Fişi Onayla</h2>
<button class="modal-close" on:click={closeModals}>×</button>
</div>
<div class="modal-content">
{#if selectedSlip}
<div class="slip-summary">
<h3>{selectedSlip.liters}L {selectedSlip.fuel_type === 'benzin' ? 'Benzin' : 'Motorin'}</h3>
<p>Araç: {selectedSlip.vehicle_info.brand} {selectedSlip.vehicle_info.model} ({selectedSlip.vehicle_info.plate})</p>
<p>Birlik: {selectedSlip.unit_name}</p>
</div>
<form on:submit|preventDefault={handleApproveSlip}>
<div class="form-group">
<label for="approval-notes">Onay Notları (Opsiyonel)</label>
<textarea
id="approval-notes"
class="form-textarea"
bind:value={approvalNotes}
placeholder="Onay gerekçesi..."
rows="3"
></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModals}>İptal</button>
<button type="submit" class="btn btn-success">Onayla</button>
</div>
</form>
{/if}
</div>
</div>
</div>
{/if}
<!-- Reddetme Modal -->
{#if showRejectionModal}
<div class="modal-overlay" on:click={closeModals}>
<div class="modal" on:click|stopPropagation>
<div class="modal-header">
<h2>❌ Fişi Reddet</h2>
<button class="modal-close" on:click={closeModals}>×</button>
</div>
<div class="modal-content">
{#if selectedSlip}
<div class="slip-summary">
<h3>{selectedSlip.liters}L {selectedSlip.fuel_type === 'benzin' ? 'Benzin' : 'Motorin'}</h3>
<p>Araç: {selectedSlip.vehicle_info.brand} {selectedSlip.vehicle_info.model} ({selectedSlip.vehicle_info.plate})</p>
<p>Birlik: {selectedSlip.unit_name}</p>
</div>
<form on:submit|preventDefault={handleRejectSlip}>
<div class="form-group">
<label for="rejection-notes">Red Gerekçesi *</label>
<textarea
id="rejection-notes"
class="form-textarea"
bind:value={rejectionNotes}
placeholder="Reddetme nedenini belirtin..."
rows="3"
required
></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModals}>İptal</button>
<button type="submit" class="btn btn-danger">Reddet</button>
</div>
</form>
{/if}
</div>
</div>
</div>
{/if}
<style>
.goods-manager-page {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
gap: 1rem;
flex-wrap: wrap;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: var(--text-color);
margin: 0;
}
.stats-badge {
background: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.5rem;
border-radius: 10px;
font-weight: 700;
}
.user-info {
color: var(--text-secondary);
font-weight: 500;
}
.error-message {
background: #FEE2E2;
color: #DC2626;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #FECACA;
}
.success-message {
background: #D1FAE5;
color: #059669;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid #A7F3D0;
}
.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 {
color: var(--text-secondary);
margin-bottom: 1rem;
}
.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;
}
.slips-container {
display: grid;
gap: 1.5rem;
}
.slip-card {
background: white;
border: 1px solid var(--card-border-color);
border-radius: 12px;
padding: 1.5rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.slip-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.slip-card.priority-high {
border-left: 4px solid #DC2626;
background: #FEF2F2;
}
.slip-card.priority-medium {
border-left: 4px solid #F59E0B;
background: #FFFBEB;
}
.slip-card.priority-low {
border-left: 4px solid #10B981;
background: #F0FDF4;
}
.slip-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border-color);
}
.slip-title {
font-size: 1.3rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.25rem 0;
}
.slip-date {
color: var(--text-secondary);
margin: 0;
font-size: 0.9rem;
}
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.priority-badge.high {
background: #DC2626;
color: white;
}
.priority-badge.medium {
background: #F59E0B;
color: white;
}
.priority-badge.low {
background: #10B981;
color: white;
}
.slip-details {
margin-bottom: 1.5rem;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
background: #F9FAFB;
border-radius: 6px;
}
.detail-label {
font-weight: 500;
color: var(--text-secondary);
font-size: 0.9rem;
}
.detail-value {
font-weight: 600;
color: var(--text-color);
text-align: right;
}
.notes-section {
background: #F9FAFB;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.notes-text {
margin: 0.5rem 0 0 0;
color: var(--text-color);
line-height: 1.5;
}
.slip-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.btn-success {
background: #10B981;
color: white;
border: 1px solid #059669;
}
.btn-success:hover {
background: #059669;
}
.btn-danger {
background: #DC2626;
color: white;
border: 1px solid #B91C1C;
}
.btn-danger:hover {
background: #B91C1C;
}
/* Modal Stilleri */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--card-border-color);
}
.modal-header h2 {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
color: var(--text-color);
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
}
.modal-content {
padding: 1.5rem;
}
.slip-summary {
background: #F9FAFB;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.slip-summary h3 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
font-size: 1.1rem;
}
.slip-summary p {
margin: 0.25rem 0;
color: var(--text-secondary);
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.modal-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--card-border-color);
}
/* Responsive Tasarım */
@media (max-width: 768px) {
.goods-manager-page {
padding: 1rem;
}
.page-header {
flex-direction: column;
align-items: stretch;
gap: 1rem;
}
.header-left {
flex-direction: column;
gap: 0.5rem;
}
.page-title {
font-size: 1.5rem;
}
.detail-grid {
grid-template-columns: 1fr;
}
.detail-item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.detail-value {
text-align: left;
}
.slip-actions {
justify-content: stretch;
}
.modal {
margin: 0;
max-height: 100vh;
}
.modal-actions {
flex-direction: column;
}
}
</style>