first commit

This commit is contained in:
2025-11-03 22:54:10 +03:00
commit 7edbab2689
21 changed files with 5760 additions and 0 deletions

65
.gitignore vendored Normal file
View File

@@ -0,0 +1,65 @@
# ===============================
# Node.js + Svelte Project .gitignore
# ===============================
# Node modules
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Dependency directories
jspm_packages/
# Build output
/dist
/build
/public/build/
.svelte-kit/
.vite/
# Environment files
.env
.env.*
!.env.example
# Logs
logs
*.log
*.pid
*.seed
*.pid.lock
# OS generated files
.DS_Store
Thumbs.db
# IDE and editor folders
.vscode/
.idea/
*.swp
# Test coverage
coverage/
.nyc_output/
# Temporary files
tmp/
temp/
# Optional npm cache
.npm/
.pnpm-store/
package-lock.json
# SvelteKit adapter outputs (e.g. for adapter-static, adapter-node)
output/
.vercel/
.netlify/
functions/
# Local development data
*.local
*.cache/

20
client/index.html Normal file
View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
integrity="sha512-TURnPpq3yYHnHzqfdWD7TmIjnNw0UMt3LJ2gq8SbBG08FQkwbKoy0eZZ+j9RnBbTK1ZBOqVaki3P6KyQx04BAg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Akaryakit Istasyonu Giris</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

20
client/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "ytp-client",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2",
"socket.io-client": "^4.7.5",
"svelte": "^4.2.12"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"vite": "^5.2.0"
}
}

266
client/src/App.svelte Normal file
View File

