Files
ytp/server/index.js
2025-11-03 22:54:10 +03:00

836 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});
});