first commit
This commit is contained in:
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal 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
20
client/index.html
Normal 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
20
client/package.json
Normal 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
266
client/src/App.svelte
Normal 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
224
client/src/api.js
Normal 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
1
client/src/app.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||||
1447
client/src/components/AdminPanel.svelte
Normal file
1447
client/src/components/AdminPanel.svelte
Normal file
File diff suppressed because it is too large
Load Diff
782
client/src/components/FuelManagerPanel.svelte
Normal file
782
client/src/components/FuelManagerPanel.svelte
Normal 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ı açı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>
|
||||||
632
client/src/components/InventoryManagerPanel.svelte
Normal file
632
client/src/components/InventoryManagerPanel.svelte
Normal 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> {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>
|
||||||
303
client/src/components/LoginView.svelte
Normal file
303
client/src/components/LoginView.svelte
Normal 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>
|
||||||
65
client/src/components/RoleWelcome.svelte
Normal file
65
client/src/components/RoleWelcome.svelte
Normal 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>
|
||||||
91
client/src/lib/numberToWordsTr.js
Normal file
91
client/src/lib/numberToWordsTr.js
Normal 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
9
client/src/main.js
Normal 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
22
client/src/styles.css
Normal 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
5
client/svelte.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess()
|
||||||
|
};
|
||||||
19
client/vite.config.js
Normal file
19
client/vite.config.js
Normal 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
BIN
data/app.db
Normal file
Binary file not shown.
25
package.json
Normal file
25
package.json
Normal 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
13
server/db-init.js
Normal 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
916
server/db.js
Normal 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
835
server/index.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user