From 7edbab268979a7300212cdf5804f1d70bddcbaa4 Mon Sep 17 00:00:00 2001 From: sbilketay Date: Mon, 3 Nov 2025 22:54:10 +0300 Subject: [PATCH] first commit --- .gitignore | 65 + client/index.html | 20 + client/package.json | 20 + client/src/App.svelte | 266 +++ client/src/api.js | 224 +++ client/src/app.css | 1 + client/src/components/AdminPanel.svelte | 1447 +++++++++++++++++ client/src/components/FuelManagerPanel.svelte | 782 +++++++++ .../components/InventoryManagerPanel.svelte | 632 +++++++ client/src/components/LoginView.svelte | 303 ++++ client/src/components/RoleWelcome.svelte | 65 + client/src/lib/numberToWordsTr.js | 91 ++ client/src/main.js | 9 + client/src/styles.css | 22 + client/svelte.config.js | 5 + client/vite.config.js | 19 + data/app.db | Bin 0 -> 49152 bytes package.json | 25 + server/db-init.js | 13 + server/db.js | 916 +++++++++++ server/index.js | 835 ++++++++++ 21 files changed, 5760 insertions(+) create mode 100644 .gitignore create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/src/App.svelte create mode 100644 client/src/api.js create mode 100644 client/src/app.css create mode 100644 client/src/components/AdminPanel.svelte create mode 100644 client/src/components/FuelManagerPanel.svelte create mode 100644 client/src/components/InventoryManagerPanel.svelte create mode 100644 client/src/components/LoginView.svelte create mode 100644 client/src/components/RoleWelcome.svelte create mode 100644 client/src/lib/numberToWordsTr.js create mode 100644 client/src/main.js create mode 100644 client/src/styles.css create mode 100644 client/svelte.config.js create mode 100644 client/vite.config.js create mode 100644 data/app.db create mode 100644 package.json create mode 100644 server/db-init.js create mode 100644 server/db.js create mode 100644 server/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f957bde --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..820d22d --- /dev/null +++ b/client/index.html @@ -0,0 +1,20 @@ + + + + + + + + Akaryakit Istasyonu Giris + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..eeba8a9 --- /dev/null +++ b/client/package.json @@ -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" + } +} diff --git a/client/src/App.svelte b/client/src/App.svelte new file mode 100644 index 0000000..effb628 --- /dev/null +++ b/client/src/App.svelte @@ -0,0 +1,266 @@ + + +
+
+
+
+

Yakit Takip Sistemi

+

İstasyondaki stok ve yakıt hareketlerini tek ekrandan yönetin.