@@ -0,0 +1,266 @@
<script>
import { onMount } from 'svelte';
import LoginView from './components/LoginView.svelte';
import AdminPanel from './components/AdminPanel.svelte';
import RoleWelcome from './components/RoleWelcome.svelte';
import FuelManagerPanel from './components/FuelManagerPanel.svelte';
import InventoryManagerPanel from './components/InventoryManagerPanel.svelte';
import { fetchSession, logout } from './api';
let user = null;
let token = null;
let loading = false;
let feedback = '';
const roleLabels = {
admin: 'Admin',
fuel_manager: 'Yakit sorumlusu',
inventory_manager: 'Mal sorumlusu'
};
onMount(async () => {
const storedToken = typeof window !== 'undefined' ? window.localStorage.getItem('sessionToken') : null;
if (!storedToken) {
return;
}
loading = true;
token = storedToken;
try {
const session = await fetchSession(token);
user = session.user;
} catch (err) {
console.warn('Oturum dogrulanamadi:', err);
clearSession();
} finally {
loading = false;
}
});
function handleLoginSuccess(event) {
user = event.detail.user;
token = event.detail.token;
feedback = '';
if (typeof window !== 'undefined') {
window.localStorage.setItem('sessionToken', token);
}
}
async function handleLogout() {
if (!token) {
return;
}
try {
await logout(token);
} catch (err) {
console.warn('Cikis islemi tamamlanamadi:', err);
} finally {
feedback = 'Oturum kapatildi.';
clearSession();
}
}
function clearSession() {
user = null;
token = null;
if (typeof window !== 'undefined') {
window.localStorage.removeItem('sessionToken');
}
}
function roleLabel(role) {
return roleLabels[role] || role;
}
</script>
<div class="app-shell">
<header class="hero">
<div class="hero-content">
<div class="title-group">
<h1>Yakit Takip Sistemi</h1>
<p>İstasyondaki stok ve yakıt hareketlerini tek ekrandan yönetin.</p>
</div>
{#if user}
<div class="user-card">
<div class="user-info">
<span class="name">{user.displayName}</span>
<span class="role-tag">{roleLabel(user.role)}</span>
</div>
<button type="button" class="logout" on:click={handleLogout}>Cikis</button>
</div>
{/if}
</div>
</header>
<main class="content">
{#if loading}
<div class="card status">Oturum dogrulaniyor...</div>
{:else if user}
<section class="card">
{#if feedback}
<div class="feedback">{feedback}</div>
{/if}
{#if user.role === 'admin'}
<AdminPanel {token} />
{:else if user.role === 'fuel_manager'}
<FuelManagerPanel {user} {token} />
{:else if user.role === 'inventory_manager'}
<InventoryManagerPanel {user} {token} />
{:else}
<RoleWelcome {user} />
{/if}
</section>
{:else}
<section class="card">
{#if feedback}
<div class="feedback">{feedback}</div>
{/if}
<LoginView on:success={handleLoginSuccess} />
</section>
{/if}
</main>
</div>
<style>
.app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
background: transparent;
}
.hero {
padding: 1.9rem 2rem 1.4rem;
background: linear-gradient(135deg, #ffffff, #f0f4ff);
border-bottom: 1px solid #d9e1f5;
}
.hero-content {
max-width: 1280px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.title-group h1 {
margin: 0;
font-size: clamp(1.55rem, 2.8vw, 2rem);
font-weight: 600;
color: #1e2a3b;
}
.title-group p {
margin: 0.35rem 0 0;
color: #5d6a83;
font-size: 0.92rem;
}
.user-card {
background: #ffffff;
border: 1px solid #dbe5f6;
border-radius: 12px;
padding: 0.7rem 1rem;
display: flex;
align-items: center;
gap: 0.9rem;
color: #22324d;
box-shadow: 0 12px 24px rgba(30, 57, 109, 0.12);
}
.user-info {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.role-tag {
background: rgba(99, 132, 255, 0.18);
border-radius: 999px;
padding: 0.2rem 0.65rem;
text-transform: uppercase;
font-size: 0.68rem;
letter-spacing: 0.08em;
color: #4c6ef5;
}
.name {
font-size: 0.95rem;
font-weight: 600;
color: #1f2d44;
}
.logout {
padding: 0.4rem 0.85rem;
border-radius: 10px;
border: none;
background: #eff3ff;
color: #2c3d5e;
cursor: pointer;
font-weight: 500;
transition: background 0.2s ease;
}
.logout:hover {
background: #dbe6ff;
}
.content {
flex: 1;
display: flex;
justify-content: center;
padding: 3rem 1.5rem 4rem;
background: transparent;
}
.card {
background: #ffffff;
width: min(80vw, 1280px);
border-radius: 20px;
border: 1px solid #dbe3f4;
box-shadow: 0 30px 65px rgba(30, 57, 109, 0.18);
padding: clamp(1.8rem, 2vw, 2.75rem);
color: #26334d;
}
.feedback {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
border-radius: 10px;
background: #eaf2ff;
color: #2c4d8d;
border: 1px solid #c7dafc;
font-weight: 500;
}
.status {
text-align: center;
font-weight: 600;
color: #2d3d58;
}
@media (max-width: 768px) {
.hero {
padding: 1.75rem 1.5rem 1.5rem;
}
.hero-content {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.user-card {
width: 100%;
justify-content: space-between;
}
.card {
padding: 1.6rem;
}
}
</style>

224
client/src/api.js Normal file
View File

@@ -0,0 +1,224 @@
const API_BASE = import.meta.env.VITE_API_BASE || '';
async function request(path, { method = 'GET', body, token } = {}) {
const headers = {};
const options = { method, headers };
if (body) {
headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(body);
}
if (token) {
headers['x-session-token'] = token;
}
const response = await fetch(`${API_BASE}${path}`, options);
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
const message = errorBody.message || 'Beklenmeyen bir hata olustu.';
throw new Error(message);
}
if (response.status === 204) {
return null;
}
return response.json();
}
export function login(credentials) {
return request('/api/auth/login', {
method: 'POST',
body: credentials
});
}
export function logout(token) {
return request('/api/auth/logout', {
method: 'POST',
token
});
}
export function fetchSession(token) {
return request('/api/session', {
method: 'GET',
token
});
}
export function listInventoryManagers(token) {
return request('/api/inventory-managers', {
method: 'GET',
token
});
}
export function createInventoryManager(token, payload) {
return request('/api/inventory-managers', {
method: 'POST',
body: payload,
token
});
}
export function updateInventoryManager(token, id, payload) {
return request(`/api/inventory-managers/${id}`, {
method: 'PUT',
body: payload,
token
});
}
export function deleteInventoryManager(token, id) {
return request(`/api/inventory-managers/${id}`, {
method: 'DELETE',
token
});
}
export function listVehicles(token) {
return request('/api/vehicles', {
method: 'GET',
token
});
}
export function createVehicle(token, payload) {
return request('/api/vehicles', {
method: 'POST',
body: payload,
token
});
}
export function updateVehicle(token, id, payload) {
return request(`/api/vehicles/${id}`, {
method: 'PUT',
body: payload,
token
});
}
export function deleteVehicle(token, id) {
return request(`/api/vehicles/${id}`, {
method: 'DELETE',
token
});
}
export function listUnits(token) {
return request('/api/units', {
method: 'GET',
token
});
}
export function createUnit(token, payload) {
return request('/api/units', {
method: 'POST',
body: payload,
token
});
}
export function updateUnit(token, id, payload) {
return request(`/api/units/${id}`, {
method: 'PUT',
body: payload,
token
});
}
export function deleteUnit(token, id) {
return request(`/api/units/${id}`, {
method: 'DELETE',
token
});
}
export function listFuelPersonnel(token) {
return request('/api/fuel-personnel', {
method: 'GET',
token
});
}
export function createFuelPersonnel(token, payload) {
return request('/api/fuel-personnel', {
method: 'POST',
body: payload,
token
});
}
export function updateFuelPersonnel(token, id, payload) {
return request(`/api/fuel-personnel/${id}`, {
method: 'PUT',
body: payload,
token
});
}
export function deleteFuelPersonnel(token, id) {
return request(`/api/fuel-personnel/${id}`, {
method: 'DELETE',
token
});
}
export function fetchFuelResources(token) {
return request('/api/fuel/resources', {
method: 'GET',
token
});
}
export function listFuelSlips(token) {
return request('/api/fuel-slips', {
method: 'GET',
token
});
}
export function createFuelSlip(token, payload) {
return request('/api/fuel-slips', {
method: 'POST',
body: payload,
token
});
}
export async function downloadFuelSlipPdf(token, id) {
const response = await fetch(`${API_BASE}/api/fuel-slips/${id}/pdf`, {
method: 'GET',
headers: {
'x-session-token': token
}
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
const message = errorBody.message || 'PDF indirilemedi.';
throw new Error(message);
}
return response.blob();
}
export function fetchAssignedFuelSlips(token) {
return request('/api/fuel-slips/assigned', {
method: 'GET',
token
});
}
export function updateFuelSlipStatus(token, id, payload) {
return request(`/api/fuel-slips/${id}/status`, {
method: 'PATCH',
body: payload,
token
});
}

1
client/src/app.css Normal file
View File

@@ -0,0 +1 @@
@import '@fortawesome/fontawesome-free/css/all.min.css';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,782 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { io } from 'socket.io-client';
import {
fetchFuelResources,
createFuelSlip,
listFuelSlips,
downloadFuelSlipPdf
} from '../api';
import numberToWordsTr from '../lib/numberToWordsTr';
export let token;
export let user;
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || undefined;
const fuelTypes = ['Motorin', 'Kurşunsuz Benzin', 'Süper Benzin', 'Jet A-1', 'Gazyağı'];
const statusLabels = {
pending: 'Beklemede',
approved: 'Kabul',
rejected: 'Red'
};
let loading = true;
let saving = false;
let slipsLoading = false;
let feedback = null;
let forces = [];
let vehicles = [];
let units = [];
let personnel = [];
let inventoryManagers = [];
let slips = [];
let socket;
let form = {
date: new Date().toISOString().slice(0, 10),
force: '',
unitId: '',
vehicleId: '',
vehicleDescription: '',
plate: '',
fuelAmountNumber: '',
fuelAmountText: '',
fuelType: fuelTypes[0],
receiverId: '',
receiverPhone: '',
giverId: '',
giverPhone: '',
inventoryManagerId: '',
notes: ''
};
onMount(async () => {
await loadResources();
await loadSlips();
setupSocket();
});
onDestroy(() => {
socket?.disconnect();
});
async function loadResources() {
loading = true;
feedback = null;
try {
const response = await fetchFuelResources(token);
forces = response.forces || [];
vehicles = (response.vehicles || []).map((vehicle) => ({
...vehicle,
label: `${vehicle.brand} ${vehicle.model}`.trim()
}));
units = response.units || [];
personnel = response.personnel || [];
inventoryManagers = response.inventoryManagers || [];
if (forces.length > 0 && !form.force) {
form.force = forces[0];
}
if (fuelTypes.length > 0 && !form.fuelType) {
form.fuelType = fuelTypes[0];
}
if (inventoryManagers.length > 0 && !form.inventoryManagerId) {
form.inventoryManagerId = String(inventoryManagers[0].id);
}
} catch (err) {
feedback = { type: 'error', text: err.message };
} finally {
loading = false;
}
}
async function loadSlips() {
slipsLoading = true;
try {
const response = await listFuelSlips(token);
slips = response.slips || [];
} catch (err) {
feedback = { type: 'error', text: err.message };
} finally {
slipsLoading = false;
}
}
function setupSocket() {
if (typeof window === 'undefined') {
return;
}
socket?.disconnect();
socket = io(SOCKET_URL || window.location.origin, {
auth: { token },
transports: ['websocket', 'polling']
});
socket.on('fuelSlip:status', (updated) => {
let found = false;
slips = slips.map((slip) => {
if (slip.id === updated.id) {
found = true;
return updated;
}
return slip;
});
if (!found) {
slips = [updated, ...slips];
}
});
socket.on('connect_error', (err) => {
console.warn('Socket bağlantı hatası:', err.message);
});
}
function setFeedback(message, type = 'success') {
feedback = { type, text: message };
}
function handleVehicleChange(event) {
const selectedId = event.target.value;
const selected = vehicles.find((v) => String(v.id) === selectedId);
form = {
...form,
vehicleId: selectedId,
vehicleDescription: selected ? selected.label : '',
plate: selected ? selected.plate : ''
};
}
function handleReceiverChange(event) {
const id = Number(event.target.value);
form.receiverId = event.target.value;
const selected = personnel.find((p) => p.id === id);
if (selected) {
form.receiverPhone = selected.phone;
}
}
function handleGiverChange(event) {
const id = Number(event.target.value);
form.giverId = event.target.value;
const selected = personnel.find((p) => p.id === id);
if (selected) {
form.giverPhone = selected.phone;
}
}
function resetForm() {
form = {
date: new Date().toISOString().slice(0, 10),
force: forces[0] || '',
unitId: '',
vehicleId: '',
vehicleDescription: '',
plate: '',
fuelAmountNumber: '',
fuelAmountText: '',
fuelType: fuelTypes[0],
receiverId: '',
receiverPhone: '',
giverId: '',
giverPhone: '',
inventoryManagerId: inventoryManagers[0] ? String(inventoryManagers[0].id) : '',
notes: ''
};
}
$: {
if (form.fuelAmountNumber !== undefined && form.fuelAmountNumber !== null && form.fuelAmountNumber !== '') {
const words = numberToWordsTr(form.fuelAmountNumber);
const upper = words ? words.toUpperCase() : '';
if (form.fuelAmountText !== upper) {
form.fuelAmountText = upper;
}
} else if (form.fuelAmountText) {
form.fuelAmountText = '';
}
}
async function handleSubmit(event) {
event.preventDefault();
feedback = null;
saving = true;
try {
const payload = {
date: form.date,
force: form.force,
unitId: Number(form.unitId),
vehicleId: Number(form.vehicleId),
fuelAmountNumber: form.fuelAmountNumber,
fuelAmountText: form.fuelAmountText,
fuelType: form.fuelType,
receiverId: Number(form.receiverId),
receiverPhone: form.receiverPhone,
giverId: Number(form.giverId),
giverPhone: form.giverPhone,
inventoryManagerId: Number(form.inventoryManagerId),
notes: form.notes
};
const response = await createFuelSlip(token, payload);
setFeedback(`Fiş oluşturuldu. Seri No: ${response.slip.slipNumber}`);
slips = [response.slip, ...slips];
resetForm();
} catch (err) {
setFeedback(err.message, 'error');
} finally {
saving = false;
}
}
async function handleDownloadPdf(id) {
try {
const blob = await downloadFuelSlipPdf(token, id);
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.target = '_blank';
anchor.download = `akaryakit-senedi-${id}.pdf`;
anchor.click();
URL.revokeObjectURL(url);
} catch (err) {
setFeedback(err.message, 'error');
}
}
function personnelDisplay(person) {
return `${person.fullName}${person.rank}${person.registryNumber}`;
}
function formatDate(value) {
if (!value) return '';
return new Date(value).toLocaleDateString('tr-TR');
}
function formatSlipNumber(value) {
return String(value).padStart(4, '0');
}
function statusClass(status) {
return `status-tag ${status}`;
}
function showReason(reason) {
if (reason) {
alert(`Red gerekçesi: ${reason}`);
}
}
</script>
<div class="fuel-manager">
<div class="content-grid">
<form class="panel" on:submit|preventDefault={handleSubmit}>
<div class="panel-header">
<h3>İkmal senedi</h3>
{#if saving}
<span class="status">Kaydediliyor...</span>
{/if}
</div>
{#if feedback}
<div class={`feedback ${feedback.type}`}>{feedback.text}</div>
{/if}
{#if !loading && inventoryManagers.length === 0}
<div class="warning">Mal sorumlusu tanımlanmadan fiş oluşturamazsınız.</div>
{/if}
<fieldset disabled={loading || saving || inventoryManagers.length === 0}>
<div class="field-grid two">
<label>
<span>Tarih</span>
<input type="date" bind:value={form.date} required />
</label>
<label>
<span>Fiş No</span>
<input type="text" value={slips[0] ? formatSlipNumber(Number(slips[0].slipNumber) + 1) : ''} readonly placeholder="Otomatik" />
</label>
</div>
<label>
<span>Aracın ait olduğu kuvvet</span>
<select bind:value={form.force} required>
<option value="" disabled>Seçiniz</option>
{#each forces as forceOption}
<option value={forceOption}>{forceOption}</option>
{/each}
</select>
</label>
<label>
<span>Aracın ait olduğu birlik</span>
<select bind:value={form.unitId} required>
<option value="" disabled>Birlik seçin</option>
{#each units as unit}
<option value={unit.id}>{unit.name}</option>
{/each}
</select>
</label>
<label>
<span>Plaka bilgisi</span>
<select bind:value={form.vehicleId} on:change={handleVehicleChange} required>
<option value="" disabled>Plaka seçin</option>
{#each vehicles as vehicle}
<option value={String(vehicle.id)}>{vehicle.plate} {vehicle.label}</option>
{/each}
</select>
</label>
<label>
<span>Aracın cinsi</span>
<input
type="text"
value={form.vehicleDescription}
readonly
placeholder="Araç seçildiğinde otomatik dolar"
/>
</label>
<div class="field-grid two">
<label>
<span>Yakıt miktarı (rakam)</span>
<input type="number" min="0" step="0.01" bind:value={form.fuelAmountNumber} required />
</label>
<label>
<span>Yakıt miktarı (yazı)</span>
<input type="text" value={form.fuelAmountText} readonly placeholder="Otomatik yazıya döner" />
</label>
</div>
<label>
<span>Yakıt cinsi</span>
<select bind:value={form.fuelType} required>
{#each fuelTypes as type}
<option value={type}>{type}</option>
{/each}
</select>
</label>
<label>
<span>Mal sorumlusu</span>
<select bind:value={form.inventoryManagerId} required>
<option value="" disabled>Mal sorumlusu seçin</option>
{#each inventoryManagers as manager}
<option value={manager.id}>{manager.displayName || manager.username}</option>
{/each}
</select>
</label>
<div class="field-grid two">
<label>
<span>Teslim alan personel</span>
<select bind:value={form.receiverId} on:change={handleReceiverChange} required>
<option value="" disabled>Personel seçin</option>
{#each personnel as person}
<option value={String(person.id)}>{personnelDisplay(person)}</option>
{/each}
</select>
</label>
<label>
<span>Teslim alan telefon</span>
<input type="tel" bind:value={form.receiverPhone} required />
</label>
</div>
<div class="field-grid two">
<label>
<span>Teslim eden personel</span>
<select bind:value={form.giverId} on:change={handleGiverChange} required>
<option value="" disabled>Personel seçin</option>
{#each personnel as person}
<option value={String(person.id)}>{personnelDisplay(person)}</option>
{/each}
</select>
</label>
<label>
<span>Teslim eden telefon</span>
<input type="tel" bind:value={form.giverPhone} required />
</label>
</div>
<label>
<span>Not</span>
<textarea rows="2" bind:value={form.notes} placeholder="İsteğe bağlı ıklama"></textarea>
</label>
<button type="submit" class="primary" disabled={saving || loading}>
{saving ? 'Kaydediliyor...' : 'Fişi Kaydet'}
</button>
</fieldset>
</form>
<section class="panel slip-panel">
<div class="panel-header">
<h3>Oluşturulan fişler</h3>
<button type="button" class="ghost" on:click={loadSlips} disabled={slipsLoading}>
{slipsLoading ? 'Yükleniyor...' : 'Yenile'}
</button>
</div>
{#if slipsLoading}
<p class="info">Fişler yükleniyor...</p>
{:else if slips.length === 0}
<p class="info">Henüz fiş kaydı bulunmuyor.</p>
{:else}
<table class="simple-table">
<thead>
<tr>
<th>Fiş No</th>
<th>Tarih</th>
<th>Birlik</th>
<th>Mal sorumlusu</th>
<th>Araç</th>
<th>Yakıt</th>
<th>Durum</th>
<th>PDF</th>
</tr>
</thead>
<tbody>
{#each slips as slip (slip.id)}
<tr>
<td>{formatSlipNumber(slip.slipNumber)}</td>
<td>{formatDate(slip.slipDate)}</td>
<td>{slip.unitName}</td>
<td>{slip.inventoryManagerName || '-'}</td>
<td>{slip.vehicleDescription}</td>
<td>{slip.fuelAmountNumber} lt</td>
<td>
{#if slip.status === 'rejected' && slip.rejectionReason}
<button
type="button"
class={`${statusClass(slip.status)} status-tag-button`}
on:click={() => showReason(slip.rejectionReason)}
aria-label="Red gerekçesini görüntüle"
>
{statusLabels[slip.status] || slip.status}
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
</button>
{:else}
<span class={statusClass(slip.status)}>
{statusLabels[slip.status] || slip.status}
</span>
{/if}
</td>
<td>
<button type="button" class="ghost" on:click={() => handleDownloadPdf(slip.id)}>
PDF indir
</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</section>
</div>
</div>
<style>
.fuel-manager {
display: grid;
gap: 1.5rem;
color: #1f2d44;
}
.hero-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
background: linear-gradient(135deg, #f7f9ff, #eef2ff);
border: 1px solid #dbe3f6;
border-radius: 18px;
padding: 1.6rem 2rem;
box-shadow: 0 18px 32px rgba(80, 110, 185, 0.12);
}
.hero-card h2 {
margin: 0;
font-size: 1.8rem;
color: #111f37;
}
.hero-card p {
margin: 0.4rem 0 0;
color: #4b5a78;
}
.meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.35rem;
}
.badge {
display: inline-flex;
background: rgba(76, 110, 245, 0.15);
color: #2f45a1;
font-weight: 600;
border-radius: 999px;
padding: 0.35rem 1rem;
font-size: 0.9rem;
}
.info {
color: #4b5a78;
font-size: 0.9rem;
}
.content-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: minmax(0, 440px) minmax(0, 1fr);
align-items: flex-start;
}
.panel {
border: 1px solid #dde3f1;
border-radius: 20px;
background: #ffffff;
padding: 1.9rem;
box-shadow: 0 24px 48px rgba(88, 115, 173, 0.14);
display: grid;
gap: 1.2rem;
}
.slip-panel {
align-self: flex-start;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.panel-header h3 {
margin: 0;
font-size: 1.25rem;
color: #1d2943;
}
fieldset {
border: none;
padding: 0;
margin: 0;
display: grid;
gap: 1.1rem;
}
label {
display: grid;
gap: 0.35rem;
color: #2a3856;
font-weight: 600;
}
input,
select,
textarea {
border: 1px solid rgba(121, 139, 189, 0.45);
border-radius: 12px;
padding: 0.85rem 1rem;
font-size: 1rem;
background: #fdfdff;
color: #1f2d44;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
max-width: 100%;
}
select {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
option {
white-space: nowrap;
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #4c6ef5;
box-shadow: 0 0 0 3px rgba(76, 110, 245, 0.18);
background: #ffffff;
}
textarea {
resize: vertical;
min-height: 70px;
}
.field-grid {
display: grid;
gap: 1rem;
}
.field-grid.two {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.primary {
background: linear-gradient(135deg, #4c6ef5, #6f8bff);
color: #ffffff;
border: none;
border-radius: 12px;
padding: 0.9rem 1.6rem;
font-weight: 600;
cursor: pointer;
justify-self: flex-start;
box-shadow: 0 18px 32px rgba(76, 110, 245, 0.28);
}
.primary:disabled {
background: #b5c0e3;
cursor: wait;
box-shadow: none;
}
.ghost {
border: 1px solid #d3dcf0;
background: #f8faff;
color: #30426a;
border-radius: 10px;
padding: 0.35rem 0.9rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
.ghost:hover {
background: #e9f0ff;
}
.status {
font-size: 0.9rem;
color: #4c6ef5;
}
.feedback {
border-radius: 12px;
padding: 0.85rem 1rem;
font-weight: 500;
}
.feedback.success {
background: rgba(76, 175, 80, 0.12);
color: #2e7d32;
border: 1px solid rgba(76, 175, 80, 0.3);
}
.feedback.error {
background: rgba(229, 57, 53, 0.12);
color: #c62828;
border: 1px solid rgba(229, 57, 53, 0.28);
}
.warning {
border: 1px solid rgba(255, 193, 7, 0.45);
background: rgba(255, 214, 102, 0.18);
color: #9a6a00;
border-radius: 12px;
padding: 0.85rem 1rem;
font-weight: 600;
}
.simple-table {
width: 100%;
border-collapse: collapse;
border-radius: 16px;
overflow: hidden;
font-size: 0.95rem;
}
.simple-table th,
.simple-table td {
padding: 0.85rem 1rem;
border-bottom: 1px solid #e7ecf7;
text-align: left;
}
.simple-table th {
background: #f0f4ff;
color: #2a3856;
}
.simple-table tr:last-child td {
border-bottom: none;
}
.status-tag {
display: inline-flex;
align-items: center;
gap: 0.3rem;
border-radius: 999px;
padding: 0.25rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
cursor: default;
}
.status-tag-button {
border: none;
background: none;
font: inherit;
}
.status-tag-button:focus-visible {
outline: 2px solid #4c6ef5;
outline-offset: 2px;
}
.status-tag.pending {
background: rgba(255, 193, 7, 0.18);
color: #c28a00;
}
.status-tag.approved {
background: rgba(76, 175, 80, 0.18);
color: #2e7d32;
}
.status-tag.rejected {
background: rgba(229, 57, 53, 0.18);
color: #c62828;
cursor: pointer;
}
.status-tag i {
font-size: 0.85rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: minmax(0, 1fr);
}
.hero-card {
flex-direction: column;
align-items: flex-start;
}
.meta {
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,632 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { io } from 'socket.io-client';
import { fetchAssignedFuelSlips, updateFuelSlipStatus } from '../api';
export let token;
export let user;
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL || undefined;
const statusLabels = {
pending: 'Beklemede',
approved: 'Kabul',
rejected: 'Red'
};
let slips = [];
let loading = true;
let feedback = null;
let processing = {};
let rejectingId = null;
let rejectionReason = {};
let socket;
onMount(async () => {
await loadSlips();
setupSocket();
});
onDestroy(() => {
socket?.disconnect();
});
async function loadSlips() {
loading = true;
feedback = null;
try {
const response = await fetchAssignedFuelSlips(token);
slips = response.slips || [];
} catch (err) {
feedback = { type: 'error', text: err.message };
} finally {
loading = false;
}
}
function setupSocket() {
if (typeof window === 'undefined') {
return;
}
socket?.disconnect();
socket = io(SOCKET_URL || window.location.origin, {
auth: { token },
transports: ['websocket', 'polling']
});
socket.on('fuelSlip:new', (slip) => {
if (slip.inventoryManagerId === user.id) {
slips = [slip, ...slips.filter((existing) => existing.id !== slip.id)];
}
});
socket.on('fuelSlip:status', (slip) => {
let updated = false;
slips = slips.map((existing) => {
if (existing.id === slip.id) {
updated = true;
return slip;
}
return existing;
});
if (!updated && slip.inventoryManagerId === user.id) {
slips = [slip, ...slips];
}
});
socket.on('connect_error', (err) => {
console.warn('Socket bağlantı hatası:', err.message);
});
}
function setProcessing(id, state) {
processing = { ...processing, [id]: state };
}
async function approveSlip(slip) {
setProcessing(slip.id, true);
feedback = null;
try {
const response = await updateFuelSlipStatus(token, slip.id, { status: 'approved' });
updateSlip(response.slip);
if (rejectingId === slip.id) {
rejectingId = null;
}
} catch (err) {
feedback = { type: 'error', text: err.message };
} finally {
setProcessing(slip.id, false);
}
}
function startReject(slip) {
rejectingId = slip.id;
rejectionReason = { ...rejectionReason, [slip.id]: rejectionReason[slip.id] || '' };
}
function cancelReject() {
rejectingId = null;
}
async function submitReject(slip) {
const reason = (rejectionReason[slip.id] || '').trim();
if (!reason) {
feedback = { type: 'error', text: 'Red gerekçesi girilmeli.' };
return;
}
setProcessing(slip.id, true);
feedback = null;
try {
const response = await updateFuelSlipStatus(token, slip.id, {
status: 'rejected',
reason
});
updateSlip(response.slip);
rejectingId = null;
} catch (err) {
feedback = { type: 'error', text: err.message };
} finally {
setProcessing(slip.id, false);
}
}
function updateSlip(updated) {
slips = slips.map((existing) => (existing.id === updated.id ? updated : existing));
if (rejectingId === updated.id && updated.status !== 'pending') {
rejectingId = null;
}
}
function formatDate(value) {
if (!value) return '';
return new Date(value).toLocaleDateString('tr-TR');
}
function statusClass(status) {
return `status-tag ${status}`;
}
</script>
<div class="inventory-manager">
{#if feedback}
<div class={`feedback ${feedback.type}`}>{feedback.text}</div>
{/if}
<section class="panel">
<div class="panel-header">
<h3>Bekleyen fişler</h3>
<button type="button" class="ghost" on:click={loadSlips} disabled={loading}>
{loading ? 'Yükleniyor...' : 'Yenile'}
</button>
</div>
{#if loading}
<p class="info">Fişler yükleniyor...</p>
{:else if slips.length === 0}
<p class="info">Henüz size atanmış fiş bulunmuyor.</p>
{:else}
{#if slips.filter(slip => slip.status === 'pending').length === 0}
<p class="no-pending">Bekleyen fiş yok</p>
{:else}
<div class="slip-list">
{#each slips.filter(slip => slip.status === 'pending') as slip (slip.id)}
<article class={`slip-card ${slip.status}`}>
<header>
<div>
<h4>Seri No: {String(slip.slipNumber).padStart(4, '0')}</h4>
<span class="sub">Tarih: {formatDate(slip.slipDate)}</span>
</div>
{#if slip.status === 'pending'}
<span class="badge pending">
<i class="fa-solid fa-hourglass-half"></i> Beklemede
</span>
{:else if slip.status === 'approved'}
<span class="badge approved">
<i class="fa-solid fa-circle-check"></i> Kabul
</span>
{:else if slip.status === 'rejected'}
<span class="badge rejected">
<i class="fa-solid fa-circle-xmark"></i> Red
</span>
{/if}
</header>
<dl>
<div>
<dt>Birlik</dt>
<dd>{slip.unitName}</dd>
</div>
<div>
<dt>Araç</dt>
<dd>{slip.vehicleDescription} ({slip.plate})</dd>
</div>
<div>
<dt>Yakıt</dt>
<dd>{slip.fuelAmountNumber} lt • {slip.fuelType}</dd>
</div>
<div>
<dt>Teslim alan</dt>
<dd>{slip.receiverName} ({slip.receiverRank}) • {slip.receiverPhone}</dd>
</div>
<div>
<dt>Teslim eden</dt>
<dd>{slip.giverName} ({slip.giverRank}) • {slip.giverPhone}</dd>
</div>
{#if slip.notes}
<div>
<dt>Not</dt>
<dd>{slip.notes}</dd>
</div>
{/if}
{#if slip.status === 'rejected' && slip.rejectionReason}
<div>
<dt>Red gerekçesi</dt>
<dd>{slip.rejectionReason}</dd>
</div>
{/if}
</dl>
{#if slip.status === 'pending'}
{#if rejectingId === slip.id}
<div class="reject-box">
<input
placeholder="İptal gerekçesi"
bind:value={rejectionReason[slip.id]}
/>
<div class="actions">
<button
type="button"
class="primary"
on:click={() => submitReject(slip)}
disabled={processing[slip.id]}
>
{processing[slip.id] ? 'Gönderiliyor...' : 'Gönder'}
</button>
<button type="button" class="ghost" on:click={cancelReject}>Vazgeç</button>
</div>
</div>
{:else}
<div class="actions">
<button
type="button"
class="primary"
on:click={() => approveSlip(slip)}
disabled={processing[slip.id]}
>
{processing[slip.id] ? 'Onaylanıyor...' : 'Kabul'}
</button>
<button
type="button"
class="ghost danger"
on:click={() => startReject(slip)}
disabled={processing[slip.id]}
>
İptal
</button>
</div>
{/if}
{/if}
</article>
{/each}
</div>
{/if}
{/if}
</section>
<section class="approved-panel">
<h3>İşlem Yapılan Fişler</h3>
<div class="approved-header">
<span>Seri No</span>
<span>Tarih</span>
<span>Birlik</span>
<span>Araç</span>
<span>Yakıt</span>
<span>Teslim Alan</span>
<span>Teslim Eden</span>
<span>Durum</span>
</div>
{#each slips.filter(s => s.status === 'approved' || s.status === 'rejected') as slip}
<div class="slip-card approved">
<span>{String(slip.slipNumber).padStart(4, '0')}</span>
<span>{formatDate(slip.slipDate)}</span>
<span>{slip.unitName}</span>
<span>{slip.vehicleDescription} ({slip.plate})</span>
<span>{slip.fuelAmountNumber} lt • {slip.fuelType}</span>
<span>{slip.receiverName}</span>
<span>{slip.giverName}</span>
{#if slip.status === 'approved'}
<span class="badge approved">
<i class="fa-solid fa-circle-check"></i> {statusLabels[slip.status]}
</span>
{:else if slip.status === 'rejected'}
<span
class="badge rejected"
title={slip.rejectionReason || 'Red gerekçesi belirtilmemiş'}
>
<i class="fa-solid fa-circle-xmark"></i>&nbsp;{statusLabels[slip.status]}
</span>
{/if}
</div>
{/each}
</section>
</div>
<style>
.inventory-manager {
display: grid;
gap: 1.5rem;
color: #1f2d44;
}
.hero-card {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1.5rem;
background: linear-gradient(135deg, #f8faff, #eef2ff);
border: 1px solid #dbe3f6;
border-radius: 18px;
padding: 1.6rem 2rem;
box-shadow: 0 18px 32px rgba(80, 110, 185, 0.12);
}
.hero-card h2 {
margin: 0;
font-size: 1.8rem;
color: #111f37;
}
.hero-card p {
margin: 0.4rem 0 0;
color: #4b5a78;
}
.meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.35rem;
}
.badge {
display: inline-flex;
background: rgba(76, 110, 245, 0.15);
color: #2f45a1;
font-weight: 600;
border-radius: 999px;
padding: 0.35rem 1rem;
font-size: 0.9rem;
}
.badge.pending {
background: rgba(255, 193, 7, 0.15);
color: #c28a00;
}
.badge.approved {
background: rgba(76, 175, 80, 0.15);
color: #2e7d32;
}
.badge.rejected {
background: rgba(206, 34, 31, 0.15);
color: #e53935; /* brighter red for better contrast */
}
.badge.rejected i {
margin-right: 0.25rem; /* one-character space between icon and text */
}
.info {
color: #4b5a78;
font-size: 0.9rem;
}
.panel {
border: 1px solid #dde3f1;
border-radius: 20px;
background: #ffffff;
padding: 1.9rem;
box-shadow: 0 24px 48px rgba(88, 115, 173, 0.14);
display: grid;
gap: 1.2rem;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.panel-header h3 {
margin: 0;
font-size: 1.25rem;
color: #1d2943;
}
.ghost {
border: 1px solid #d3dcf0;
background: #f8faff;
color: #30426a;
border-radius: 10px;
padding: 0.35rem 0.9rem;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
.ghost:hover {
background: #e9f0ff;
}
.ghost.danger {
border-color: #f2bbb6;
background: #ffe9e7;
color: #c62828;
}
.feedback {
border-radius: 12px;
padding: 0.85rem 1rem;
font-weight: 500;
}
.feedback.error {
background: rgba(229, 57, 53, 0.12);
color: #c62828;
border: 1px solid rgba(229, 57, 53, 0.28);
}
.slip-list {
display: grid;
gap: 1rem;
}
.slip-card {
border: 1px solid #dde3f1;
border-radius: 16px;
padding: 1.4rem;
background: #f9fbff;
box-shadow: 0 12px 24px rgba(88, 115, 173, 0.12);
display: grid;
gap: 0.9rem;
}
.slip-card header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 1rem;
}
.slip-card header h4 {
margin: 0;
font-size: 1.1rem;
color: #1d2943;
}
.slip-card header .sub {
display: block;
font-size: 0.85rem;
color: #5c6a87;
}
dl {
display: grid;
gap: 0.5rem;
margin: 0;
}
dl div {
display: grid;
gap: 0.2rem;
}
dt {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #72809f;
}
dd {
margin: 0;
font-size: 0.95rem;
color: #2a3856;
}
.status-tag {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.25rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
}
.status-tag.pending {
background: rgba(255, 193, 7, 0.18);
color: #c28a00;
}
.status-tag.approved {
background: rgba(76, 175, 80, 0.18);
color: #2e7d32;
}
.status-tag.rejected {
background: rgba(229, 57, 53, 0.18);
color: #c62828;
}
.actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.primary {
background: linear-gradient(135deg, #4c6ef5, #6f8bff);
color: #ffffff;
border: none;
border-radius: 12px;
padding: 0.65rem 1.4rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 12px 22px rgba(76, 110, 245, 0.24);
}
.primary:disabled {
background: #b5c0e3;
cursor: wait;
box-shadow: none;
}
.reject-box {
display: grid;
gap: 0.6rem;
}
.reject-box input {
border: 1px solid rgba(229, 57, 53, 0.4);
border-radius: 10px;
padding: 0.6rem 0.8rem;
}
.slip-card.approved {
display: grid;
grid-template-columns: 0.8fr 1fr 1.4fr 1.6fr 1fr 1.4fr 1.4fr 0.8fr;
align-items: center;
gap: 0.5rem;
background: #f9fbff;
border: 1px solid rgba(76, 175, 80, 0.35);
border-radius: 8px;
margin-top: 0.4rem;
}
.slip-card.approved span {
font-size: 0.9rem;
color: #2a3856;
}
/* Ensure approved badge in approved-panel is styled like FuelManagerPanel */
.approved-panel .badge.approved {
background: rgba(76, 175, 80, 0.15);
color: #2e7d32;
display: inline-flex;
align-items: center;
gap: 0.35rem;
border-radius: 999px;
padding: 0.25rem 0.7rem;
font-weight: 600;
font-size: 0.85rem;
}
.approved-panel {
border: 1px solid #dde3f1;
border-radius: 12px;
background: #fff;
padding: 1rem;
box-shadow: 0 8px 20px rgba(0,0,0,0.05);
}
.approved-header {
display: grid;
grid-template-columns: 0.8fr 1fr 1.4fr 1.6fr 1fr 1.4fr 1.4fr 0.8fr;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.8rem;
font-weight: 700;
color: #1d2943;
border-bottom: 2px solid #dce3f5;
background: #f3f6fd;
}
@media (max-width: 768px) {
.hero-card {
flex-direction: column;
align-items: flex-start;
}
.meta {
align-items: flex-start;
}
}
.no-pending {
text-align: center;
color: #9aa3b7;
font-style: italic;
font-size: 0.95rem;
margin-top: 0.5rem;
}
</style>

View File

@@ -0,0 +1,303 @@
<script>
import { createEventDispatcher } from 'svelte';
import { login } from '../api';
const dispatch = createEventDispatcher();
let username = '';
let password = '';
let loading = false;
let error = '';
const sampleUsers = [
{
role: 'Admin',
username: 'admin',
password: 'Admin!123',
description: 'Mal sorumlularini olusturur ve butun kayitlara erisir.'
},
{
role: 'Yakit sorumlusu',
username: 'yakitsorum',
password: 'Yakit@123',
description: 'Yakit teslimlerini ve sarfiyatini takip eder.'
},
{
role: 'Mal sorumlusu',
username: 'malsorum1',
password: 'Mal@123',
description: 'Depodaki ekipman ve sarf malzemelerini raporlar.'
}
];
async function handleSubmit(event) {
event.preventDefault();
error = '';
if (!username || !password) {
error = 'Kullanici adi ve sifre girilmeli.';
return;
}
loading = true;
try {
const response = await login({ username, password });
dispatch('success', response);
} catch (err) {
error = err.message;
} finally {
loading = false;
}
}
function fillSample(sample) {
username = sample.username;
password = sample.password;
error = '';
}
</script>
<div class="login">
<div class="intro">
<h2>Hos geldiniz</h2>
<p>
Akaryakit istasyonunda rolunuze uygun kontrol paneline erismek icin lutfen bilgilerinizi girin.
</p>
</div>
<form class="form" on:submit|preventDefault={handleSubmit}>
<label>
<span>Kullanici adi</span>
<input
autocomplete="username"
name="username"
placeholder="ornek: admin"
bind:value={username}
required
/>
</label>
<label>
<span>Sifre</span>
<input
type="password"
name="password"
autocomplete="current-password"
placeholder="ornek: Admin!123"
bind:value={password}
required
/>
</label>
{#if error}
<div class="error">{error}</div>
{/if}
<button type="submit" disabled={loading}>
{#if loading}
Giris yapiliyor...
{:else}
Giris yap
{/if}
</button>
</form>
<div class="samples">
<h3>Ornek kullanicilar</h3>
<div class="sample-grid">
{#each sampleUsers as sample}
<article class="sample-card">
<header>
<span class="tag">{sample.role}</span>
<button type="button" on:click={() => fillSample(sample)}>
<i class="fa-solid fa-right-to-bracket" aria-hidden="true"></i>
Doldur
</button>
</header>
<dl>
<div>
<dt>Kullanici adi</dt>
<dd>{sample.username}</dd>
</div>
<div>
<dt>Sifre</dt>
<dd>{sample.password}</dd>
</div>
</dl>
<p>{sample.description}</p>
</article>
{/each}
</div>
</div>
</div>
<style>
.login {
display: grid;
gap: 2rem;
}
.intro h2 {
margin: 0;
font-size: 2rem;
color: #1f2d44;
}
.intro p {
margin: 0.5rem 0 0;
color: #4b5a78;
}
.form {
display: grid;
gap: 1rem;
max-width: 420px;
}
label {
display: grid;
gap: 0.35rem;
font-weight: 600;
color: #27334f;
}
input {
border-radius: 12px;
border: 1px solid rgba(126, 143, 191, 0.4);
padding: 0.85rem 1rem;
font-size: 1rem;
background: #fdfdff;
color: #1f2d44;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
input::placeholder {
color: rgba(123, 139, 180, 0.7);
}
input:focus {
border-color: #4c6ef5;
box-shadow: 0 0 0 3px rgba(76, 110, 245, 0.2);
background: #ffffff;
outline: none;
}
button[type='submit'] {
background: linear-gradient(135deg, #ff7a45, #ff9a5d);
color: #ffffff;
padding: 0.9rem 1rem;
border-radius: 12px;
font-weight: 600;
border: none;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 18px 28px rgba(255, 111, 60, 0.35);
}
button[type='submit']:hover {
transform: translateY(-1px);
box-shadow: 0 22px 36px rgba(255, 111, 60, 0.45);
}
button[type='submit'][disabled] {
background: #bcc4d9;
cursor: wait;
box-shadow: none;
}
.error {
background: rgba(255, 107, 107, 0.12);
color: #ff9a9a;
border-radius: 12px;
border: 1px solid rgba(255, 138, 138, 0.35);
padding: 0.75rem 1rem;
font-weight: 500;
}
.samples h3 {
margin: 0 0 1rem;
font-size: 1.1rem;
color: #1f2d44;
}
.sample-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.sample-card {
border: 1px solid #dee5f5;
border-radius: 18px;
padding: 1.35rem;
background: linear-gradient(135deg, #ffffff, #f6f8ff);
box-shadow: 0 18px 32px rgba(88, 115, 173, 0.16);
display: flex;
flex-direction: column;
gap: 0.75rem;
color: #2a3856;
}
.sample-card header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
.sample-card header button {
border: 1px solid #d2dcf3;
background: #edf2ff;
color: #4357b2;
border-radius: 999px;
padding: 0.35rem 1rem;
font-size: 0.85rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.sample-card header button:hover {
border-color: #4357b2;
}
.tag {
background: rgba(76, 110, 245, 0.12);
color: #354fa8;
text-transform: uppercase;
letter-spacing: 0.03em;
font-size: 0.75rem;
font-weight: 600;
padding: 0.2rem 0.75rem;
border-radius: 999px;
}
dl {
margin: 0;
display: grid;
gap: 0.25rem;
font-family: 'Roboto Mono', monospace;
font-size: 0.85rem;
color: #3c4c6e;
}
dt {
font-weight: 600;
color: #6a7796;
}
dd {
margin: 0;
color: #1f2d44;
font-size: 0.95rem;
}
.sample-card p {
margin: 0;
color: #556582;
font-size: 0.9rem;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,65 @@
<script>
export let user;
const roleCopy = {
fuel_manager: {
title: 'Yakit sorumlusu paneli',
message:
'Teslimatlar, pompa kontrolleri ve sarf kayitlari icin gerekli raporlara buradan ulasabilirsiniz.'
},
inventory_manager: {
title: 'Mal sorumlusu paneli',
message:
'Depo cikislari, stok sayimlari ve sarf raporlarini bu ekranda olusturup takip edebilirsiniz.'
}
};
const role = roleCopy[user.role] || {
title: 'Karsilama',
message: 'Yetkili oldugunuz modullere buradan ulasabilirsiniz.'
};
</script>
<div class="welcome">
<h2>Merhaba {user.displayName}</h2>
<p class="subtitle">{role.title}</p>
<p>{role.message}</p>
<ul>
<li>Rolunuze tanimlanan operasyon kartlari bu bolumde toplanir.</li>
<li>Guncel rapor ve bildirimler buradan duyurulur.</li>
<li>Yeni moduller eklendikce erisim haklariniz otomatik olarak guncellenir.</li>
</ul>
</div>
<style>
.welcome {
display: grid;
gap: 0.75rem;
color: #32415e;
}
h2 {
margin: 0;
font-size: 1.75rem;
color: #1f2d44;
}
.subtitle {
margin: 0;
color: #4c6ef5;
font-weight: 600;
}
p {
margin: 0;
color: #4b5a78;
line-height: 1.5;
}
ul {
margin: 0;
padding-left: 1.2rem;
color: #4b5a78;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,91 @@
const ONES = ['', 'bir', 'iki', 'üç', 'dört', 'beş', 'altı', 'yedi', 'sekiz', 'dokuz'];
const TENS = ['', 'on', 'yirmi', 'otuz', 'kırk', 'elli', 'altmış', 'yetmiş', 'seksen', 'doksan'];
const THOUSANDS = ['', 'bin', 'milyon', 'milyar'];
function threeDigitsToWords(num) {
let result = '';
const hundred = Math.floor(num / 100);
const ten = Math.floor((num % 100) / 10);
const one = num % 10;
if (hundred > 0) {
if (hundred === 1) {
result += 'yüz';
} else {
result += `${ONES[hundred]} yüz`;
}
}
if (ten > 0) {
result += (result ? ' ' : '') + TENS[ten];
}
if (one > 0) {
result += (result ? ' ' : '') + ONES[one];
}
return result.trim();
}
export function numberToWordsTr(value) {
if (value === null || value === undefined || value === '') {
return '';
}
const number = Number(value);
if (Number.isNaN(number)) {
return '';
}
const isNegative = number < 0;
const absolute = Math.abs(number);
const integerPart = Math.floor(absolute);
const fractionPart = Math.round((absolute - integerPart) * 100);
if (integerPart === 0 && fractionPart === 0) {
return 'sıfır';
}
let words = '';
if (integerPart === 0) {
words = 'sıfır';
} else {
let temp = integerPart;
let thousandIndex = 0;
while (temp > 0) {
const chunk = temp % 1000;
if (chunk !== 0) {
let chunkWords = threeDigitsToWords(chunk);
if (thousandIndex === 1 && chunk === 1) {
chunkWords = 'bin';
} else if (THOUSANDS[thousandIndex]) {
chunkWords = `${chunkWords} ${THOUSANDS[thousandIndex]}`.trim();
}
words = words ? `${chunkWords} ${words}` : chunkWords;
}
temp = Math.floor(temp / 1000);
thousandIndex += 1;
}
}
if (fractionPart > 0) {
const centsWords = fractionPart
.toString()
.split('')
.map((digit) => ONES[Number(digit)])
.join(' ');
words = `${words} virgül ${centsWords}`.trim();
}
if (isNegative) {
words = `eksi ${words}`;
}
return words.trim();
}
export default numberToWordsTr;

9
client/src/main.js Normal file
View File

@@ -0,0 +1,9 @@
import './styles.css';
import './app.css';
import App from './App.svelte';
const app = new App({
target: document.getElementById('app')
});
export default app;

22
client/src/styles.css Normal file
View File

@@ -0,0 +1,22 @@
:root {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
color: #23304a;
background-color: #f5f7fb;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at 10% 10%, #ffffff, #eef2fb 55%, #e3e8f7 100%);
color: inherit;
}
#app {
min-height: 100vh;
}
button,
input,
textarea {
font-family: inherit;
}

5
client/svelte.config.js Normal file
View File

@@ -0,0 +1,5 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
};

19
client/vite.config.js Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5005',
changeOrigin: true
},
'/socket.io': {
target: 'http://localhost:5005',
ws: true
}
}
}
});

BIN
data/app.db Normal file

Binary file not shown.

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "ytp",
"version": "1.0.0",
"description": "Akaryakıt istasyonu için rol bazlı giriş ekranı örneği",
"main": "server/index.js",
"scripts": {
"dev": "npm run dev:server",
"dev:server": "nodemon server/index.js",
"start:server": "node server/index.js",
"prepare:db": "node server/db-init.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"pdfkit": "^0.13.0",
"socket.io": "^4.7.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"cors": "^2.8.5",
"express": "^4.19.2",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"nodemon": "^3.0.3"
}
}

13
server/db-init.js Normal file
View File

@@ -0,0 +1,13 @@
const { db, initialize } = require('./db');
initialize()
.then(() => {
console.log('Veritabani hazirlandi.');
})
.catch((err) => {
console.error('Veritabani hazirlama hatasi:', err);
process.exitCode = 1;
})
.finally(() => {
db.close();
});

916
server/db.js Normal file
View File

@@ -0,0 +1,916 @@
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const dbPath = path.join(__dirname, '..', 'data', 'app.db');
const db = new sqlite3.Database(dbPath);
const SAMPLE_USERS = [
{
username: 'admin',
password: 'Admin!123',
role: 'admin',
displayName: 'Istasyon Admini'
},
{
username: 'yakitsorum',
password: 'Yakit@123',
role: 'fuel_manager',
displayName: 'Yakit Sorumlusu'
},
{
username: 'malsorum1',
password: 'Mal@123',
role: 'inventory_manager',
displayName: 'Mal Sorumlusu 1'
}
];
const SAMPLE_VEHICLES = [
{ brand: 'Ford', model: 'Transit', year: 2021, plate: '34 AYT 312' },
{ brand: 'Isuzu', model: 'NPR', year: 2019, plate: '34 FZT 908' }
];
const SAMPLE_UNITS = [
{
name: 'Merkez Birlik',
address: 'Cumhuriyet Mah. İstasyon Cad. No:12/1 İstanbul',
stk: 'STK-4589',
btk: 'BTK-9021',
contactName: 'Yzb. Murat Kaya',
contactRank: 'Yuzbasi',
contactRegistry: 'MK4587',
contactIdentity: '25478963210',
contactPhone: '+90 532 456 78 12'
},
{
name: 'Doğu Lojistik Birimi',
address: 'Sanayi Mah. Depo Sok. No:8 Erzurum',
stk: 'STK-7865',
btk: 'BTK-6674',
contactName: 'Uzm. Cav. Esra Yilmaz',
contactRank: 'Uzman Cavus',
contactRegistry: 'EY3345',
contactIdentity: '19876543219',
contactPhone: '+90 532 998 11 44'
}
];
const SAMPLE_FUEL_PERSONNEL = [
{
fullName: 'Astsb. Cahit Demir',
rank: 'Astsubay',
registryNumber: 'CD5561',
identityNumber: '14523698741',
phone: '+90 532 223 45 67'
},
{
fullName: 'Sv. Uzm. Er Ali Korkmaz',
rank: 'Sozlesmeli Er',
registryNumber: 'AK7812',
identityNumber: '32987456100',
phone: '+90 555 893 22 10'
}
];
function run(query, params = []) {
return new Promise((resolve, reject) => {
db.run(query, params, function callback(err) {
if (err) {
reject(err);
} else {
resolve(this);
}
});
});
}
function ensureColumn(table, column, definition) {
return new Promise((resolve, reject) => {
db.all(`PRAGMA table_info(${table})`, [], (err, rows) => {
if (err) {
reject(err);
return;
}
const exists = rows.some((row) => row.name === column);
if (exists) {
resolve();
return;
}
db.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`, (alterErr) => {
if (alterErr) {
reject(alterErr);
} else {
resolve();
}
});
});
});
}
function initialize() {
return new Promise((resolve, reject) => {
db.serialize(async () => {
try {
await run(
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin','fuel_manager','inventory_manager')),
display_name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`
);
await run(
`CREATE TABLE IF NOT EXISTS vehicles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand TEXT NOT NULL,
model TEXT NOT NULL,
year INTEGER NOT NULL,
plate TEXT UNIQUE NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`
);
await run(
`CREATE TABLE IF NOT EXISTS units (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
address TEXT NOT NULL,
stk TEXT NOT NULL,
btk TEXT NOT NULL,
contact_name TEXT NOT NULL,
contact_rank TEXT NOT NULL,
contact_registry TEXT NOT NULL,
contact_identity TEXT NOT NULL,
contact_phone TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`
);
await run(
`CREATE TABLE IF NOT EXISTS fuel_personnel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
full_name TEXT NOT NULL,
rank TEXT NOT NULL,
registry_number TEXT UNIQUE NOT NULL,
identity_number TEXT NOT NULL,
phone TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)`
);
await run(
`CREATE TABLE IF NOT EXISTS fuel_slips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slip_number INTEGER UNIQUE,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
slip_date TEXT NOT NULL,
force TEXT NOT NULL,
unit_id INTEGER NOT NULL,
unit_name TEXT NOT NULL,
vehicle_id INTEGER NOT NULL,
vehicle_description TEXT NOT NULL,
plate TEXT NOT NULL,
fuel_amount_number REAL NOT NULL,
fuel_amount_text TEXT NOT NULL,
fuel_type TEXT NOT NULL,
receiver_id INTEGER NOT NULL,
receiver_name TEXT NOT NULL,
receiver_rank TEXT NOT NULL,
receiver_registry TEXT NOT NULL,
receiver_phone TEXT NOT NULL,
giver_id INTEGER NOT NULL,
giver_name TEXT NOT NULL,
giver_rank TEXT NOT NULL,
giver_registry TEXT NOT NULL,
giver_phone TEXT NOT NULL,
notes TEXT,
inventory_manager_id INTEGER NOT NULL,
fuel_manager_id INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
rejection_reason TEXT,
FOREIGN KEY (unit_id) REFERENCES units(id),
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id),
FOREIGN KEY (receiver_id) REFERENCES fuel_personnel(id),
FOREIGN KEY (giver_id) REFERENCES fuel_personnel(id),
FOREIGN KEY (inventory_manager_id) REFERENCES users(id),
FOREIGN KEY (fuel_manager_id) REFERENCES users(id)
)`
);
await Promise.all([
ensureColumn('fuel_slips', 'inventory_manager_id', 'INTEGER'),
ensureColumn('fuel_slips', 'fuel_manager_id', 'INTEGER'),
ensureColumn('fuel_slips', 'status', "TEXT DEFAULT 'pending'"),
ensureColumn('fuel_slips', 'rejection_reason', 'TEXT')
]).catch((err) => {
// Ignored if columns already exist
if (err && !/duplicate column/i.test(err.message)) {
throw err;
}
});
for (const user of SAMPLE_USERS) {
await run(
`INSERT OR IGNORE INTO users (username, password, role, display_name)
VALUES (?, ?, ?, ?)`,
[user.username, user.password, user.role, user.displayName]
);
}
for (const vehicle of SAMPLE_VEHICLES) {
await run(
`INSERT OR IGNORE INTO vehicles (brand, model, year, plate)
VALUES (?, ?, ?, ?)`,
[vehicle.brand, vehicle.model, vehicle.year, vehicle.plate]
);
}
for (const unit of SAMPLE_UNITS) {
await run(
`INSERT OR IGNORE INTO units (
name, address, stk, btk, contact_name, contact_rank,
contact_registry, contact_identity, contact_phone
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
unit.name,
unit.address,
unit.stk,
unit.btk,
unit.contactName,
unit.contactRank,
unit.contactRegistry,
unit.contactIdentity,
unit.contactPhone
]
);
}
for (const personnel of SAMPLE_FUEL_PERSONNEL) {
await run(
`INSERT OR IGNORE INTO fuel_personnel (
full_name, rank, registry_number, identity_number, phone
) VALUES (?, ?, ?, ?, ?)`,
[
personnel.fullName,
personnel.rank,
personnel.registryNumber,
personnel.identityNumber,
personnel.phone
]
);
}
resolve();
} catch (err) {
reject(err);
}
});
});
}
function getUserByUsername(username) {
return new Promise((resolve, reject) => {
db.get(
`SELECT id, username, password, role, display_name AS displayName
FROM users
WHERE username = ?`,
[username],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
function createInventoryManager({ username, password, displayName }) {
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO users (username, password, role, display_name)
VALUES (?, ?, 'inventory_manager', ?)`,
[username, password, displayName],
function insertCallback(err) {
if (err) {
reject(err);
return;
}
resolve({
id: this.lastID,
username,
displayName,
role: 'inventory_manager'
});
}
);
});
}
function listInventoryManagers() {
return new Promise((resolve, reject) => {
db.all(
`SELECT id, username, display_name AS displayName, created_at AS createdAt
FROM users
WHERE role = 'inventory_manager'
ORDER BY created_at DESC`,
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function updateInventoryManager({ id, displayName, password }) {
const updates = [];
const params = [];
if (displayName) {
updates.push('display_name = ?');
params.push(displayName);
}
if (password) {
updates.push('password = ?');
params.push(password);
}
if (updates.length === 0) {
return Promise.resolve();
}
params.push(id);
return run(
`UPDATE users SET ${updates.join(', ')} WHERE id = ? AND role = 'inventory_manager'`,
params
);
}
function deleteInventoryManager(id) {
return run(`DELETE FROM users WHERE id = ? AND role = 'inventory_manager'`, [id]);
}
function createVehicle({ brand, model, year, plate }) {
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO vehicles (brand, model, year, plate)
VALUES (?, ?, ?, ?)`,
[brand, model, year, plate],
function insertCallback(err) {
if (err) {
reject(err);
return;
}
resolve({
id: this.lastID,
brand,
model,
year,
plate
});
}
);
});
}
function listVehicles() {
return new Promise((resolve, reject) => {
db.all(
`SELECT id, brand, model, year, plate, created_at AS createdAt
FROM vehicles
ORDER BY created_at DESC`,
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function updateVehicle({ id, brand, model, year, plate }) {
return run(
`UPDATE vehicles
SET brand = ?, model = ?, year = ?, plate = ?
WHERE id = ?`,
[brand, model, year, plate, id]
);
}
function deleteVehicle(id) {
return run(`DELETE FROM vehicles WHERE id = ?`, [id]);
}
function createUnit(payload) {
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO units (
name, address, stk, btk, contact_name, contact_rank,
contact_registry, contact_identity, contact_phone
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
payload.name,
payload.address,
payload.stk,
payload.btk,
payload.contactName,
payload.contactRank,
payload.contactRegistry,
payload.contactIdentity,
payload.contactPhone
],
function insertCallback(err) {
if (err) {
reject(err);
return;
}
resolve({
id: this.lastID,
...payload
});
}
);
});
}
function listUnits() {
return new Promise((resolve, reject) => {
db.all(
`SELECT
id,
name,
address,
stk,
btk,
contact_name AS contactName,
contact_rank AS contactRank,
contact_registry AS contactRegistry,
contact_identity AS contactIdentity,
contact_phone AS contactPhone,
created_at AS createdAt
FROM units
ORDER BY created_at DESC`,
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function updateUnit(payload) {
return run(
`UPDATE units
SET name = ?, address = ?, stk = ?, btk = ?, contact_name = ?, contact_rank = ?,
contact_registry = ?, contact_identity = ?, contact_phone = ?
WHERE id = ?`,
[
payload.name,
payload.address,
payload.stk,
payload.btk,
payload.contactName,
payload.contactRank,
payload.contactRegistry,
payload.contactIdentity,
payload.contactPhone,
payload.id
]
);
}
function deleteUnit(id) {
return run(`DELETE FROM units WHERE id = ?`, [id]);
}
function createFuelPersonnel(payload) {
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO fuel_personnel (
full_name, rank, registry_number, identity_number, phone
)
VALUES (?, ?, ?, ?, ?)`,
[
payload.fullName,
payload.rank,
payload.registryNumber,
payload.identityNumber,
payload.phone
],
function insertCallback(err) {
if (err) {
reject(err);
return;
}
resolve({
id: this.lastID,
...payload
});
}
);
});
}
function listFuelPersonnel() {
return new Promise((resolve, reject) => {
db.all(
`SELECT
id,
full_name AS fullName,
rank,
registry_number AS registryNumber,
identity_number AS identityNumber,
phone,
created_at AS createdAt
FROM fuel_personnel
ORDER BY created_at DESC`,
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function updateFuelPersonnel(payload) {
return run(
`UPDATE fuel_personnel
SET full_name = ?, rank = ?, registry_number = ?, identity_number = ?, phone = ?
WHERE id = ?`,
[
payload.fullName,
payload.rank,
payload.registryNumber,
payload.identityNumber,
payload.phone,
payload.id
]
);
}
function deleteFuelPersonnel(id) {
return run(`DELETE FROM fuel_personnel WHERE id = ?`, [id]);
}
function getVehicleById(id) {
return new Promise((resolve, reject) => {
db.get(
`SELECT id, brand, model, year, plate, created_at AS createdAt
FROM vehicles
WHERE id = ?`,
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
function getUnitById(id) {
return new Promise((resolve, reject) => {
db.get(
`SELECT id, name, address, stk, btk, contact_name AS contactName, contact_rank AS contactRank,
contact_registry AS contactRegistry, contact_identity AS contactIdentity,
contact_phone AS contactPhone
FROM units
WHERE id = ?`,
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
function getFuelPersonnelById(id) {
return new Promise((resolve, reject) => {
db.get(
`SELECT id, full_name AS fullName, rank, registry_number AS registryNumber,
identity_number AS identityNumber, phone
FROM fuel_personnel
WHERE id = ?`,
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
function getInventoryManagerById(id) {
return new Promise((resolve, reject) => {
db.get(
`SELECT id, username, display_name AS displayName
FROM users
WHERE id = ? AND role = 'inventory_manager'`,
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
async function createFuelSlip(payload) {
const nextNumber = await new Promise((resolve, reject) => {
db.get(`SELECT IFNULL(MAX(slip_number), 0) + 1 AS nextNumber FROM fuel_slips`, [], (err, row) => {
if (err) {
reject(err);
} else {
resolve(row ? row.nextNumber : 1);
}
});
});
return new Promise((resolve, reject) => {
db.run(
`INSERT INTO fuel_slips (
slip_number,
slip_date,
force,
unit_id,
unit_name,
vehicle_id,
vehicle_description,
plate,
fuel_amount_number,
fuel_amount_text,
fuel_type,
receiver_id,
receiver_name,
receiver_rank,
receiver_registry,
receiver_phone,
giver_id,
giver_name,
giver_rank,
giver_registry,
giver_phone,
notes,
inventory_manager_id,
fuel_manager_id,
status,
rejection_reason
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ,
[
nextNumber,
payload.date,
payload.force,
payload.unit.id,
payload.unit.name,
payload.vehicle.id,
payload.vehicle.description,
payload.vehicle.plate,
payload.fuelAmountNumber,
payload.fuelAmountText,
payload.fuelType,
payload.receiver.id,
payload.receiver.fullName,
payload.receiver.rank,
payload.receiver.registryNumber,
payload.receiver.phone,
payload.giver.id,
payload.giver.fullName,
payload.giver.rank,
payload.giver.registryNumber,
payload.giver.phone,
payload.notes || null,
payload.inventoryManager.id,
payload.fuelManagerId,
'pending',
null
],
function insertCallback(err) {
if (err) {
reject(err);
} else {
resolve({ id: this.lastID, slipNumber: nextNumber });
}
}
);
});
}
function listFuelSlips() {
return new Promise((resolve, reject) => {
db.all(
`SELECT
fs.id,
fs.slip_number AS slipNumber,
fs.created_at AS createdAt,
fs.slip_date AS slipDate,
fs.force,
fs.unit_id AS unitId,
fs.unit_name AS unitName,
fs.vehicle_id AS vehicleId,
fs.vehicle_description AS vehicleDescription,
fs.plate,
fs.fuel_amount_number AS fuelAmountNumber,
fs.fuel_amount_text AS fuelAmountText,
fs.fuel_type AS fuelType,
fs.receiver_id AS receiverId,
fs.receiver_name AS receiverName,
fs.receiver_rank AS receiverRank,
fs.receiver_registry AS receiverRegistry,
fs.receiver_phone AS receiverPhone,
fs.giver_id AS giverId,
fs.giver_name AS giverName,
fs.giver_rank AS giverRank,
fs.giver_registry AS giverRegistry,
fs.giver_phone AS giverPhone,
fs.notes,
fs.inventory_manager_id AS inventoryManagerId,
inv.display_name AS inventoryManagerName,
fs.fuel_manager_id AS fuelManagerId,
fs.status,
fs.rejection_reason AS rejectionReason
FROM fuel_slips fs
LEFT JOIN users inv ON inv.id = fs.inventory_manager_id
ORDER BY fs.created_at DESC`,
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function listFuelSlipsByInventoryManager(inventoryManagerId) {
return new Promise((resolve, reject) => {
db.all(
`SELECT
fs.id,
fs.slip_number AS slipNumber,
fs.created_at AS createdAt,
fs.slip_date AS slipDate,
fs.force,
fs.unit_id AS unitId,
fs.unit_name AS unitName,
fs.vehicle_id AS vehicleId,
fs.vehicle_description AS vehicleDescription,
fs.plate,
fs.fuel_amount_number AS fuelAmountNumber,
fs.fuel_amount_text AS fuelAmountText,
fs.fuel_type AS fuelType,
fs.receiver_id AS receiverId,
fs.receiver_name AS receiverName,
fs.receiver_rank AS receiverRank,
fs.receiver_registry AS receiverRegistry,
fs.receiver_phone AS receiverPhone,
fs.giver_id AS giverId,
fs.giver_name AS giverName,
fs.giver_rank AS giverRank,
fs.giver_registry AS giverRegistry,
fs.giver_phone AS giverPhone,
fs.notes,
fs.inventory_manager_id AS inventoryManagerId,
inv.display_name AS inventoryManagerName,
fs.fuel_manager_id AS fuelManagerId,
fs.status,
fs.rejection_reason AS rejectionReason
FROM fuel_slips fs
LEFT JOIN users inv ON inv.id = fs.inventory_manager_id
WHERE fs.inventory_manager_id = ?
ORDER BY fs.created_at DESC`,
[inventoryManagerId],
(err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
);
});
}
function getFuelSlipById(id) {
return new Promise((resolve, reject) => {
db.get(
`SELECT
fs.id,
fs.slip_number AS slipNumber,
fs.created_at AS createdAt,
fs.slip_date AS slipDate,
fs.force,
fs.unit_id AS unitId,
fs.unit_name AS unitName,
fs.vehicle_id AS vehicleId,
fs.vehicle_description AS vehicleDescription,
fs.plate,
fs.fuel_amount_number AS fuelAmountNumber,
fs.fuel_amount_text AS fuelAmountText,
fs.fuel_type AS fuelType,
fs.receiver_id AS receiverId,
fs.receiver_name AS receiverName,
fs.receiver_rank AS receiverRank,
fs.receiver_registry AS receiverRegistry,
fs.receiver_phone AS receiverPhone,
fs.giver_id AS giverId,
fs.giver_name AS giverName,
fs.giver_rank AS giverRank,
fs.giver_registry AS giverRegistry,
fs.giver_phone AS giverPhone,
fs.notes,
fs.inventory_manager_id AS inventoryManagerId,
inv.display_name AS inventoryManagerName,
fs.fuel_manager_id AS fuelManagerId,
fs.status,
fs.rejection_reason AS rejectionReason
FROM fuel_slips fs
LEFT JOIN users inv ON inv.id = fs.inventory_manager_id
WHERE fs.id = ?`,
[id],
(err, row) => {
if (err) {
reject(err);
} else {
resolve(row);
}
}
);
});
}
function updateFuelSlipStatus({ id, status, rejectionReason }) {
return run(
`UPDATE fuel_slips
SET status = ?, rejection_reason = ?
WHERE id = ?`,
[status, rejectionReason || null, id]
);
}
module.exports = {
db,
initialize,
getUserByUsername,
createInventoryManager,
listInventoryManagers,
updateInventoryManager,
deleteInventoryManager,
createVehicle,
listVehicles,
updateVehicle,
deleteVehicle,
createUnit,
listUnits,
updateUnit,
deleteUnit,
createFuelPersonnel,
listFuelPersonnel,
updateFuelPersonnel,
deleteFuelPersonnel,
getVehicleById,
getUnitById,
getFuelPersonnelById,
getInventoryManagerById,
createFuelSlip,
listFuelSlips,
listFuelSlipsByInventoryManager,
getFuelSlipById,
updateFuelSlipStatus
};

835
server/index.js Normal file
View File

@@ -0,0 +1,835 @@
const http = require('http');
const crypto = require('crypto');
const express = require('express');
const cors = require('cors');
const { Server } = require('socket.io');
const {
db,
initialize,
getUserByUsername,
createInventoryManager,
listInventoryManagers,
updateInventoryManager,
deleteInventoryManager,
createVehicle,
listVehicles,
updateVehicle,
deleteVehicle,
createUnit,
listUnits,
updateUnit,
deleteUnit,
createFuelPersonnel,
listFuelPersonnel,
updateFuelPersonnel,
deleteFuelPersonnel,
getVehicleById,
getUnitById,
getFuelPersonnelById,
getInventoryManagerById,
createFuelSlip,
listFuelSlips,
listFuelSlipsByInventoryManager,
getFuelSlipById,
updateFuelSlipStatus
} = require('./db');
const PDFDocument = require('pdfkit');
const app = express();
const PORT = process.env.PORT || 5005;
const sessions = new Map();
const httpServer = http.createServer(app);
const io = new Server(httpServer, {
cors: {
origin: '*'
}
});
const FUEL_FORCES = ['MSB', 'K.K.K.', 'Dz.K.K.', 'Hv.K.K.', 'SGK', 'Gnkur. Bşk.', 'Hrt.Gn.K.'];
const fuelManagerSockets = new Map();
const inventoryManagerSockets = new Map();
function addSocket(map, key, socket) {
if (!map.has(key)) {
map.set(key, new Set());
}
map.get(key).add(socket);
}
function removeSocket(map, key, socket) {
const set = map.get(key);
if (!set) {
return;
}
set.delete(socket);
if (set.size === 0) {
map.delete(key);
}
}
function emitToInventoryManager(id, event, payload) {
const set = inventoryManagerSockets.get(id);
if (!set) {
return;
}
for (const socket of set) {
socket.emit(event, payload);
}
}
function emitToFuelManager(id, event, payload) {
const set = fuelManagerSockets.get(id);
if (!set) {
return;
}
for (const socket of set) {
socket.emit(event, payload);
}
}
io.use((socket, next) => {
const token = socket.handshake.auth?.token || socket.handshake.query?.token;
if (!token || !sessions.has(token)) {
return next(new Error('Oturum bulunamadı.'));
}
socket.sessionToken = token;
socket.user = sessions.get(token);
return next();
});
io.on('connection', (socket) => {
const { user } = socket;
if (user.role === 'fuel_manager') {
addSocket(fuelManagerSockets, user.id, socket);
} else if (user.role === 'inventory_manager') {
addSocket(inventoryManagerSockets, user.id, socket);
}
socket.on('disconnect', () => {
if (user.role === 'fuel_manager') {
removeSocket(fuelManagerSockets, user.id, socket);
} else if (user.role === 'inventory_manager') {
removeSocket(inventoryManagerSockets, user.id, socket);
}
});
});
app.use(cors());
app.use(express.json());
function createSession(user) {
const token = crypto.randomBytes(24).toString('hex');
const session = {
id: user.id,
username: user.username,
role: user.role,
displayName: user.displayName
};
sessions.set(token, session);
return { token, session };
}
function requireSession(req, res, next) {
const token = req.header('x-session-token');
if (!token || !sessions.has(token)) {
return res.status(401).json({ message: 'Oturum bulunamadi.' });
}
req.session = sessions.get(token);
req.sessionToken = token;
return next();
}
function requireAdmin(req, res, next) {
if (req.session.role !== 'admin') {
return res.status(403).json({ message: 'Bu islem icin yetki yok.' });
}
return next();
}
function requireFuelManager(req, res, next) {
if (req.session.role !== 'fuel_manager') {
return res.status(403).json({ message: 'Bu islem icin yetki yok.' });
}
return next();
}
function requireInventoryManager(req, res, next) {
if (req.session.role !== 'inventory_manager') {
return res.status(403).json({ message: 'Bu islem icin yetki yok.' });
}
return next();
}
app.post('/api/auth/login', async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ message: 'Kullanici adi ve sifre zorunlu.' });
}
try {
const user = await getUserByUsername(username);
if (!user || user.password !== password) {
return res.status(401).json({ message: 'Kullanici adi veya sifre hatali.' });
}
const { token, session } = createSession(user);
return res.json({
token,
user: session
});
} catch (err) {
console.error('Login hatasi:', err);
return res.status(500).json({ message: 'Beklenmeyen bir sorun olustu.' });
}
});
app.post('/api/auth/logout', requireSession, (req, res) => {
sessions.delete(req.sessionToken);
return res.json({ message: 'Oturum kapatildi.' });
});
app.get('/api/session', requireSession, (req, res) => {
return res.json({
user: req.session
});
});
app.get('/api/inventory-managers', requireSession, requireAdmin, async (req, res) => {
try {
const managers = await listInventoryManagers();
return res.json({ managers });
} catch (err) {
console.error('Listeleme hatasi:', err);
return res.status(500).json({ message: 'Mal sorumlulari okunamadi.' });
}
});
app.post('/api/inventory-managers', requireSession, requireAdmin, async (req, res) => {
const { username, password, displayName } = req.body || {};
if (!username || !password || !displayName) {
return res
.status(400)
.json({ message: 'Kullanici adi, sifre ve gorunen ad zorunlu alanlardir.' });
}
try {
const created = await createInventoryManager({ username, password, displayName });
return res.status(201).json({ manager: created });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu kullanici adi zaten mevcut.' });
}
console.error('Ekleme hatasi:', err);
return res.status(500).json({ message: 'Mal sorumlusu eklenemedi.' });
}
});
app.put('/api/inventory-managers/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
const { displayName, password } = req.body || {};
if (!displayName && !password) {
return res.status(400).json({ message: 'Guncelleme icin en az bir alan girilmeli.' });
}
try {
await updateInventoryManager({ id, displayName, password });
return res.json({ message: 'Kayit guncellendi.' });
} catch (err) {
console.error('Mal sorumlusu guncelleme hatasi:', err);
return res.status(500).json({ message: 'Guncelleme yapilamadi.' });
}
});
app.delete('/api/inventory-managers/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await deleteInventoryManager(id);
return res.status(204).end();
} catch (err) {
console.error('Mal sorumlusu silme hatasi:', err);
return res.status(500).json({ message: 'Silme islemi tamamlanamadi.' });
}
});
app.get('/api/vehicles', requireSession, requireAdmin, async (req, res) => {
try {
const vehicles = await listVehicles();
return res.json({ vehicles });
} catch (err) {
console.error('Arac listeleme hatasi:', err);
return res.status(500).json({ message: 'Araçlar okunamadı.' });
}
});
app.post('/api/vehicles', requireSession, requireAdmin, async (req, res) => {
const { brand, model, year, plate } = req.body || {};
if (!brand || !model || !year || !plate) {
return res.status(400).json({ message: 'Marka, model, yıl ve plaka zorunludur.' });
}
const parsedYear = Number(year);
if (!Number.isInteger(parsedYear) || parsedYear < 1990 || parsedYear > new Date().getFullYear() + 1) {
return res.status(400).json({ message: 'Lütfen geçerli bir model yılı girin.' });
}
try {
const vehicle = await createVehicle({
brand,
model,
year: parsedYear,
plate: plate.trim().toUpperCase()
});
return res.status(201).json({ vehicle });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu plaka ile kayıt zaten mevcut.' });
}
console.error('Arac ekleme hatasi:', err);
return res.status(500).json({ message: 'Araç kaydedilemedi.' });
}
});
app.put('/api/vehicles/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
const { brand, model, year, plate } = req.body || {};
if (!brand || !model || !year || !plate) {
return res.status(400).json({ message: 'Tum alanlar zorunlusudur.' });
}
const parsedYear = Number(year);
if (!Number.isInteger(parsedYear) || parsedYear < 1990 || parsedYear > new Date().getFullYear() + 1) {
return res.status(400).json({ message: 'Gecerli bir model yılı girin.' });
}
try {
await updateVehicle({ id, brand, model, year: parsedYear, plate: plate.trim().toUpperCase() });
return res.json({ message: 'Araç kaydı güncellendi.' });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu plaka ile kayıt mevcut.' });
}
console.error('Arac guncelleme hatasi:', err);
return res.status(500).json({ message: 'Guncelleme yapilamadi.' });
}
});
app.delete('/api/vehicles/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await deleteVehicle(id);
return res.status(204).end();
} catch (err) {
console.error('Arac silme hatasi:', err);
return res.status(500).json({ message: 'Silme islemi tamamlanamadi.' });
}
});
app.get('/api/units', requireSession, requireAdmin, async (req, res) => {
try {
const units = await listUnits();
return res.json({ units });
} catch (err) {
console.error('Birlik listeleme hatasi:', err);
return res.status(500).json({ message: 'Birlikler okunamadı.' });
}
});
app.post('/api/units', requireSession, requireAdmin, async (req, res) => {
const {
name,
address,
stk,
btk,
contactName,
contactRank,
contactRegistry,
contactIdentity,
contactPhone
} = req.body || {};
if (
!name ||
!address ||
!stk ||
!btk ||
!contactName ||
!contactRank ||
!contactRegistry ||
!contactIdentity ||
!contactPhone
) {
return res.status(400).json({ message: 'Birlik ve sorumlu bilgileri eksiksiz girilmeli.' });
}
try {
const unit = await createUnit({
name,
address,
stk,
btk,
contactName,
contactRank,
contactRegistry,
contactIdentity,
contactPhone
});
return res.status(201).json({ unit });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu birlik adı ile kayıt mevcut.' });
}
console.error('Birlik ekleme hatasi:', err);
return res.status(500).json({ message: 'Birlik kaydedilemedi.' });
}
});
app.put('/api/units/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
const {
name,
address,
stk,
btk,
contactName,
contactRank,
contactRegistry,
contactIdentity,
contactPhone
} = req.body || {};
if (
!name ||
!address ||
!stk ||
!btk ||
!contactName ||
!contactRank ||
!contactRegistry ||
!contactIdentity ||
!contactPhone
) {
return res.status(400).json({ message: 'Tum alanlar zorunludur.' });
}
try {
await updateUnit({
id,
name,
address,
stk,
btk,
contactName,
contactRank,
contactRegistry,
contactIdentity,
contactPhone
});
return res.json({ message: 'Birlik kaydı güncellendi.' });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu birlik adı ile kayıt mevcut.' });
}
console.error('Birlik guncelleme hatasi:', err);
return res.status(500).json({ message: 'Birlik güncellenemedi.' });
}
});
app.delete('/api/units/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await deleteUnit(id);
return res.status(204).end();
} catch (err) {
console.error('Birlik silme hatasi:', err);
return res.status(500).json({ message: 'Silme islemi tamamlanamadi.' });
}
});
app.get('/api/fuel-personnel', requireSession, requireAdmin, async (req, res) => {
try {
const personnel = await listFuelPersonnel();
return res.json({ personnel });
} catch (err) {
console.error('Personel listeleme hatasi:', err);
return res.status(500).json({ message: 'Yakıt personeli okunamadı.' });
}
});
app.post('/api/fuel-personnel', requireSession, requireAdmin, async (req, res) => {
const { fullName, rank, registryNumber, identityNumber, phone } = req.body || {};
if (!fullName || !rank || !registryNumber || !identityNumber || !phone) {
return res.status(400).json({ message: 'Personel bilgileri eksiksiz girilmeli.' });
}
try {
const created = await createFuelPersonnel({
fullName,
rank,
registryNumber,
identityNumber,
phone
});
return res.status(201).json({ personnel: created });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu sicil numarası ile kayıt mevcut.' });
}
console.error('Personel ekleme hatasi:', err);
return res.status(500).json({ message: 'Personel kaydedilemedi.' });
}
});
app.put('/api/fuel-personnel/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
const { fullName, rank, registryNumber, identityNumber, phone } = req.body || {};
if (!fullName || !rank || !registryNumber || !identityNumber || !phone) {
return res.status(400).json({ message: 'Tum alanlar zorunludur.' });
}
try {
await updateFuelPersonnel({
id,
fullName,
rank,
registryNumber,
identityNumber,
phone
});
return res.json({ message: 'Personel kaydı güncellendi.' });
} catch (err) {
if (err && err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ message: 'Bu sicil numarası ile kayıt mevcut.' });
}
console.error('Personel guncelleme hatasi:', err);
return res.status(500).json({ message: 'Guncelleme yapilamadi.' });
}
});
app.delete('/api/fuel-personnel/:id', requireSession, requireAdmin, async (req, res) => {
const { id } = req.params;
try {
await deleteFuelPersonnel(id);
return res.status(204).end();
} catch (err) {
console.error('Personel silme hatasi:', err);
return res.status(500).json({ message: 'Silme islemi tamamlanamadi.' });
}
});
app.get('/api/fuel/resources', requireSession, requireFuelManager, async (req, res) => {
try {
const [vehicles, units, personnel, inventoryManagers] = await Promise.all([
listVehicles(),
listUnits(),
listFuelPersonnel(),
listInventoryManagers()
]);
return res.json({
forces: FUEL_FORCES,
vehicles,
units,
personnel,
inventoryManagers
});
} catch (err) {
console.error('Kaynaklar okunamadi:', err);
return res.status(500).json({ message: 'Referans verileri yüklenemedi.' });
}
});
app.get('/api/fuel-slips', requireSession, requireFuelManager, async (req, res) => {
try {
const slips = await listFuelSlips();
return res.json({ slips });
} catch (err) {
console.error('Fisi listeleme hatasi:', err);
return res.status(500).json({ message: 'Fişler okunamadı.' });
}
});
app.get('/api/fuel-slips/assigned', requireSession, requireInventoryManager, async (req, res) => {
try {
const slips = await listFuelSlipsByInventoryManager(req.session.id);
return res.json({ slips });
} catch (err) {
console.error('Atanan fişleri listeleme hatasi:', err);
return res.status(500).json({ message: 'Fişler okunamadı.' });
}
});
app.post('/api/fuel-slips', requireSession, requireFuelManager, async (req, res) => {
const {
date,
force,
unitId,
vehicleId,
fuelAmountNumber,
fuelAmountText,
fuelType,
receiverId,
giverId,
receiverPhone,
giverPhone,
notes,
inventoryManagerId
} = req.body || {};
if (
!date ||
!force ||
!unitId ||
!vehicleId ||
!fuelAmountNumber ||
!fuelAmountText ||
!fuelType ||
!receiverId ||
!giverId ||
!inventoryManagerId
) {
return res.status(400).json({ message: 'Fiş oluşturmak için zorunlu alanları doldurun.' });
}
try {
const [unit, vehicle, receiver, giver, inventoryManager] = await Promise.all([
getUnitById(unitId),
getVehicleById(vehicleId),
getFuelPersonnelById(receiverId),
getFuelPersonnelById(giverId),
getInventoryManagerById(inventoryManagerId)
]);
if (!unit || !vehicle || !receiver || !giver || !inventoryManager) {
return res.status(404).json({ message: 'Referans kaydı bulunamadı.' });
}
const payload = {
date,
force,
unit: { id: unit.id, name: unit.name },
vehicle: {
id: vehicle.id,
description: `${vehicle.brand} ${vehicle.model}`.trim(),
plate: vehicle.plate
},
fuelAmountNumber: Number(fuelAmountNumber),
fuelAmountText,
fuelType,
receiver: {
id: receiver.id,
fullName: receiver.fullName,
rank: receiver.rank,
registryNumber: receiver.registryNumber,
phone: receiverPhone || receiver.phone
},
giver: {
id: giver.id,
fullName: giver.fullName,
rank: giver.rank,
registryNumber: giver.registryNumber,
phone: giverPhone || giver.phone
},
inventoryManager: {
id: inventoryManager.id,
displayName: inventoryManager.displayName
},
fuelManagerId: req.session.id,
notes: notes || null
};
const created = await createFuelSlip(payload);
const slip = await getFuelSlipById(created.id);
emitToInventoryManager(slip.inventoryManagerId, 'fuelSlip:new', slip);
emitToFuelManager(req.session.id, 'fuelSlip:status', slip);
return res.status(201).json({ slip });
} catch (err) {
console.error('Fiş olusturma hatasi:', err);
return res.status(500).json({ message: 'Fiş olusturulamadı.' });
}
});
app.patch('/api/fuel-slips/:id/status', requireSession, requireInventoryManager, async (req, res) => {
const { id } = req.params;
const { status, reason } = req.body || {};
if (!['approved', 'rejected'].includes(status)) {
return res.status(400).json({ message: 'Geçersiz durum değeri.' });
}
if (status === 'rejected' && (!reason || !reason.trim())) {
return res.status(400).json({ message: 'Red gerekçesi girilmeli.' });
}
try {
const slip = await getFuelSlipById(id);
if (!slip) {
return res.status(404).json({ message: 'Fiş bulunamadı.' });
}
if (slip.inventoryManagerId !== req.session.id) {
return res.status(403).json({ message: 'Bu fiş size atanmamış.' });
}
if (slip.status !== 'pending') {
return res.status(409).json({ message: 'Fiş durumu zaten güncellenmiş.' });
}
const trimmedReason = status === 'rejected' ? reason.trim() : null;
await updateFuelSlipStatus({ id, status, rejectionReason: trimmedReason });
const updated = await getFuelSlipById(id);
emitToFuelManager(updated.fuelManagerId, 'fuelSlip:status', updated);
emitToInventoryManager(updated.inventoryManagerId, 'fuelSlip:status', updated);
return res.json({ slip: updated });
} catch (err) {
console.error('Fiş durum güncelleme hatasi:', err);
return res.status(500).json({ message: 'Durum güncellenemedi.' });
}
});
app.get('/api/fuel-slips/:id/pdf', requireSession, requireFuelManager, async (req, res) => {
const { id } = req.params;
try {
const slip = await getFuelSlipById(id);
if (!slip) {
return res.status(404).json({ message: 'Fiş bulunamadı.' });
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename=akaryakit-senedi-${slip.slipNumber}.pdf`);
const doc = new PDFDocument({ size: 'A4', margin: 36 });
doc.pipe(res);
const title = 'TSK AKARYAKIT İKMAL SENEDİ';
doc.fontSize(18).text(title, { align: 'center', underline: true });
doc.moveDown(0.4);
doc.fontSize(11).text(`Seri No: ${slip.slipNumber}`, { align: 'center' });
doc.moveDown(0.6);
const startX = doc.page.margins.left;
let y = doc.y;
const tableWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
const rowHeight = 28;
const drawCell = (x, yPos, width, height) => {
doc.rect(x, yPos, width, height).stroke();
};
const writeCell = (x, yPos, width, label, value) => {
drawCell(x, yPos, width, rowHeight);
doc.fontSize(9).font('Helvetica-Bold').text(label, x + 6, yPos + 6, {
width: width - 12
});
if (value) {
doc.fontSize(10).font('Helvetica').text(value, x + 6, yPos + 14, {
width: width - 12
});
}
};
const halfWidth = tableWidth / 2;
writeCell(startX, y, halfWidth, 'Tarih', new Date(slip.slipDate).toLocaleDateString('tr-TR'));
writeCell(startX + halfWidth, y, halfWidth, 'Fiş No', String(slip.slipNumber).padStart(4, '0'));
y += rowHeight;
writeCell(startX, y, tableWidth, 'Aracın Ait Olduğu Kuvvet', slip.force);
y += rowHeight;
writeCell(startX, y, tableWidth, 'Aracın Ait Olduğu Birlik', slip.unitName);
y += rowHeight;
writeCell(startX, y, tableWidth, 'Aracın Cinsi', slip.vehicleDescription);
y += rowHeight;
writeCell(startX, y, tableWidth, 'Aracın Plakası', slip.plate);
y += rowHeight;
writeCell(startX, y, halfWidth, 'Yakıt Miktarı (Rakam)', `${slip.fuelAmountNumber}`);
writeCell(startX + halfWidth, y, halfWidth, 'Yakıt Miktarı (Yazı)', slip.fuelAmountText);
y += rowHeight;
writeCell(startX, y, halfWidth, 'Yakıt Cinsi', slip.fuelType);
writeCell(startX + halfWidth, y, halfWidth, 'Not', slip.notes || '-');
y += rowHeight;
writeCell(startX, y, halfWidth, 'Teslim Alan', `${slip.receiverName}\n${slip.receiverRank} / ${slip.receiverRegistry}`);
writeCell(startX + halfWidth, y, halfWidth, 'Teslim Eden', `${slip.giverName}\n${slip.giverRank} / ${slip.giverRegistry}`);
y += rowHeight;
writeCell(startX, y, halfWidth, 'Teslim Alan Telefonu', slip.receiverPhone);
writeCell(startX + halfWidth, y, halfWidth, 'Teslim Eden Telefonu', slip.giverPhone);
y += rowHeight;
doc.moveDown(2);
doc.fontSize(9).text('Not: Seri numarası yıl + sıra şeklinde takip edilir. Bu fiş sistem üzerinden oluşturulmuştur.', {
align: 'center'
});
doc.end();
} catch (err) {
console.error('PDF olusturma hatasi:', err);
return res.status(500).json({ message: 'PDF oluşturulamadı.' });
}
});
initialize()
.then(() => {
httpServer.listen(PORT, () => {
console.log(`Sunucu ${PORT} portunda hazir.`);
});
})
.catch((err) => {
console.error('Sunucu baslatilamadi:', err);
process.exit(1);
});
process.on('SIGINT', () => {
console.log('Sunucu kapatiliyor...');
io.close();
db.close(() => {
httpServer.close(() => {
process.exit(0);
});
});
});