fix: persist tender preparation and correct calendar export

This commit is contained in:
2026-09-06 08:30:32 +03:00
parent 909ebae7c2
commit c91631d99f
19 changed files with 569 additions and 182 deletions
+14
View File
@@ -1061,6 +1061,20 @@ const server = http.createServer(async (req, res) => {
}
}
if (url.pathname === '/api/me/tender-state') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
if (req.method === 'GET') return json(res, 200, {success:true, records:store.getTenderStates(user.email)});
if (req.method !== 'PUT') return json(res, 405, {success:false,error:'desteklenmeyen metot'});
try {
const body = await readBody(req, 512 * 1024);
const record = store.saveTenderState(user.email, body.key, body.patch, body.snapshot, body.version);
return json(res, 200, {success:true, record});
} catch (e) {
return json(res, e.kod === 'VERSION_CONFLICT' ? 409 : 400, {success:false,error:e.message,kod:e.kod});
}
}
// --- Kullanıcı verisi (localStorage yerine sunucu tarafı) ---
if (url.pathname.startsWith('/api/me/data')) {
const user = userFromAuthHeader(req.headers['authorization']);
+56 -13
View File
@@ -19,7 +19,7 @@ try {
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, 'data');
const DATA_DIR = process.env.IHALEIST_USER_DATA_DIR || path.join(__dirname, 'data');
const DB_PATH = path.join(DATA_DIR, 'users.sqlite');
export const BELGE_DIR = path.join(DATA_DIR, 'belgeler');
@@ -47,6 +47,11 @@ CREATE TABLE IF NOT EXISTS kullanici_verisi (
PRIMARY KEY (email, anahtar)
);
CREATE TABLE IF NOT EXISTS tender_state (
email TEXT NOT NULL, tender_key TEXT NOT NULL, data TEXT NOT NULL,
snapshot TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL,
PRIMARY KEY (email, tender_key)
);
CREATE TABLE IF NOT EXISTS api_anahtarlari (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
@@ -73,7 +78,7 @@ CREATE TABLE IF NOT EXISTS belgeler (
olusturma TEXT
);
CREATE INDEX IF NOT EXISTS idx_belge_email ON belgeler(email);
CREATE INDEX IF NOT EXISTS idx_belge_ikn ON belgeler(email, ikn);
`;
export function available() { return Boolean(DatabaseSync); }
@@ -83,26 +88,34 @@ export function openDb() {
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(BELGE_DIR, { recursive: true });
const db = new DatabaseSync(DB_PATH);
db.exec(SCHEMA);
// Geçiş: belgeler ihaleye bağlanabilsin diye ikn kolonu eklendi
try {
const k = db.prepare('PRAGMA table_info(belgeler)').all().map((c) => c.name);
if (!k.includes('ikn')) {
db.exec('ALTER TABLE belgeler ADD COLUMN ikn TEXT');
console.log('[user-store] belgeler.ikn kolonu eklendi');
db.exec('PRAGMA busy_timeout = 5000');
db.exec('BEGIN IMMEDIATE');
db.exec(SCHEMA);
const columns = new Set(db.prepare('PRAGMA table_info(belgeler)').all().map((c) => c.name));
// Existing volumes predate these columns. Indexes must be created AFTER migration.
for (const column of ['ikn', 'son_kullanma']) {
if (!columns.has(column)) db.exec(`ALTER TABLE belgeler ADD COLUMN ${column} TEXT`);
}
} catch (e) {
console.warn('[user-store] belge şema geçişi atlandı:', e.message);
db.exec('CREATE INDEX IF NOT EXISTS idx_belge_ikn ON belgeler(email, ikn)');
db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`);
db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)')
.run('20260906_document_columns', new Date().toISOString());
db.exec('COMMIT');
} catch (error) {
try { db.exec('ROLLBACK'); } catch { /* original error is authoritative */ }
db.close();
throw error;
}
return db;
}
const now = () => new Date().toISOString();
const withDb = (fn, fallback = null) => {
const withDb = (fn) => {
const db = openDb();
if (!db) return fallback;
if (!db) throw new Error('Kullanıcı veri deposu kullanılamıyor.');
// Never report a failed write as a successful operation or a failed read as an empty account.
try { return fn(db); }
catch (e) { console.warn('[user-store]', e.message); return fallback; }
finally { db.close(); }
};
@@ -275,3 +288,33 @@ export function documentUsage(email) {
return { adet: r?.adet ?? 0, bayt: r?.bayt ?? 0 };
}, { adet: 0, bayt: 0 });
}
// Per-tender compare-and-swap prevents stale tabs from overwriting newer preparation.
export function getTenderStates(email) {
return withDb(db => Object.fromEntries(db.prepare('SELECT * FROM tender_state WHERE email=?').all(email)
.map(r => [r.tender_key, { data: JSON.parse(r.data), snapshot: JSON.parse(r.snapshot), version: r.version }])));
}
export function saveTenderState(email, key, patch, snapshot, expectedVersion) {
const fields = new Set(['takipte','incelendi','kanbanStatus','atananPersonel','gorev','notes','checklist','bidDraft']);
if (!key || key.length > 100 || !patch || typeof patch !== 'object' || Array.isArray(patch)
|| Object.keys(patch).some(k => !fields.has(k)) || !Number.isInteger(expectedVersion) || expectedVersion < 0)
throw new Error('Geçersiz hazırlık kaydı.');
return withDb(db => {
db.exec('BEGIN IMMEDIATE');
try {
const row = db.prepare('SELECT * FROM tender_state WHERE email=? AND tender_key=?').get(email,key);
if ((row?.version || 0) !== expectedVersion) {
const error = new Error('Bu ihale başka bir sekmede değişti. Güncel kayıt yüklendi; değişikliğinizi tekrar uygulayın.');
error.kod = 'VERSION_CONFLICT'; throw error;
}
const record = { data: {...(row ? JSON.parse(row.data) : {}), ...patch},
snapshot: row ? {...JSON.parse(row.snapshot), ...snapshot} : snapshot,
version: expectedVersion + 1 };
db.prepare(`INSERT INTO tender_state VALUES (?,?,?,?,?,?)
ON CONFLICT(email,tender_key) DO UPDATE SET data=excluded.data,snapshot=excluded.snapshot,
version=excluded.version,updated_at=excluded.updated_at`)
.run(email,key,JSON.stringify(record.data),JSON.stringify(record.snapshot || {}),record.version,now());
db.exec('COMMIT'); return record;
} catch (error) { db.exec('ROLLBACK'); throw error; }
});
}