+
+ {#if user} +
+ + +
+ {/if} +
+
+ +
+ {#if loading} +
Oturum dogrulaniyor...
+ {:else if user} +
+ {#if feedback} + + {/if} + {#if user.role === 'admin'} + + {:else if user.role === 'fuel_manager'} + + {:else if user.role === 'inventory_manager'} + + {:else} + + {/if} +
+ {:else} +
+ {#if feedback} + + {/if} + +
+ {/if} +
+
+ + diff --git a/client/src/api.js b/client/src/api.js new file mode 100644 index 0000000..245538c --- /dev/null +++ b/client/src/api.js @@ -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 + }); +} diff --git a/client/src/app.css b/client/src/app.css new file mode 100644 index 0000000..c9d6c8f --- /dev/null +++ b/client/src/app.css @@ -0,0 +1 @@ +@import '@fortawesome/fontawesome-free/css/all.min.css'; diff --git a/client/src/components/AdminPanel.svelte b/client/src/components/AdminPanel.svelte new file mode 100644 index 0000000..a73115e --- /dev/null +++ b/client/src/components/AdminPanel.svelte @@ -0,0 +1,1447 @@ + + +
+ + +
+ {#if activeSection === 'inventory'} +
+
+
+

{editing.inventory ? 'Mal sorumlusu düzenle' : 'Mal sorumlusu ekle'}

+ {#if editing.inventory} + + {/if} +
+

+ {editing.inventory + ? 'Var olan kaydı güncelleyebilir, yeni şifre vermek için alanı doldurabilirsiniz.' + : 'Depo sürecini yönetecek kullanıcıyı tanımlayın.'} +

+ + {#if feedback.inventory} +
{feedback.inventory.text}
+ {/if} + +
+ + +
+ + + + +
+ +
+
+
+

Kayıtlı mal sorumluları

+

Oluşturma tarihi ve kullanıcı adı ile listelenir.

+
+ +
+ + {#if loading.inventory} +

Liste yükleniyor...

+ {:else if lists.inventory.length === 0} +

Henüz mal sorumlusu oluşturulmadı.

+ {:else} + + + + + + + + + + + {#each lists.inventory as manager} + + + + + + + {/each} + +
Görünen adKullanıcı adıOluşturma tarihiİşlemler
{manager.displayName}{manager.username}{formatDate(manager.createdAt)} + + +
+ {/if} + +
+
+ {:else if activeSection === 'vehicles'} +
+
+
+

{editing.vehicles ? 'Araç kaydını düzenle' : 'Araç oluştur'}

+ {#if editing.vehicles} + + {/if} +
+

+ {editing.vehicles + ? 'Araç bilgilerini güncelleyebilir, plaka değişikliklerini kaydedebilirsiniz.' + : 'Yakıt transferinde kullanılacak araç bilgilerini girin.'} +

+ + {#if feedback.vehicles} +
{feedback.vehicles.text}
+ {/if} + +
+ + +
+ +
+ + +
+ + +
+ +
+
+
+

Araç listesi

+

Plaka bazlı kayıtlarınızı buradan görüntüleyin.

+
+ +
+ + {#if loading.vehicles} +

Liste yükleniyor...

+ {:else if lists.vehicles.length === 0} +

Kayıtlı aracınız bulunmuyor.

+ {:else} + + + + + + + + + + + + {#each lists.vehicles as vehicle} + + + + + + + + {/each} + +
AraçModel yılıPlakaEklenmeİşlemler
{vehicle.brand} {vehicle.model}{vehicle.year}{vehicle.plate}{formatDate(vehicle.createdAt)} + + +
+ {/if} + +
+
+ {:else if activeSection === 'units'} +
+
+
+

{editing.units ? 'Birlik kaydını düzenle' : 'Birlik ekle'}

+ {#if editing.units} + + {/if} +
+

+ {editing.units + ? 'Birlik ve sorumlu bilgilerini güncelleyebilirsiniz.' + : 'Birlik ve sorumlularını ilişkilendirerek kayıt edin.'} +

+ + {#if feedback.units} +
{feedback.units.text}
+ {/if} + + + + + + + +
+ +
+
+

Oluşturulan fişler

+ +
+ + {#if slipsLoading} +

Fişler yükleniyor...

+ {:else if slips.length === 0} +

Henüz fiş kaydı bulunmuyor.

+ {:else} + + + + + + + + + + + + + + + {#each slips as slip (slip.id)} + + + + + + + + + + + {/each} + +
Fiş NoTarihBirlikMal sorumlusuAraçYakıtDurumPDF
{formatSlipNumber(slip.slipNumber)}{formatDate(slip.slipDate)}{slip.unitName}{slip.inventoryManagerName || '-'}{slip.vehicleDescription}{slip.fuelAmountNumber} lt + {#if slip.status === 'rejected' && slip.rejectionReason} + + {:else} + + {statusLabels[slip.status] || slip.status} + + {/if} + + +
+ {/if} +
+
+
+ + diff --git a/client/src/components/InventoryManagerPanel.svelte b/client/src/components/InventoryManagerPanel.svelte new file mode 100644 index 0000000..e26ef89 --- /dev/null +++ b/client/src/components/InventoryManagerPanel.svelte @@ -0,0 +1,632 @@ + + +
+ + {#if feedback} +
{feedback.text}
+ {/if} + +
+
+

Bekleyen fişler

+ +
+ + {#if loading} +

Fişler yükleniyor...

+ {:else if slips.length === 0} +

Henüz size atanmış fiş bulunmuyor.

+ {:else} + {#if slips.filter(slip => slip.status === 'pending').length === 0} +

Bekleyen fiş yok

+ {:else} +
+ {#each slips.filter(slip => slip.status === 'pending') as slip (slip.id)} +
+
+
+

Seri No: {String(slip.slipNumber).padStart(4, '0')}

+ Tarih: {formatDate(slip.slipDate)} +
+ {#if slip.status === 'pending'} + + Beklemede + + {:else if slip.status === 'approved'} + + Kabul + + {:else if slip.status === 'rejected'} + + Red + + {/if} +
+ +
+
+
Birlik
+
{slip.unitName}
+
+
+
Araç
+
{slip.vehicleDescription} ({slip.plate})
+
+
+
Yakıt
+
{slip.fuelAmountNumber} lt • {slip.fuelType}
+
+
+
Teslim alan
+
{slip.receiverName} ({slip.receiverRank}) • {slip.receiverPhone}
+
+
+
Teslim eden
+
{slip.giverName} ({slip.giverRank}) • {slip.giverPhone}
+
+ {#if slip.notes} +
+
Not
+
{slip.notes}
+
+ {/if} + {#if slip.status === 'rejected' && slip.rejectionReason} +
+
Red gerekçesi
+
{slip.rejectionReason}
+
+ {/if} +
+ + {#if slip.status === 'pending'} + {#if rejectingId === slip.id} +
+ +
+ + +
+
+ {:else} +
+ + +
+ {/if} + {/if} +
+ {/each} +
+ {/if} + {/if} +
+ +
+

İşlem Yapılan Fişler

+
+ Seri No + Tarih + Birlik + Araç + Yakıt + Teslim Alan + Teslim Eden + Durum +
+ {#each slips.filter(s => s.status === 'approved' || s.status === 'rejected') as slip} +
+ {String(slip.slipNumber).padStart(4, '0')} + {formatDate(slip.slipDate)} + {slip.unitName} + {slip.vehicleDescription} ({slip.plate}) + {slip.fuelAmountNumber} lt • {slip.fuelType} + {slip.receiverName} + {slip.giverName} + {#if slip.status === 'approved'} + + {statusLabels[slip.status]} + + {:else if slip.status === 'rejected'} + +  {statusLabels[slip.status]} + + {/if} +
+ {/each} +
+
+ + \ No newline at end of file diff --git a/client/src/components/LoginView.svelte b/client/src/components/LoginView.svelte new file mode 100644 index 0000000..a8cfde7 --- /dev/null +++ b/client/src/components/LoginView.svelte @@ -0,0 +1,303 @@ + + +
+
+

Hos geldiniz

+

+ Akaryakit istasyonunda rolunuze uygun kontrol paneline erismek icin lutfen bilgilerinizi girin. +

+
+ +
+ + + + + {#if error} +
{error}
+ {/if} + + +
+ +
+

Ornek kullanicilar

+
+ {#each sampleUsers as sample} +
+
+ {sample.role} + +
+
+
+
Kullanici adi
+
{sample.username}
+
+
+
Sifre
+
{sample.password}
+
+
+

{sample.description}

+
+ {/each} +
+
+
+ + diff --git a/client/src/components/RoleWelcome.svelte b/client/src/components/RoleWelcome.svelte new file mode 100644 index 0000000..172566b --- /dev/null +++ b/client/src/components/RoleWelcome.svelte @@ -0,0 +1,65 @@ + + +
+

Merhaba {user.displayName}

+

{role.title}

+

{role.message}

+ +
+ + diff --git a/client/src/lib/numberToWordsTr.js b/client/src/lib/numberToWordsTr.js new file mode 100644 index 0000000..61ce588 --- /dev/null +++ b/client/src/lib/numberToWordsTr.js @@ -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; diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 0000000..2dac04e --- /dev/null +++ b/client/src/main.js @@ -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; diff --git a/client/src/styles.css b/client/src/styles.css new file mode 100644 index 0000000..cbdb48c --- /dev/null +++ b/client/src/styles.css @@ -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; +} diff --git a/client/svelte.config.js b/client/svelte.config.js new file mode 100644 index 0000000..21b9399 --- /dev/null +++ b/client/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: vitePreprocess() +}; diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..0412b8f --- /dev/null +++ b/client/vite.config.js @@ -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 + } + } + } +}); diff --git a/data/app.db b/data/app.db new file mode 100644 index 0000000000000000000000000000000000000000..31e07757a1a6ed35e39f7c0fe6f1ac34ad1b4532 GIT binary patch literal 49152 zcmeI5OKjuD8GuRAmMpDpHr*DAVW=Lj8_0sK2SvTCZGdQPc~_MmyRwATxd^nywHZ^Q zLQ>9JL4oeJL4Y1xw5PTk^pe9OZT1qNx1x4{=8&L3fW8iCf+T3sUfKeE9D3+K)O%N! zoNn7>lb^e$$l*Vu`M>#RNX_Ht*0nXol*x5XFH0s7na?mR%RE5{!*JIah8c&y*5-tR zk=6-(vR%ig96}la{f86$Kbdpfqs-)W{*B4cjo+Dg&-MKH*T#PCy23p=_U%)mVlWc; z2nl@ihRfleon^ms(Uf*n`Fc%Oi-xLHjn2`z>}Ub$UemQ1-+ zgo>L_e=(yOV$TAnNyaydw}n9r=IxALj=tG&^zJPc{l`YrR6yAnXnl zs8tlR*ri+lWGtzQR34brp}eo`s`BvET7o6n*wvM)sc4l!I#ktRO43r5%37^rws=F4 zu95z_SS8$)Hsu>l=mTWUqv}Atb$M4-4rP6~;%&)cDBFU%R5=*LrXwitDTb*Zok_U5 zuT=)Bw0Aar8e+qgX-Eyzq$zaUf!x)DJhjb?;=qu zhjPW#^rK=~sz`f1MY-5$68c@x<)`IcYQ06M!A8@Fd+XooW-gt%ykRvJ^Ub>EBf0cy z8mjhkdW+C7V_qrw2E=RC7m(_X+6*2RyKN%(M7QceRn{Tn6sTb>#Ji9x6j8uAMLc)?L7A8*m-}u-Ijdd zw)=WbRg1$6dzeY=OMxC5NhHYp{rL&ZGmiP67l8fyP%zO8f@+C`_`z3CfR zXl=;Nf@`0Qs+6Yza@1&sSGsA|EOvP zNA2!q&uNHTpo&84$bOZxyB8MNZ3}a=0XF1kYI0>)zUOqL2l_o1=c#EgLQLPS5^GiP z=e>;zZou86Mt8QJ_*PI)iFG|-p6TJ4rqHQS16Woi!+2KH2b8R?srNkUkmW1s;BTb(ZlI|PX%6I&t1Bt7*OEQRu8QCAHJ-8KHI>`M1ezPq8}Xm zv>Ky7uO{GcWbz%3{~3Q{@*RGWhT(<;kN^@u0!RP}AOR$R1dsp{KmthMVI%P63l3&_ zF6b4-sIaQ2ngabf`$|~|i;??&rk}jhxJa^+E`>wks9zBLA(04TQj8=;;qsn?nehbY z#ImFsnqDgl(t%d*o7AdgD*GgLaQte$JE2H297{$c@c)X1QYyEm{I;^hJTs=3;zNC3;w5{K5D2j5a6OC%0>4O?w*N38Wm5ONy?Pl`VL; z&5=UZrTqX|k*gZGqdN$Y4K0}g51gmJS0XZRir& zR@Aaohd`-9Lu*EQTNEQvVJQ)hMI&NZSXx*Lk*F9ZOG^nN2qY5e9;%Kdg>W(yzxl=b zIO8pQJPXq_tLw6UAlGRYRpns0R^G4a%8_g~vU&1K%k$l`RI>6ago6UHf-5^Um1Z~+ zO)Sw2FNMOwc6}#6)@wSvN-HZJN!zvhj$|n7S&)f`qmg)GDF)dLwXzwB#z-7;+B3%A z%I1Q9oGBE}pZEIb=H{s1K}xNn)X5bYa;jBLc&!(8MM$y%_2ll!?UTFB%hI9L38PL3 zO}Q!uv)Zl%5yPQGIuYu)C`{#2h2SNUGIj#T-#dQk@U`0km_(2WhC@+75NN(*QOI^E z5sCt0QH;kEJyQl?MnWhVj#B@B9Dj%5f5qS7U;Bg_IjV&OkN^@u0!RP}AOR$R1dsp{ zKmthM;UO@^#o507GtWK3dD))skor7VppF2C_B{9-Fasf*+E_`ipS+TL?FYxNUJuZ@ z3db)r$IpczjOmAA_&hAeV0J=8XpQ)X!y<`9Neou%cfS|VT6Ql5#+Zv?p-Iapbi@7Ah0T27DYA0> z-CTZMGc{eQoPCt+zM-+e*@wTZxZPu3y8N`fYs#fP+a_HrOe8~k$Y7#-m7pJg7bgZl6$O()(Bzgg!uH^|Cnw(wLF%!6q6 z08l$%@{+FXlO09fQw)V1$^_o$eR9_%W#zz>^pm?}Uos_FQ5+zla6k}XIdC{if+QRw zs(DizX&DteGfOeTXkxI9ks0Zx7)y$wWJv4;ooZAz%P~0t&*UAyHjpbly&{WIkF` z=_r58Z)+-Fpvq6(qx_S$j;5CK(PU_;6Li)})1SpSu*hUTVic`fbHTPHSHLMiyK1ak zl?7Jr2cl3dh^9)r(m~+nv+NY(6lbSdi&v}7rFDeqa3Mk@8CgmSUCs$s>VYi$J5w9e zA6G2BwwC#CwZI>~uF>`XcK&UK{}*`w|1 { + console.log('Veritabani hazirlandi.'); + }) + .catch((err) => { + console.error('Veritabani hazirlama hatasi:', err); + process.exitCode = 1; + }) + .finally(() => { + db.close(); + }); diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..a88a7dc --- /dev/null +++ b/server/db.js @@ -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 +}; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..90ef4d0 --- /dev/null +++ b/server/index.js @@ -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); + }); + }); +});