This commit is contained in:
odzugkoev
2026-03-01 17:10:03 -05:00
parent 7e0887c62d
commit 85cf732a61
19 changed files with 2284 additions and 32 deletions

View File

@@ -100,6 +100,30 @@ class DatabaseManager {
)
`);
// Abandoned call print tracking
this.db.exec(`
CREATE TABLE IF NOT EXISTS abandoned_call_prints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
abandoned_call_id INTEGER UNIQUE NOT NULL,
printed_at INTEGER NOT NULL,
printer_count INTEGER NOT NULL DEFAULT 0
)
`);
// Abandoned calls local cache
this.db.exec(`
CREATE TABLE IF NOT EXISTS abandoned_calls_cache (
id INTEGER PRIMARY KEY AUTOINCREMENT,
remote_id INTEGER UNIQUE NOT NULL,
data TEXT NOT NULL,
callback_status TEXT,
callback_priority TEXT,
callback_score INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
// Create indexes
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
@@ -108,6 +132,9 @@ class DatabaseManager {
CREATE INDEX IF NOT EXISTS idx_print_queue_status ON print_queue(status);
CREATE INDEX IF NOT EXISTS idx_printers_enabled ON printers(is_enabled);
CREATE INDEX IF NOT EXISTS idx_printers_default ON printers(is_default);
CREATE INDEX IF NOT EXISTS idx_abandoned_prints_call_id ON abandoned_call_prints(abandoned_call_id);
CREATE INDEX IF NOT EXISTS idx_abandoned_cache_remote ON abandoned_calls_cache(remote_id);
CREATE INDEX IF NOT EXISTS idx_abandoned_cache_status ON abandoned_calls_cache(callback_status);
`);
// Initialize default config values if not exists
@@ -115,6 +142,9 @@ class DatabaseManager {
// Migrate old printer config to new table if needed
this.migrateOldPrinterConfig();
// Add print_abandoned_calls column to printers if missing
this.migratePrintersAddAbandonedCalls();
}
setConfigDefaults() {
@@ -474,9 +504,9 @@ class DatabaseManager {
header_text, footer_text,
business_name, business_address, business_phone, business_website, business_email,
business_contact_size, show_customer_info, show_order_items, show_prices, show_timestamps,
logo_path, logo_max_width_dots, created_at, updated_at
logo_path, logo_max_width_dots, print_abandoned_calls, created_at, updated_at
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
`);
@@ -509,6 +539,7 @@ class DatabaseManager {
config.show_timestamps !== false ? 1 : 0,
config.logo_path || null,
config.logo_max_width_dots || null,
config.print_abandoned_calls !== false ? 1 : 0,
now,
now
);
@@ -531,7 +562,7 @@ class DatabaseManager {
header_text = ?, footer_text = ?,
business_name = ?, business_address = ?, business_phone = ?, business_website = ?, business_email = ?,
business_contact_size = ?, show_customer_info = ?, show_order_items = ?, show_prices = ?, show_timestamps = ?,
logo_path = ?, logo_max_width_dots = ?, updated_at = ?
logo_path = ?, logo_max_width_dots = ?, print_abandoned_calls = ?, updated_at = ?
WHERE id = ?
`);
@@ -564,6 +595,7 @@ class DatabaseManager {
config.show_timestamps !== false ? 1 : 0,
config.logo_path || null,
config.logo_max_width_dots || null,
config.print_abandoned_calls !== false ? 1 : 0,
now,
id
);
@@ -647,6 +679,7 @@ class DatabaseManager {
show_timestamps: row.show_timestamps === 1,
logo_path: row.logo_path,
logo_max_width_dots: row.logo_max_width_dots,
print_abandoned_calls: row.print_abandoned_calls == null ? true : row.print_abandoned_calls === 1,
created_at: row.created_at,
updated_at: row.updated_at
};
@@ -710,6 +743,128 @@ class DatabaseManager {
}
}
migratePrintersAddAbandonedCalls() {
try {
const cols = this.db.pragma('table_info(printers)');
const hasCol = cols.some(c => c.name === 'print_abandoned_calls');
if (!hasCol) {
this.db.exec('ALTER TABLE printers ADD COLUMN print_abandoned_calls INTEGER NOT NULL DEFAULT 1');
console.log('Migration: added print_abandoned_calls column to printers');
}
} catch (err) {
console.error('Migration error (print_abandoned_calls):', err.message);
}
}
getAbandonedCallPrinters() {
const rows = this.db.prepare(
'SELECT * FROM printers WHERE is_enabled = 1 AND print_abandoned_calls = 1 ORDER BY is_default DESC, name ASC'
).all();
return rows.map(row => this.mapPrinterRow(row));
}
// Abandoned call print tracking
hasAbandonedCallPrint(abandonedCallId) {
const row = this.db.prepare(
'SELECT 1 FROM abandoned_call_prints WHERE abandoned_call_id = ? LIMIT 1'
).get(abandonedCallId);
return !!row;
}
addAbandonedCallPrint(abandonedCallId, printerCount = 0) {
const now = Math.floor(Date.now() / 1000);
try {
this.db.prepare(
'INSERT OR IGNORE INTO abandoned_call_prints (abandoned_call_id, printed_at, printer_count) VALUES (?, ?, ?)'
).run(abandonedCallId, now, printerCount);
} catch (_) {}
}
getLastAbandonedCallPrintTime() {
const row = this.db.prepare(
'SELECT printed_at FROM abandoned_call_prints ORDER BY printed_at DESC LIMIT 1'
).get();
return row ? row.printed_at : 0;
}
// Abandoned calls cache
cacheAbandonedCall(remoteId, data) {
const now = Math.floor(Date.now() / 1000);
const json = typeof data === 'string' ? data : JSON.stringify(data);
const callbackStatus = (typeof data === 'object' ? data.callback_status : null) || null;
const callbackPriority = (typeof data === 'object' ? data.callback_priority : null) || null;
const callbackScore = (typeof data === 'object' ? Number(data.callback_score) : 0) || 0;
this.db.prepare(`
INSERT INTO abandoned_calls_cache (remote_id, data, callback_status, callback_priority, callback_score, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(remote_id) DO UPDATE SET
data = excluded.data,
callback_status = excluded.callback_status,
callback_priority = excluded.callback_priority,
callback_score = excluded.callback_score,
updated_at = excluded.updated_at
`).run(remoteId, json, callbackStatus, callbackPriority, callbackScore, now, now);
}
getCachedAbandonedCalls(options = {}) {
let query = 'SELECT * FROM abandoned_calls_cache WHERE 1=1';
const params = [];
if (options.status) {
query += ' AND callback_status = ?';
params.push(options.status);
}
if (options.priority) {
query += ' AND callback_priority = ?';
params.push(options.priority);
}
query += ' ORDER BY callback_score DESC, updated_at DESC';
if (options.limit) {
query += ' LIMIT ?';
params.push(options.limit);
}
const rows = this.db.prepare(query).all(...params);
return rows.map(row => {
try { return JSON.parse(row.data); } catch (_) { return null; }
}).filter(Boolean);
}
updateCachedCallbackStatus(remoteId, status) {
const now = Math.floor(Date.now() / 1000);
this.db.prepare(
'UPDATE abandoned_calls_cache SET callback_status = ?, updated_at = ? WHERE remote_id = ?'
).run(status, now, remoteId);
}
getPendingAbandonedCallCount() {
const row = this.db.prepare(
"SELECT COUNT(*) as count FROM abandoned_calls_cache WHERE callback_status IN ('queued', 'deferred')"
).get();
return row ? row.count : 0;
}
getAbandonedCallStats() {
const row = this.db.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN callback_status IN ('queued','deferred') THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN callback_status = 'converted' THEN 1 ELSE 0 END) as converted,
SUM(CASE WHEN callback_status = 'dismissed' THEN 1 ELSE 0 END) as dismissed
FROM abandoned_calls_cache
`).get();
return {
total: (row && row.total) || 0,
pending: (row && row.pending) || 0,
converted: (row && row.converted) || 0,
dismissed: (row && row.dismissed) || 0
};
}
cleanOldAbandonedCallCache(maxAgeDays = 7) {
const cutoff = Math.floor(Date.now() / 1000) - (maxAgeDays * 86400);
this.db.prepare('DELETE FROM abandoned_calls_cache WHERE updated_at < ?').run(cutoff);
this.db.prepare('DELETE FROM abandoned_call_prints WHERE printed_at < ?').run(cutoff);
}
close() {
if (this.db) {
this.db.close();