first commit

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

20
client/index.html Normal file
View File

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

20
client/package.json Normal file
View File

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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