UX fixes: fix Map tab crash, remove Edit/Delete buttons, update Help dialog and menu labels
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# 🔄 Rotating Backup — keep N copies, replace oldest
|
||||
#
|
||||
# Usage:
|
||||
# ./rotate-backup.sh [--keep N] [--dir DIR] <path/to/database.db>
|
||||
#
|
||||
# Examples:
|
||||
# ./rotate-backup.sh /home/oplabs/projects/canteen-asset-tracker/assets.db
|
||||
# ./rotate-backup.sh --keep 5 --dir /backups/dbs /var/lib/app/data.db
|
||||
#
|
||||
# Default: keep 3 backups in /home/oplabs/backups/canteen-db/
|
||||
set -euo pipefail
|
||||
|
||||
KEEP=3
|
||||
BACKUP_DIR="/home/oplabs/backups/canteen-db"
|
||||
PREFIX="assets.db"
|
||||
|
||||
# ── Parse args ────────────────────────────────────────────────────────────────
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--keep) shift; KEEP="$1"; shift ;;
|
||||
--dir) shift; BACKUP_DIR="$1"; shift ;;
|
||||
--prefix) shift; PREFIX="$1"; shift ;;
|
||||
-h|--help)
|
||||
echo "Usage: $(basename "$0") [--keep N] [--dir DIR] [--prefix NAME] <database-path>"
|
||||
echo ""
|
||||
echo "Backs up a SQLite database with rotation (keep N, remove oldest)."
|
||||
echo ""
|
||||
echo " --keep N Number of backups to retain (default: 3)"
|
||||
echo " --dir DIR Target backup directory (default: /home/oplabs/backups/canteen-db)"
|
||||
echo " --prefix NAME Backup file name prefix (default: assets.db)"
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
DB_PATH="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${DB_PATH:-}" ]]; then
|
||||
echo "❌ Usage: $(basename "$0") [--keep N] <database-path>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$DB_PATH" ]]; then
|
||||
echo "❌ Database not found: $DB_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Create backup ─────────────────────────────────────────────────────────────
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_FILE="${BACKUP_DIR}/${PREFIX}-${TIMESTAMP}"
|
||||
|
||||
# Use sqlite3 .backup for safe online backup (consistent snapshot even if app is running)
|
||||
if command -v sqlite3 &>/dev/null; then
|
||||
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
|
||||
echo "✅ $(basename "$DB_PATH") → $BACKUP_FILE (sqlite3 snapshot)"
|
||||
else
|
||||
cp "$DB_PATH" "$BACKUP_FILE"
|
||||
echo "✅ $(basename "$DB_PATH") → $BACKUP_FILE (file copy)"
|
||||
fi
|
||||
|
||||
# ── Rotate: keep only N most recent ───────────────────────────────────────────
|
||||
KEEP=$((KEEP + 0)) # ensure numeric
|
||||
REMOVE_COUNT=$((KEEP))
|
||||
REMOVE_FROM=$((REMOVE_COUNT + 1))
|
||||
|
||||
OLD_COUNT=$(ls -t "${BACKUP_DIR}/${PREFIX}-"* 2>/dev/null | wc -l)
|
||||
ls -t "${BACKUP_DIR}/${PREFIX}-"* 2>/dev/null | tail -n +${REMOVE_FROM} | xargs rm -f 2>/dev/null || true
|
||||
REMOVED=$((OLD_COUNT > KEEP ? OLD_COUNT - KEEP : 0))
|
||||
|
||||
echo " 📦 ${KEEP} kept · ${REMOVED} removed · ${BACKUP_DIR}"
|
||||
@@ -42,8 +42,8 @@ if [ -f "$DB_PATH" ]; then
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
cp "$DB_PATH" "${BACKUP_DIR}/assets.db-${TIMESTAMP}"
|
||||
# Keep last 30 backups
|
||||
ls -t "${BACKUP_DIR}/assets.db-"* 2>/dev/null | tail -n +31 | xargs rm -f 2>/dev/null || true
|
||||
# Keep last 3 backups (rotate: oldest gets replaced)
|
||||
ls -t "${BACKUP_DIR}/assets.db-"* 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
|
||||
echo " Backup: ${BACKUP_DIR}/assets.db-${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
|
||||
+7
-171
@@ -1061,7 +1061,7 @@
|
||||
</div>
|
||||
<nav class="drawer-nav">
|
||||
<button class="dn-item active" data-tab="tabAddAsset" onclick="navFromDrawer('tabAddAsset')">
|
||||
<span class="dn-icon">📷</span> Add / Find Asset
|
||||
<span class="dn-icon">📷</span> Find Asset
|
||||
</button>
|
||||
<button class="dn-item" data-tab="tabAssets" onclick="navFromDrawer('tabAssets')">
|
||||
<span class="dn-icon">📦</span> Assets
|
||||
@@ -1330,40 +1330,7 @@
|
||||
<button class="btn btn-outline btn-sm" id="detailNavigateBtn" onclick="navigateToAsset()" style="flex:1;min-width:100px;display:none;">🧭 Navigate</button>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
||||
<button class="btn btn-outline btn-sm" onclick="editAsset()" style="flex:1;">✏️ Edit</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="deleteAsset()" style="flex:1;">🗑️ Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Edit View ────────────────────────────────────────────────────── -->
|
||||
<div id="assetsEditView" style="display:none;">
|
||||
<div class="detail-header">
|
||||
<button class="back-btn" onclick="showAssetList()" title="Go back to asset list">←</button>
|
||||
<div style="font-size:18px;font-weight:700;">Edit Asset</div>
|
||||
</div>
|
||||
<div class="card" id="editFormCard">
|
||||
<input id="editBarcode" type="text" class="input-field" placeholder="Machine ID *">
|
||||
<input id="editName" type="text" class="input-field" placeholder="Asset name *">
|
||||
<textarea id="editDesc" class="input-field" placeholder="Telemetry ID" rows="2"></textarea>
|
||||
<div class="form-row" style="margin-bottom:8px;">
|
||||
<select id="editCategory" class="input-field">
|
||||
<option value="">Category</option>
|
||||
<option>Furniture</option>
|
||||
<option>Appliances</option>
|
||||
<option>Utensils & Serveware</option>
|
||||
<option>Equipment</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
<select id="editStatus" class="input-field">
|
||||
<option value="active">Active</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
<option value="retired">Retired</option>
|
||||
</select>
|
||||
</div>
|
||||
<select id="editAssignedTo" class="input-field">
|
||||
<option value="">Assigned To (none)</option>
|
||||
</select>
|
||||
<button class="btn btn-primary" onclick="submitEditAsset()">Save Changes</button>
|
||||
<!-- Edit/Delete removed — main app is lookup-only; use admin server -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1685,18 +1652,12 @@
|
||||
</ul>
|
||||
<p style="margin-bottom:10px;"><strong>Navigation</strong></p>
|
||||
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
|
||||
<li>Bottom tabs switch between Assets, Add, Route, and Map views</li>
|
||||
<li>Bottom tabs switch between Find Asset, Assets, Map, Nav, and Route views</li>
|
||||
<li>Use the ☰ menu to access settings, profile, and admin panel</li>
|
||||
</ul>
|
||||
<p style="margin-bottom:10px;"><strong>Offline Mode</strong></p>
|
||||
<p style="margin-bottom:10px;"><strong>Scanning</strong></p>
|
||||
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
|
||||
<li>When disconnected, changes are queued and sync automatically</li>
|
||||
<li>Click the ⬇️ badge to process the queue immediately</li>
|
||||
<li>Barcode scanning and manual entry work offline</li>
|
||||
</ul>
|
||||
<p><strong>Scanning</strong></p>
|
||||
<ul style="margin:0 0 0 20px;padding:0;list-style:disc;">
|
||||
<li>Use the Add tab to scan barcodes or read ConnectID stickers via camera</li>
|
||||
<li>Use the Find Asset tab to scan barcodes or read ConnectID stickers via camera</li>
|
||||
<li>Stickers in <code>XXXXX-XXXXXX</code> format are read automatically via OCR</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -3821,7 +3782,7 @@
|
||||
|
||||
// ── View switching ────────────────────────────────────────────────────
|
||||
function hideAllAssetViews() {
|
||||
['assetsListView','assetsDetailView','assetsEditView'].forEach(id => {
|
||||
['assetsListView','assetsDetailView'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
@@ -3838,11 +3799,6 @@
|
||||
document.getElementById('assetsDetailView').style.display = 'block';
|
||||
}
|
||||
|
||||
function showEditView() {
|
||||
hideAllAssetViews();
|
||||
document.getElementById('assetsEditView').style.display = 'block';
|
||||
}
|
||||
|
||||
// ── Detail view ───────────────────────────────────────────────────────
|
||||
function nvl(v, fallback) { return (v !== null && v !== undefined && v !== '') ? v : fallback; }
|
||||
|
||||
@@ -4500,131 +4456,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── Edit ──────────────────────────────────────────────────────────────
|
||||
async function editAsset() {
|
||||
if (!AppState.currentAssetId) return;
|
||||
try {
|
||||
const a = await api('/api/assets/' + AppState.currentAssetId);
|
||||
showEditView();
|
||||
document.getElementById('editFormCard').innerHTML = renderEditForm(a);
|
||||
// Assigned To user dropdown (users managed via admin app)
|
||||
setTimeout(function() {
|
||||
var sel = document.getElementById('editAssignedTo');
|
||||
if (sel && a.assigned_to) sel.value = a.assigned_to;
|
||||
}, 0);
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEditForm(a) {
|
||||
return `
|
||||
<div class="card-title">Edit Asset #${a.id}</div>
|
||||
<input id="editMachineId" type="text" class="input-field" placeholder="Machine ID *" value="${esc(a.machine_id)}">
|
||||
<input id="editName" type="text" class="input-field" placeholder="Asset name *" value="${esc(a.name)}">
|
||||
<input id="editSerialNumber" type="text" class="input-field" placeholder="Serial number" value="${esc(a.serial_number || '')}">
|
||||
<textarea id="editDesc" class="input-field" placeholder="Telemetry ID" rows="2">${esc(a.description || '')}</textarea>
|
||||
<div class="form-row">
|
||||
<input id="editCategory" type="text" class="input-field" placeholder="Category" value="${esc(a.category || '')}" list="catList">
|
||||
<input id="editMake" type="text" class="input-field" placeholder="Make" value="${esc(a.make || '')}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="editModel" type="text" class="input-field" placeholder="Model" value="${esc(a.model || '')}">
|
||||
<select id="editStatus" class="input-field">
|
||||
<option value="active" ${a.status === 'active' ? 'selected' : ''}>Active</option>
|
||||
<option value="maintenance" ${a.status === 'maintenance' ? 'selected' : ''}>Maintenance</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
<select id="editAssignedTo" class="input-field">
|
||||
<option value="">Assigned To (none)</option>
|
||||
</select>
|
||||
<div class="card-title" style="margin-top:8px;">📍 Directions</div>
|
||||
<input id="editAddress" type="text" class="input-field" placeholder="Address" value="${esc(a.address || '')}">
|
||||
<div class="form-row">
|
||||
<input id="editBuildingName" type="text" class="input-field" placeholder="Building name" value="${esc(a.building_name || '')}">
|
||||
<input id="editBuildingNumber" type="text" class="input-field" placeholder="Building #" value="${esc(a.building_number || '')}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="editFloor" type="text" class="input-field" placeholder="Floor" value="${esc(a.floor || '')}">
|
||||
<input id="editRoom" type="text" class="input-field" placeholder="Room" value="${esc(a.room || '')}">
|
||||
</div>
|
||||
<input id="editWalkingDirections" type="text" class="input-field" placeholder="Walking directions" value="${esc(a.walking_directions || '')}">
|
||||
<input id="editMapLink" type="text" class="input-field" placeholder="Map link (URL)" value="${esc(a.map_link || '')}">
|
||||
<div class="form-row">
|
||||
<input id="editParking" type="text" class="input-field" placeholder="Parking location" value="${esc(a.parking_location || '')}">
|
||||
<button class="btn btn-outline btn-sm" onclick="fillGpsParking('editParking')" style="flex-shrink:0;">📍 GPS</button>
|
||||
</div>
|
||||
<datalist id="catList">
|
||||
<option value="Coffee"><option value="Cold Beverage"><option value="Snacks">
|
||||
<option value="Cold Food"><option value="Market"><option value="Smart Cooler">
|
||||
</datalist>
|
||||
<button class="btn btn-primary" onclick="submitEditAsset()">Save Changes</button>
|
||||
`;
|
||||
}
|
||||
|
||||
async function submitEditAsset() {
|
||||
if (!AppState.currentAssetId) return;
|
||||
const machine_id = document.getElementById('editMachineId').value.trim();
|
||||
const name = document.getElementById('editName').value.trim();
|
||||
if (!machine_id || !name) {
|
||||
showToast('Machine ID and name are required', true);
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
machine_id, name,
|
||||
serial_number: document.getElementById('editSerialNumber').value.trim() || null,
|
||||
description: document.getElementById('editDesc').value.trim() || null,
|
||||
category: document.getElementById('editCategory').value.trim() || null,
|
||||
make: document.getElementById('editMake').value.trim() || null,
|
||||
model: document.getElementById('editModel').value.trim() || null,
|
||||
status: document.getElementById('editStatus').value,
|
||||
address: document.getElementById('editAddress').value.trim() || null,
|
||||
building_name: document.getElementById('editBuildingName').value.trim() || null,
|
||||
building_number: document.getElementById('editBuildingNumber').value.trim() || null,
|
||||
floor: document.getElementById('editFloor').value.trim() || null,
|
||||
room: document.getElementById('editRoom').value.trim() || null,
|
||||
walking_directions: document.getElementById('editWalkingDirections').value.trim() || null,
|
||||
map_link: document.getElementById('editMapLink').value.trim() || null,
|
||||
parking_location: document.getElementById('editParking').value.trim() || null,
|
||||
assigned_to: document.getElementById('editAssignedTo').value ? parseInt(document.getElementById('editAssignedTo').value) : null,
|
||||
};
|
||||
try {
|
||||
await api('/api/assets/' + AppState.currentAssetId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
showToast('Asset updated!');
|
||||
viewAsset(AppState.currentAssetId);
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Delete ────────────────────────────────────────────────────────────
|
||||
async function deleteAsset() {
|
||||
if (!AppState.currentAssetId) return;
|
||||
const confirmed = await showModal(
|
||||
'Delete Asset',
|
||||
'Delete this asset and all its check-ins? This cannot be undone.',
|
||||
'Delete'
|
||||
);
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
await api('/api/assets/' + AppState.currentAssetId, { method: 'DELETE' });
|
||||
showToast('Asset deleted');
|
||||
showAssetList();
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Map filter state ──────────────────────────────────────────────────
|
||||
let mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
|
||||
customer_id: null, location_id: null, assigned_to: null, branch: null };
|
||||
let mapFilterDropdownsLoaded = false;
|
||||
let _mapFilterTimer;
|
||||
|
||||
function toggleMapFilterPanel() {
|
||||
const panel = document.getElementById('mapFilterPanel');
|
||||
|
||||
Reference in New Issue
Block a user