Initial commit: Yakıt Takip Modülü - Akaryakıt İstasyonu Yönetim Sistemi

🚀 Features Implemented:
- Full-stack SvelteKit application with Express backend
- Role-based authentication (Admin, Fuel Manager, Goods Manager)
- Real-time notifications with Socket.IO
- SQLite database with auto-initialization in /db directory
- Comprehensive user management and fuel slip tracking
- Responsive design with Turkish language support

🏗️ Architecture:
- Frontend: Svelte + SvelteKit + Vite
- Backend: Node.js + Express + Socket.IO
- Database: SQLite3 with automatic schema creation
- Security: bcrypt password hashing + session management
- Real-time: Socket.IO for instant notifications

📁 Project Structure:
- Organized documentation in /docs directory
- Database files in /db directory with auto-setup
- Clean separation of API routes and UI components
- Comprehensive documentation including processes, architecture, and user guides

📚 Documentation:
- PROJECT_PROCESSES.md: Comprehensive project documentation
- KNOWLEDGE_BASE.md: Quick reference and user guide
- TEST_GUIDE.md: Testing and quality assurance guide
- README_ADMIN_FEATURES.md: Admin functionality guide
- Full API documentation and system architecture

🔒 Security Features:
- Role-based authorization system
- Encrypted password storage with bcrypt
- Session-based authentication
- SQL injection protection with parameterized queries
- CORS configuration and input validation

🎯 Key Features:
- Fuel slip creation and approval workflow
- Real-time notifications between users
- PDF generation for fuel slips
- User and vehicle management
- Comprehensive audit logging

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-05 08:35:13 +03:00
commit 4205a8d387
31 changed files with 11678 additions and 0 deletions

View File

