date drop added
This commit is contained in:
39
database.js
39
database.js
@@ -301,8 +301,16 @@ class DatabaseManager {
|
|||||||
params.push(filters.status);
|
params.push(filters.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.date) {
|
if (filters.startDate && filters.endDate) {
|
||||||
// Get orders from start of day to end of day
|
query += ' AND created_at BETWEEN ? AND ?';
|
||||||
|
params.push(filters.startDate, filters.endDate);
|
||||||
|
} else if (filters.startDate) {
|
||||||
|
query += ' AND created_at >= ?';
|
||||||
|
params.push(filters.startDate);
|
||||||
|
} else if (filters.endDate) {
|
||||||
|
query += ' AND created_at <= ?';
|
||||||
|
params.push(filters.endDate);
|
||||||
|
} else if (filters.date) {
|
||||||
const startOfDay = Math.floor(new Date(filters.date).setHours(0, 0, 0, 0) / 1000);
|
const startOfDay = Math.floor(new Date(filters.date).setHours(0, 0, 0, 0) / 1000);
|
||||||
const endOfDay = Math.floor(new Date(filters.date).setHours(23, 59, 59, 999) / 1000);
|
const endOfDay = Math.floor(new Date(filters.date).setHours(23, 59, 59, 999) / 1000);
|
||||||
query += ' AND created_at BETWEEN ? AND ?';
|
query += ' AND created_at BETWEEN ? AND ?';
|
||||||
@@ -333,9 +341,7 @@ class DatabaseManager {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrderStats() {
|
getOrderStats(startDate = null, endDate = null) {
|
||||||
const today = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
|
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
total: 0,
|
total: 0,
|
||||||
new: 0,
|
new: 0,
|
||||||
@@ -343,12 +349,23 @@ class DatabaseManager {
|
|||||||
ready: 0
|
ready: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows = this.db.prepare(`
|
let query, params;
|
||||||
SELECT local_status, COUNT(*) as count
|
if (startDate != null && endDate != null) {
|
||||||
FROM orders
|
query = 'SELECT local_status, COUNT(*) as count FROM orders WHERE created_at BETWEEN ? AND ? GROUP BY local_status';
|
||||||
WHERE created_at >= ?
|
params = [startDate, endDate];
|
||||||
GROUP BY local_status
|
} else if (startDate != null) {
|
||||||
`).all(today);
|
query = 'SELECT local_status, COUNT(*) as count FROM orders WHERE created_at >= ? GROUP BY local_status';
|
||||||
|
params = [startDate];
|
||||||
|
} else if (endDate != null) {
|
||||||
|
query = 'SELECT local_status, COUNT(*) as count FROM orders WHERE created_at <= ? GROUP BY local_status';
|
||||||
|
params = [endDate];
|
||||||
|
} else {
|
||||||
|
const today = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000);
|
||||||
|
query = 'SELECT local_status, COUNT(*) as count FROM orders WHERE created_at >= ? GROUP BY local_status';
|
||||||
|
params = [today];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = this.db.prepare(query).all(...params);
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (row.local_status === 'new') stats.new = row.count;
|
if (row.local_status === 'new') stats.new = row.count;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kitchen-agent",
|
"name": "kitchen-agent",
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"description": "Kitchen Agent for ThinkLink Food Order Management",
|
"description": "Kitchen Agent for ThinkLink Food Order Management",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -420,6 +420,70 @@ button, a, input, select, textarea {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Date Filter */
|
||||||
|
.date-filter-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-select {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 48px;
|
||||||
|
min-width: 160px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-custom {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-input {
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
min-height: 44px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-separator {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-apply {
|
||||||
|
padding: 9px 18px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
min-height: 44px !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* Sync Button with Integrated Loading State */
|
/* Sync Button with Integrated Loading State */
|
||||||
.sync-button {
|
.sync-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1047,6 +1111,14 @@ button, a, input, select, textarea {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-filter-group {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-select {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -1074,6 +1146,28 @@ button, a, input, select, textarea {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-filter-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-custom {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-filter-separator {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.status-modal-content {
|
.status-modal-content {
|
||||||
padding: 25px 20px;
|
padding: 25px 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// Dashboard functionality
|
// Dashboard functionality
|
||||||
|
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
|
let currentDateFilter = 'today';
|
||||||
|
let customDateStart = '';
|
||||||
|
let customDateEnd = '';
|
||||||
let currentOrderIdForCancel = null;
|
let currentOrderIdForCancel = null;
|
||||||
let pendingStatusChange = {
|
let pendingStatusChange = {
|
||||||
orderId: null,
|
orderId: null,
|
||||||
@@ -118,6 +121,7 @@ function refreshAbandonedCallCount() {
|
|||||||
// Initialize dashboard
|
// Initialize dashboard
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
setupFilterButtons();
|
setupFilterButtons();
|
||||||
|
setupDateFilter();
|
||||||
audioNotification.init();
|
audioNotification.init();
|
||||||
refreshOrders();
|
refreshOrders();
|
||||||
refreshAbandonedCallCount();
|
refreshAbandonedCallCount();
|
||||||
@@ -136,19 +140,135 @@ function setupFilterButtons() {
|
|||||||
|
|
||||||
filterButtons.forEach(button => {
|
filterButtons.forEach(button => {
|
||||||
button.addEventListener('click', function() {
|
button.addEventListener('click', function() {
|
||||||
// Remove active class from all buttons
|
|
||||||
filterButtons.forEach(btn => btn.classList.remove('active'));
|
filterButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
|
||||||
// Add active class to clicked button
|
|
||||||
this.classList.add('active');
|
this.classList.add('active');
|
||||||
|
|
||||||
// Update filter and refresh
|
|
||||||
currentFilter = this.dataset.filter;
|
currentFilter = this.dataset.filter;
|
||||||
refreshOrders();
|
refreshOrders();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupDateFilter() {
|
||||||
|
const select = document.getElementById('dateFilterSelect');
|
||||||
|
const customPanel = document.getElementById('dateFilterCustom');
|
||||||
|
const applyBtn = document.getElementById('dateFilterApply');
|
||||||
|
const fromInput = document.getElementById('dateFilterFrom');
|
||||||
|
const toInput = document.getElementById('dateFilterTo');
|
||||||
|
|
||||||
|
if (!select || !customPanel || !applyBtn || !fromInput || !toInput) return;
|
||||||
|
|
||||||
|
// Set default dates on the pickers to today
|
||||||
|
const todayStr = formatDateForInput(new Date());
|
||||||
|
fromInput.value = todayStr;
|
||||||
|
toInput.value = todayStr;
|
||||||
|
|
||||||
|
select.addEventListener('change', function() {
|
||||||
|
const val = this.value;
|
||||||
|
currentDateFilter = val;
|
||||||
|
|
||||||
|
if (val === 'custom') {
|
||||||
|
customPanel.style.display = 'flex';
|
||||||
|
// Entering custom mode should immediately reflect the currently selected dates
|
||||||
|
// (defaults to today on first use). Further changes require pressing Apply.
|
||||||
|
if (fromInput.value && toInput.value) {
|
||||||
|
customDateStart = fromInput.value;
|
||||||
|
customDateEnd = toInput.value;
|
||||||
|
}
|
||||||
|
refreshOrders();
|
||||||
|
} else {
|
||||||
|
customPanel.style.display = 'none';
|
||||||
|
refreshOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatsTotalLabel();
|
||||||
|
});
|
||||||
|
|
||||||
|
applyBtn.addEventListener('click', function() {
|
||||||
|
if (!fromInput.value || !toInput.value) {
|
||||||
|
showToast('Please select both start and end dates', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fromInput.value > toInput.value) {
|
||||||
|
showToast('Start date must be before end date', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
customDateStart = fromInput.value;
|
||||||
|
customDateEnd = toInput.value;
|
||||||
|
refreshOrders();
|
||||||
|
updateStatsTotalLabel();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow pressing Enter in date inputs to apply
|
||||||
|
fromInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') applyBtn.click(); });
|
||||||
|
toInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') applyBtn.click(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateRangeParams() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
switch (currentDateFilter) {
|
||||||
|
case 'today': {
|
||||||
|
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
|
||||||
|
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||||
|
return { startDate: Math.floor(start.getTime() / 1000), endDate: Math.floor(end.getTime() / 1000) };
|
||||||
|
}
|
||||||
|
case 'yesterday': {
|
||||||
|
const yd = new Date(now);
|
||||||
|
yd.setDate(yd.getDate() - 1);
|
||||||
|
const start = new Date(yd.getFullYear(), yd.getMonth(), yd.getDate(), 0, 0, 0);
|
||||||
|
const end = new Date(yd.getFullYear(), yd.getMonth(), yd.getDate(), 23, 59, 59, 999);
|
||||||
|
return { startDate: Math.floor(start.getTime() / 1000), endDate: Math.floor(end.getTime() / 1000) };
|
||||||
|
}
|
||||||
|
case 'last7': {
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||||
|
return { startDate: Math.floor(start.getTime() / 1000), endDate: Math.floor(end.getTime() / 1000) };
|
||||||
|
}
|
||||||
|
case 'last30': {
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setDate(start.getDate() - 29);
|
||||||
|
start.setHours(0, 0, 0, 0);
|
||||||
|
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
|
||||||
|
return { startDate: Math.floor(start.getTime() / 1000), endDate: Math.floor(end.getTime() / 1000) };
|
||||||
|
}
|
||||||
|
case 'all':
|
||||||
|
// Use a minimal unix start date to represent "all time" without relying on server defaults.
|
||||||
|
return { startDate: 1 };
|
||||||
|
case 'custom': {
|
||||||
|
if (!customDateStart || !customDateEnd) return {};
|
||||||
|
const start = new Date(customDateStart + 'T00:00:00');
|
||||||
|
const end = new Date(customDateEnd + 'T23:59:59');
|
||||||
|
return { startDate: Math.floor(start.getTime() / 1000), endDate: Math.floor(end.getTime() / 1000) };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateForInput(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const d = String(date.getDate()).padStart(2, '0');
|
||||||
|
return y + '-' + m + '-' + d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatsTotalLabel() {
|
||||||
|
const label = document.getElementById('stat-total-label');
|
||||||
|
if (!label) return;
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
today: 'Total Today',
|
||||||
|
yesterday: 'Total Yesterday',
|
||||||
|
last7: 'Total (7 Days)',
|
||||||
|
last30: 'Total (30 Days)',
|
||||||
|
all: 'Total (All Time)',
|
||||||
|
custom: 'Total (Custom)'
|
||||||
|
};
|
||||||
|
label.textContent = labels[currentDateFilter] || 'Total';
|
||||||
|
}
|
||||||
|
|
||||||
function refreshOrders() {
|
function refreshOrders() {
|
||||||
const syncButton = document.getElementById('syncButton');
|
const syncButton = document.getElementById('syncButton');
|
||||||
const syncText = syncButton ? syncButton.querySelector('.sync-text') : null;
|
const syncText = syncButton ? syncButton.querySelector('.sync-text') : null;
|
||||||
@@ -161,8 +281,15 @@ function refreshOrders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusParam = currentFilter === 'all' ? '' : currentFilter;
|
const params = new URLSearchParams();
|
||||||
const url = '/api/orders' + (statusParam ? '?status=' + statusParam : '');
|
if (currentFilter !== 'all') {
|
||||||
|
params.set('status', currentFilter);
|
||||||
|
}
|
||||||
|
const dateRange = getDateRangeParams();
|
||||||
|
if (dateRange.startDate) params.set('startDate', dateRange.startDate);
|
||||||
|
if (dateRange.endDate) params.set('endDate', dateRange.endDate);
|
||||||
|
const qs = params.toString();
|
||||||
|
const url = '/api/orders' + (qs ? '?' + qs : '');
|
||||||
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|||||||
@@ -36,12 +36,39 @@ async function ordersRoutes(fastify, options) {
|
|||||||
limit: parseInt(req.query.limit, 10) || 50
|
limit: parseInt(req.query.limit, 10) || 50
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get today's date for stats
|
const normalizeUnixSeconds = (v) => {
|
||||||
const today = new Date();
|
const n = Number(v);
|
||||||
filters.date = today;
|
if (!Number.isFinite(n) || n <= 0) return 0;
|
||||||
|
// Accept both seconds and milliseconds.
|
||||||
|
return n > 1e12 ? Math.floor(n / 1000) : Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
let startDate = normalizeUnixSeconds(req.query.startDate);
|
||||||
|
let endDate = normalizeUnixSeconds(req.query.endDate);
|
||||||
|
|
||||||
|
// Be resilient to inverted ranges
|
||||||
|
if (startDate > 0 && endDate > 0 && startDate > endDate) {
|
||||||
|
const tmp = startDate;
|
||||||
|
startDate = endDate;
|
||||||
|
endDate = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate > 0 && endDate > 0) {
|
||||||
|
filters.startDate = startDate;
|
||||||
|
filters.endDate = endDate;
|
||||||
|
} else if (startDate > 0) {
|
||||||
|
filters.startDate = startDate;
|
||||||
|
} else if (endDate > 0) {
|
||||||
|
filters.endDate = endDate;
|
||||||
|
} else {
|
||||||
|
filters.date = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
const orders = database.getOrders(filters);
|
const orders = database.getOrders(filters);
|
||||||
const stats = database.getOrderStats();
|
const stats = database.getOrderStats(
|
||||||
|
filters.startDate || null,
|
||||||
|
filters.endDate || null
|
||||||
|
);
|
||||||
|
|
||||||
return { error: false, orders, stats };
|
return { error: false, orders, stats };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<% if (showStats) { %>
|
<% if (showStats) { %>
|
||||||
<div class="stats-bar" id="statsBar">
|
<div class="stats-bar" id="statsBar">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Total Today</div>
|
<div class="stat-label" id="stat-total-label">Total Today</div>
|
||||||
<div class="stat-value" id="stat-total"><%= stats.total %></div>
|
<div class="stat-value" id="stat-total"><%= stats.total %></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card stat-new">
|
<div class="stat-card stat-new">
|
||||||
@@ -62,6 +62,22 @@
|
|||||||
<button class="filter-btn" data-filter="preparing">Preparing</button>
|
<button class="filter-btn" data-filter="preparing">Preparing</button>
|
||||||
<button class="filter-btn" data-filter="ready">Ready</button>
|
<button class="filter-btn" data-filter="ready">Ready</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="date-filter-group">
|
||||||
|
<select class="date-filter-select" id="dateFilterSelect">
|
||||||
|
<option value="today" selected>Today</option>
|
||||||
|
<option value="yesterday">Yesterday</option>
|
||||||
|
<option value="last7">Last 7 Days</option>
|
||||||
|
<option value="last30">Last 30 Days</option>
|
||||||
|
<option value="all">All Time</option>
|
||||||
|
<option value="custom">Custom Range...</option>
|
||||||
|
</select>
|
||||||
|
<div class="date-filter-custom" id="dateFilterCustom" style="display:none;">
|
||||||
|
<input type="date" id="dateFilterFrom" class="date-filter-input" title="Start date">
|
||||||
|
<span class="date-filter-separator">to</span>
|
||||||
|
<input type="date" id="dateFilterTo" class="date-filter-input" title="End date">
|
||||||
|
<button class="btn btn-primary btn-sm date-filter-apply" id="dateFilterApply">Apply</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-secondary sync-button" id="syncButton" onclick="manualSync()" title="Check for new orders now">
|
<button class="btn btn-secondary sync-button" id="syncButton" onclick="manualSync()" title="Check for new orders now">
|
||||||
<span class="sync-spinner"></span>
|
<span class="sync-spinner"></span>
|
||||||
<span class="sync-text">Sync Now</span>
|
<span class="sync-text">Sync Now</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user