@@ -0,0 +1,928 @@
<svelte:head>
<style>
body {
background: #F2F3F7 !important;
}
</style>
</svelte:head>
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
let user = null;
let units = [];
let loading = true;
let error = '';
let showAddModal = false;
let showEditModal = false;
let selectedUnit = null;
// Form değişkenleri
let formData = {
name: '',
address: '',
stk: '',
btk: '',
commander: {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: ''
}
};
onMount(async () => {
const userData = localStorage.getItem('user');
if (!userData || JSON.parse(userData).role !== 'admin') {
goto('/dashboard');
return;
}
user = JSON.parse(userData);
await loadUnits();
});
async function loadUnits() {
try {
const response = await fetch('/api/units');
if (response.ok) {
const data = await response.json();
units = data.units;
} else {
error = 'Birlikler yüklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Load units error:', err);
} finally {
loading = false;
}
}
function resetForm() {
formData = {
name: '',
address: '',
stk: '',
btk: '',
commander: {
full_name: '',
rank: '',
registration_number: '',
tc_kimlik: '',
phone: ''
}
};
selectedUnit = null;
}
function openAddModal() {
resetForm();
showAddModal = true;
}
function openEditModal(unit) {
selectedUnit = unit;
formData = {
name: unit.name,
address: unit.address,
stk: unit.stk,
btk: unit.btk,
commander: { ...unit.commander }
};
showEditModal = true;
}
function closeModal() {
showAddModal = false;
showEditModal = false;
resetForm();
}
async function handleAddUnit() {
if (!formData.name || !formData.address || !formData.stk || !formData.btk) {
error = 'Tüm alanlar zorunludur.';
return;
}
const { commander } = formData;
if (!commander.full_name || !commander.rank || !commander.registration_number || !commander.tc_kimlik || !commander.phone) {
error = 'Birlik sorumlusunun tüm bilgileri zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(commander.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/units', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
closeModal();
error = '';
} else {
error = data.message || 'Birlik eklenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Add unit error:', err);
}
}
async function handleUpdateUnit() {
if (!formData.name || !formData.address || !formData.stk || !formData.btk) {
error = 'Tüm alanlar zorunludur.';
return;
}
const { commander } = formData;
if (!commander.full_name || !commander.rank || !commander.registration_number || !commander.tc_kimlik || !commander.phone) {
error = 'Birlik sorumlusunun tüm bilgileri zorunludur.';
return;
}
if (!/^[0-9]{11}$/.test(commander.tc_kimlik)) {
error = 'TC Kimlik numarası 11 haneli olmalıdır.';
return;
}
try {
const response = await fetch('/api/units', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: selectedUnit.id,
...formData
}),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
closeModal();
error = '';
} else {
error = data.message || 'Birlik güncellenemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Update unit error:', err);
}
}
async function handleDeleteUnit(unit) {
if (!confirm(`${unit.name} birliğini silmek istediğinizden emin misiniz?`)) {
return;
}
try {
const response = await fetch('/api/units', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: unit.id }),
});
const data = await response.json();
if (response.ok) {
await loadUnits();
error = '';
} else {
error = data.message || 'Birlik silinemedi.';
}
} catch (err) {
error = 'Bağlantı hatası.';
console.error('Delete unit error:', err);
}
}
function goBack() {
goto('/dashboard');
}
</script>
<div class="units-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">Birlik Yönetimi</h1>
</div>
<button class="btn btn-primary" on:click={openAddModal}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
Yeni Birlik Ekle
</button>
</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 units.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="M3 21h18"/>
<path d="M5 21V7l8-4v18"/>
<path d="M19 21V11l-6-4"/>
</svg>
</div>
<h3>Henüz Birlik Yok</h3>
<p>Sisteme birlik eklemek için "Yeni Birlik Ekle" butonuna tıklayın.</p>
<button class="btn btn-primary" on:click={openAddModal}>
İlk Birliği Ekle
</button>
</div>
{:else}
<div class="units-grid">
{#each units as unit (unit.id)}
<div class="unit-card card">
<div class="unit-header">
<div class="unit-info">
<h3 class="unit-name">{unit.name}</h3>
<p class="unit-address">{unit.address}</p>
</div>
</div>
<div class="unit-details">
<div class="detail-item">
<span class="detail-label">STK:</span>
<span class="detail-value">{unit.stk}</span>
</div>
<div class="detail-item">
<span class="detail-label">BTK:</span>
<span class="detail-value">{unit.btk}</span>
</div>
</div>
<div class="commander-section">
<h4 class="commander-title">Birlik Sorumlusu</h4>
<div class="commander-info">
<div class="commander-details">
<p class="commander-name">{unit.commander.rank} {unit.commander.full_name}</p>
<p class="commander-detail">Sicil: {unit.commander.registration_number}</p>
<p class="commander-detail">TC: {unit.commander.tc_kimlik}</p>
<p class="commander-detail">İrtibat: {unit.commander.phone}</p>
</div>
</div>
</div>
<div class="unit-actions">
<button class="btn btn-sm btn-secondary" on:click={() => openEditModal(unit)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Düzenle
</button>
<button class="btn btn-sm btn-danger" on:click={() => handleDeleteUnit(unit)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
Sil
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Birlik Ekle Modal -->
{#if showAddModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal modal-large" on:click|stopPropagation>
<div class="modal-header">
<h2>Yeni Birlik Ekle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleAddUnit} class="modal-form">
<div class="form-section">
<h3>Birlik Bilgileri</h3>
<div class="form-group">
<label for="name">Birlik Adı</label>
<input
id="name"
type="text"
class="form-input"
bind:value={formData.name}
placeholder="1. Motorlu Piyade Tugayı"
required
/>
</div>
<div class="form-group">
<label for="address">Adres</label>
<input
id="address"
type="text"
class="form-input"
bind:value={formData.address}
placeholder="Mecidiyeköy, Şişli/İstanbul"
required
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="stk">STK</label>
<input
id="stk"
type="text"
class="form-input"
bind:value={formData.stk}
placeholder="STK-12345"
required
/>
</div>
<div class="form-group">
<label for="btk">BTK</label>
<input
id="btk"
type="text"
class="form-input"
bind:value={formData.btk}
placeholder="BTK-67890"
required
/>
</div>
</div>
</div>
<div class="form-section">
<h3>Birlik Sorumlusu</h3>
<div class="form-row">
<div class="form-group">
<label for="commander-name">Adı Soyadı</label>
<input
id="commander-name"
type="text"
class="form-input"
bind:value={formData.commander.full_name}
placeholder="Mehmet Yılmaz"
required
/>
</div>
<div class="form-group">
<label for="commander-rank">Rütbesi</label>
<input
id="commander-rank"
type="text"
class="form-input"
bind:value={formData.commander.rank}
placeholder="Yüzbaşı"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="commander-registration">Sicil No</label>
<input
id="commander-registration"
type="text"
class="form-input"
bind:value={formData.commander.registration_number}
placeholder="123456"
required
/>
</div>
<div class="form-group">
<label for="commander-phone">İrtibat No</label>
<input
id="commander-phone"
type="tel"
class="form-input"
bind:value={formData.commander.phone}
placeholder="05321234567"
required
/>
</div>
</div>
<div class="form-group">
<label for="commander-tc">TC Kimlik Numarası</label>
<input
id="commander-tc"
type="text"
class="form-input"
bind:value={formData.commander.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Kaydet</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Birlik Düzenle Modal -->
{#if showEditModal}
<div class="modal-overlay" on:click={closeModal}>
<div class="modal modal-large" on:click|stopPropagation>
<div class="modal-header">
<h2>Birlik Düzenle</h2>
<button class="modal-close" on:click={closeModal}>×</button>
</div>
<form on:submit|preventDefault={handleUpdateUnit} class="modal-form">
<div class="form-section">
<h3>Birlik Bilgileri</h3>
<div class="form-group">
<label for="edit-name">Birlik Adı</label>
<input
id="edit-name"
type="text"
class="form-input"
bind:value={formData.name}
placeholder="1. Motorlu Piyade Tugayı"
required
/>
</div>
<div class="form-group">
<label for="edit-address">Adres</label>
<input
id="edit-address"
type="text"
class="form-input"
bind:value={formData.address}
placeholder="Mecidiyeköy, Şişli/İstanbul"
required
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-stk">STK</label>
<input
id="edit-stk"
type="text"
class="form-input"
bind:value={formData.stk}
placeholder="STK-12345"
required
/>
</div>
<div class="form-group">
<label for="edit-btk">BTK</label>
<input
id="edit-btk"
type="text"
class="form-input"
bind:value={formData.btk}
placeholder="BTK-67890"
required
/>
</div>
</div>
</div>
<div class="form-section">
<h3>Birlik Sorumlusu</h3>
<div class="form-row">
<div class="form-group">
<label for="edit-commander-name">Adı Soyadı</label>
<input
id="edit-commander-name"
type="text"
class="form-input"
bind:value={formData.commander.full_name}
placeholder="Mehmet Yılmaz"
required
/>
</div>
<div class="form-group">
<label for="edit-commander-rank">Rütbesi</label>
<input
id="edit-commander-rank"
type="text"
class="form-input"
bind:value={formData.commander.rank}
placeholder="Yüzbaşı"
required
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="edit-commander-registration">Sicil No</label>
<input
id="edit-commander-registration"
type="text"
class="form-input"
bind:value={formData.commander.registration_number}
placeholder="123456"
required
/>
</div>
<div class="form-group">
<label for="edit-commander-phone">İrtibat No</label>
<input
id="edit-commander-phone"
type="tel"
class="form-input"
bind:value={formData.commander.phone}
placeholder="05321234567"
required
/>
</div>
</div>
<div class="form-group">
<label for="edit-commander-tc">TC Kimlik Numarası</label>
<input
id="edit-commander-tc"
type="text"
class="form-input"
bind:value={formData.commander.tc_kimlik}
placeholder="12345678901"
maxlength="11"
required
/>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" on:click={closeModal}>İptal</button>
<button type="submit" class="btn btn-primary">Güncelle</button>
</div>
</form>
</div>
</div>
{/if}
<style>
.units-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;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
color: var(--text-color);
margin: 0;
}
.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 {
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-bottom: 2rem;
}
.units-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.unit-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;
}
.unit-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.unit-header {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border-color);
}
.unit-name {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.5rem 0;
}
.unit-address {
color: var(--text-secondary);
margin: 0;
font-size: 0.9rem;
}
.unit-details {
margin-bottom: 1rem;
}
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.detail-label {
font-weight: 500;
color: var(--text-secondary);
}
.detail-value {
font-weight: 500;
color: var(--text-color);
}
.commander-section {
background: #F9FAFB;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.commander-title {
font-size: 1rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.75rem 0;
}
.commander-name {
font-weight: 600;
color: var(--text-color);
margin: 0 0 0.5rem 0;
}
.commander-detail {
font-size: 0.85rem;
color: var(--text-secondary);
margin: 0.25rem 0;
}
.unit-actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
.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-large {
max-width: 700px;
}
.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-form {
padding: 1.5rem;
}
.form-section {
margin-bottom: 2rem;
}
.form-section h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-color);
margin: 0 0 1rem 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--card-border-color);
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.form-row {
display: flex;
gap: 1rem;
}
.form-row .form-group {
flex: 1;
}
.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) {
.units-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;
}
.units-grid {
grid-template-columns: 1fr;
}
.unit-actions {
justify-content: stretch;
}
.modal {
margin: 0;
max-height: 100vh;
}
.modal-large {
max-width: 100%;
}
.form-row {
flex-direction: column;
gap: 0;
}
.modal-actions {
flex-direction: column;
}
.detail-item {
flex-direction: column;
gap: 0.25rem;
}
}
</style>