feat: Route tab overhaul + work order filters + Next Stop + Google Maps
- Added work order filter bar to Route tab matching the Work Orders tab UX: status, priority, work type dropdowns + date range selector - Filter params passed to /api/workorders/today endpoint (backend updated with new query params: status, priority, work_type, date_range) - Added Google Maps Navigate button on every route stop - Added Mark Done & Next Stop button — tracks current stop index, marks completed stops with green check, advances to next with auto-prompt to open Google Maps navigation - Animated stop progress: done stops dimmed with green ✓, current stop highlighted with accent border - Updated /api/workorders/today to support date_range (week/month/all) and additional filter WHERE clauses
This commit is contained in:
@@ -3311,11 +3311,16 @@ async def workorders_lookup(body: WorkorderLookupRequest):
|
|||||||
async def workorders_today(
|
async def workorders_today(
|
||||||
tech: list[str] = Query([], description="Filter by technician names"),
|
tech: list[str] = Query([], description="Filter by technician names"),
|
||||||
limit: int = Query(100, ge=1, le=500),
|
limit: int = Query(100, ge=1, le=500),
|
||||||
|
status: str = Query("", description="Filter by work order status"),
|
||||||
|
priority: str = Query("", description="Filter by priority (Urgent/High/Medium/Low)"),
|
||||||
|
work_type: str = Query("", description="Filter by work type (Install/Repair/PM/Emergency)"),
|
||||||
|
date_range: str = Query("today", description="Date range: today, week, month, all"),
|
||||||
):
|
):
|
||||||
"""Fetch today's active work orders via bookableresourcebooking.
|
"""Fetch today's active work orders via bookableresourcebooking.
|
||||||
|
|
||||||
Pass ?tech=Shawn+Canada&tech=John+Doe to filter by one or more technicians.
|
Pass ?tech=Shawn+Canada&tech=John+Doe to filter by one or more technicians.
|
||||||
Omit tech entirely to return all technicians.
|
Omit tech entirely to return all technicians.
|
||||||
|
Supports optional ?status=, ?priority=, ?work_type=, ?date_range= filters.
|
||||||
"""
|
"""
|
||||||
conn = _get_extraction_db()
|
conn = _get_extraction_db()
|
||||||
if not conn:
|
if not conn:
|
||||||
@@ -3323,20 +3328,47 @@ async def workorders_today(
|
|||||||
try:
|
try:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
today = date.today().isoformat()
|
today = date.today().isoformat()
|
||||||
today_start = f"{today}T00:00:00Z"
|
|
||||||
today_end = f"{today}T23:59:59Z"
|
# Determine date window
|
||||||
|
date_window_start = f"{today}T00:00:00Z"
|
||||||
|
date_window_end = f"{today}T23:59:59Z"
|
||||||
|
if date_range == "week":
|
||||||
|
from datetime import timedelta
|
||||||
|
week_ago = (date.today() - timedelta(days=7)).isoformat()
|
||||||
|
date_window_start = f"{week_ago}T00:00:00Z"
|
||||||
|
elif date_range == "month":
|
||||||
|
from datetime import timedelta
|
||||||
|
month_ago = (date.today() - timedelta(days=30)).isoformat()
|
||||||
|
date_window_start = f"{month_ago}T00:00:00Z"
|
||||||
|
elif date_range == "all":
|
||||||
|
date_window_start = "2000-01-01T00:00:00Z"
|
||||||
|
date_window_end = "2099-12-31T23:59:59Z"
|
||||||
|
|
||||||
active_booking_statuses = ",".join(
|
active_booking_statuses = ",".join(
|
||||||
f"'{s}'"
|
f"'{s}'"
|
||||||
for s in ["Scheduled", "Unscheduled", "On Site", "Traveling", "On Break", "Hold", "Committed"]
|
for s in ["Scheduled", "Unscheduled", "On Site", "Traveling", "On Break", "Hold", "Committed"]
|
||||||
)
|
)
|
||||||
|
|
||||||
params = [today_start, today_end]
|
params = [date_window_start, date_window_end]
|
||||||
tech_clause = ""
|
tech_clause = ""
|
||||||
if tech:
|
if tech:
|
||||||
placeholders_t = ",".join(["?" for _ in tech])
|
placeholders_t = ",".join(["?" for _ in tech])
|
||||||
tech_clause = f'AND b."resource!name" IN ({placeholders_t})'
|
tech_clause = f'AND b."resource!name" IN ({placeholders_t})'
|
||||||
params.extend(tech)
|
params.extend(tech)
|
||||||
|
|
||||||
|
# Additional filters
|
||||||
|
filter_clauses = []
|
||||||
|
if status:
|
||||||
|
filter_clauses.append("w.msdyn_systemstatus = ?")
|
||||||
|
params.append(status)
|
||||||
|
if priority:
|
||||||
|
filter_clauses.append('w."msdyn_priority!name" = ?')
|
||||||
|
params.append(priority)
|
||||||
|
if work_type:
|
||||||
|
filter_clauses.append('w."msdyn_workordertype!name" = ?')
|
||||||
|
params.append(work_type)
|
||||||
|
|
||||||
|
filter_sql = " ".join(filter_clauses)
|
||||||
params.append(limit)
|
params.append(limit)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
@@ -3365,6 +3397,7 @@ async def workorders_today(
|
|||||||
AND b.starttime <= ?
|
AND b.starttime <= ?
|
||||||
AND b."bookingstatus!name" IN ({active_booking_statuses})
|
AND b."bookingstatus!name" IN ({active_booking_statuses})
|
||||||
{tech_clause}
|
{tech_clause}
|
||||||
|
{filter_sql}
|
||||||
ORDER BY b.starttime ASC
|
ORDER BY b.starttime ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
""",
|
""",
|
||||||
|
|||||||
+185
-9
@@ -1068,6 +1068,12 @@
|
|||||||
border-radius: 6px; padding: 10px 12px; margin-bottom: 6px;
|
border-radius: 6px; padding: 10px 12px; margin-bottom: 6px;
|
||||||
display: flex; align-items: flex-start; gap: 10px;
|
display: flex; align-items: flex-start; gap: 10px;
|
||||||
}
|
}
|
||||||
|
.stop-item.stop-done { opacity: 0.5; }
|
||||||
|
.stop-item.stop-done .stop-num { background: var(--green,#3fb950) !important; }
|
||||||
|
.stop-item.stop-current {
|
||||||
|
background: var(--accent-bg,#1a2744); border-color: var(--accent,#58a6ff);
|
||||||
|
border-left: 3px solid var(--accent,#58a6ff);
|
||||||
|
}
|
||||||
.stop-num {
|
.stop-num {
|
||||||
background: var(--accent, #58a6ff); color: #fff; width: 26px; height: 26px;
|
background: var(--accent, #58a6ff); color: #fff; width: 26px; height: 26px;
|
||||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
@@ -1663,12 +1669,69 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter bar (matching WO tab style) -->
|
||||||
|
<div style="text-align:left;margin-bottom:10px;">
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:6px;">
|
||||||
|
<button class="filter-toggle" id="rpFilterToggle" onclick="rpToggleFilterPanel()" title="Toggle filter panel" style="font-size:12px;padding:4px 10px;">
|
||||||
|
<svg class="gi" aria-hidden="true"><use href="#gi-target"/></svg> Filters <span id="rpFilterCount" class="ft-count" style="display:none;">0</span>
|
||||||
|
</button>
|
||||||
|
<span style="font-size:11px;color:var(--text2,#999);">Apply filters before loading route</span>
|
||||||
|
</div>
|
||||||
|
<div class="filter-panel" id="rpFilterPanel" style="display:none;">
|
||||||
|
<div class="fp-row">
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="fp-label">Status</div>
|
||||||
|
<select id="rpFilterStatus" class="input-field" onchange="rpOnFilterChange()">
|
||||||
|
<option value="">All statuses</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="fp-label">Priority</div>
|
||||||
|
<select id="rpFilterPriority" class="input-field" onchange="rpOnFilterChange()">
|
||||||
|
<option value="">All priorities</option>
|
||||||
|
<option value="Urgent">🔴 Urgent</option>
|
||||||
|
<option value="High">🟠 High</option>
|
||||||
|
<option value="Medium">🟡 Medium</option>
|
||||||
|
<option value="Low">🟢 Low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-row">
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="fp-label">Work Type</div>
|
||||||
|
<select id="rpFilterType" class="input-field" onchange="rpOnFilterChange()">
|
||||||
|
<option value="">All types</option>
|
||||||
|
<option value="Install">🔧 Install</option>
|
||||||
|
<option value="Repair">🔧 Repair</option>
|
||||||
|
<option value="PM">📋 PM</option>
|
||||||
|
<option value="Emergency">🚨 Emergency</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="fp-label">Date Range</div>
|
||||||
|
<select id="rpFilterDate" class="input-field" onchange="rpOnFilterChange()">
|
||||||
|
<option value="today">Today</option>
|
||||||
|
<option value="week">This Week</option>
|
||||||
|
<option value="month">This Month</option>
|
||||||
|
<option value="all">🌐 All</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fp-actions">
|
||||||
|
<button class="btn btn-outline btn-xs" onclick="rpClearFilters()" style="flex:1;"><svg class="gi" aria-hidden="true"><use href="#gi-cross"/></svg> Clear filters</button>
|
||||||
|
<button class="btn btn-outline btn-xs" onclick="rpToggleFilterPanel()" style="flex:1;"><svg class="gi" aria-hidden="true"><use href="#gi-check"/></svg> Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="active-filters" id="rpActiveFilters"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-primary" style="font-size:15px;padding:12px 24px;flex:1;" onclick="rpLoadToday()">
|
<button class="btn btn-primary" style="font-size:15px;padding:12px 24px;flex:1;" onclick="rpLoadToday()">
|
||||||
<svg class="gi" aria-hidden="true"><use href="#gi-rocket"/></svg> Load Today's Route
|
<svg class="gi" aria-hidden="true"><use href="#gi-rocket"/></svg> Load Route
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size:11px;color:var(--text2,#999);margin-top:6px;">Finds today's scheduled/active work orders and plans the optimal route</p>
|
<p style="font-size:11px;color:var(--text2,#999);margin-top:6px;">Finds open/scheduled work orders matching filters and plans the optimal route</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Starting Point -->
|
<!-- Starting Point -->
|
||||||
@@ -6025,8 +6088,9 @@
|
|||||||
map: null,
|
map: null,
|
||||||
markers: [],
|
markers: [],
|
||||||
polyline: null,
|
polyline: null,
|
||||||
searchTimer: null,
|
|
||||||
isReordering: false,
|
isReordering: false,
|
||||||
|
searchTimer: null,
|
||||||
|
currentStopIndex: 0, // which stop is currently active
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Init on tab visit ────────────────────────────────────────────────
|
// ── Init on tab visit ────────────────────────────────────────────────
|
||||||
@@ -6207,19 +6271,27 @@
|
|||||||
document.querySelectorAll('#rpTechCheckboxes input[type="checkbox"]:not(#rpTechAll):checked').forEach(cb => techs.push(cb.value));
|
document.querySelectorAll('#rpTechCheckboxes input[type="checkbox"]:not(#rpTechAll):checked').forEach(cb => techs.push(cb.value));
|
||||||
if (techs.length === 0) { showToast('Select at least one technician', true); return; }
|
if (techs.length === 0) { showToast('Select at least one technician', true); return; }
|
||||||
|
|
||||||
rpShowLoading('Loading today\u2019s work orders...');
|
rpShowLoading('Loading work orders...');
|
||||||
try {
|
try {
|
||||||
|
const filters = rpGetFilters();
|
||||||
const params = techs.map(t => 'tech=' + encodeURIComponent(t)).join('&');
|
const params = techs.map(t => 'tech=' + encodeURIComponent(t)).join('&');
|
||||||
|
// Add filter params
|
||||||
|
if (filters.status) params += '&status=' + encodeURIComponent(filters.status);
|
||||||
|
if (filters.priority) params += '&priority=' + encodeURIComponent(filters.priority);
|
||||||
|
if (filters.work_type) params += '&work_type=' + encodeURIComponent(filters.work_type);
|
||||||
|
if (filters.date_range) params += '&date_range=' + encodeURIComponent(filters.date_range);
|
||||||
|
|
||||||
const data = await api('/api/workorders/today?' + params);
|
const data = await api('/api/workorders/today?' + params);
|
||||||
if (!data.workorders || data.workorders.length === 0) {
|
if (!data.workorders || data.workorders.length === 0) {
|
||||||
rpHideLoading();
|
rpHideLoading();
|
||||||
showToast('No work orders found for today', true); return;
|
showToast('No work orders found matching filters', true); return;
|
||||||
}
|
}
|
||||||
const woIds = data.workorders.map(wo => wo.name);
|
const woIds = data.workorders.map(wo => wo.name);
|
||||||
|
rpState.currentStopIndex = 0;
|
||||||
await rpOptimizeRoute(woIds, data.workorders);
|
await rpOptimizeRoute(woIds, data.workorders);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rpHideLoading();
|
rpHideLoading();
|
||||||
showToast('Error loading today\'s route: ' + e.message, true);
|
showToast('Error loading route: ' + e.message, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6350,20 +6422,31 @@
|
|||||||
|
|
||||||
// Stop list
|
// Stop list
|
||||||
let stopHtml = '';
|
let stopHtml = '';
|
||||||
|
let hasReachedCurrent = false;
|
||||||
route.forEach((stop, i) => {
|
route.forEach((stop, i) => {
|
||||||
const addr = stop.address ? [stop.address.line1, stop.address.city].filter(Boolean).join(', ') : '';
|
const addr = stop.address ? [stop.address.line1, stop.address.city].filter(Boolean).join(', ') : '';
|
||||||
const drive = stop.drive_min_to_next ? '🚗 ' + stop.drive_min_to_next + ' min (' + stop.distance_to_next_km + ' km)' : '';
|
const drive = stop.drive_min_to_next ? '🚗 ' + stop.drive_min_to_next + ' min (' + stop.distance_to_next_km + ' km)' : '';
|
||||||
const cum = stop.cumulative_min != null ? '⏱ ' + stop.cumulative_min + ' min from start' : '';
|
const cum = stop.cumulative_min != null ? '⏱ ' + stop.cumulative_min + ' min from start' : '';
|
||||||
const statusBadge = stop.booking_status ? '<span class="badge badge-info">' + esc(stop.booking_status) + '</span>' : '';
|
const statusBadge = stop.booking_status ? '<span class="badge badge-info">' + esc(stop.booking_status) + '</span>' : '';
|
||||||
const woLink = '<span style="color:var(--accent,#58a6ff);cursor:pointer;" onclick="rpShowAsset(\'' + stop.name + '\')">' + esc(stop.name) + '</span>';
|
const woLink = '<span style="color:var(--accent,#58a6ff);cursor:pointer;" onclick="rpShowAsset(\'' + stop.name + '\')">' + esc(stop.name) + '</span>';
|
||||||
stopHtml += '<div class="stop-item">' +
|
const isCurrent = i === rpState.currentStopIndex;
|
||||||
'<div class="stop-num">' + (i + 1) + '</div>' +
|
if (isCurrent) hasReachedCurrent = true;
|
||||||
|
const gps = stop.gps;
|
||||||
|
const mapsUrl = gps ? 'https://www.google.com/maps/dir/?api=1&destination=' + gps.lat + ',' + gps.lng : '';
|
||||||
|
const isDone = i < rpState.currentStopIndex;
|
||||||
|
stopHtml += '<div class="stop-item' + (isCurrent ? ' stop-current' : '') + (isDone ? ' stop-done' : '') + '">' +
|
||||||
|
'<div class="stop-num" style="' + (isDone ? 'background:var(--green,#3fb950);' : '') + (isCurrent ? 'background:var(--accent,#58a6ff);box-shadow:0 0 0 3px var(--accent-bg,#1a2744);' : '') + '">' +
|
||||||
|
(isDone ? '✓' : (i + 1)) + '</div>' +
|
||||||
'<div class="stop-body">' +
|
'<div class="stop-body">' +
|
||||||
'<div class="stop-name">' + woLink + ' ' + statusBadge + '</div>' +
|
'<div class="stop-name">' + woLink + ' ' + statusBadge + '</div>' +
|
||||||
'<div class="stop-addr">' + esc(stop.account_name || '') + (addr ? ' — ' + esc(addr) : '') + '</div>' +
|
'<div class="stop-addr">' + esc(stop.account_name || '') + (addr ? ' — ' + esc(addr) : '') + '</div>' +
|
||||||
'<div class="stop-meta">' + esc(stop.work_type || '') + (stop.priority ? ' · ' + esc(stop.priority) : '') + '</div>' +
|
'<div class="stop-meta">' + esc(stop.work_type || '') + (stop.priority ? ' · ' + esc(stop.priority) : '') + (stop.booking_status ? ' · ' + esc(stop.booking_status) : '') + '</div>' +
|
||||||
(drive ? '<div class="stop-drive">' + drive + '</div>' : '') +
|
(drive ? '<div class="stop-drive">' + drive + '</div>' : '') +
|
||||||
(cum ? '<div class="stop-cumulative">' + cum + '</div>' : '') +
|
(cum ? '<div class="stop-cumulative">' + cum + '</div>' : '') +
|
||||||
|
'<div class="stop-actions" style="display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;">' +
|
||||||
|
(gps ? '<button class="btn btn-secondary btn-xs" onclick="window.open(\'' + mapsUrl + '\',\'_blank\')" title="Open in Google Maps"><svg class="gi" aria-hidden="true"><use href="#gi-compass"/></svg> Navigate</button>' : '') +
|
||||||
|
(isCurrent ? '<button class="btn btn-primary btn-xs" onclick="rpNextStop()"><svg class="gi" aria-hidden="true"><use href="#gi-check"/></svg> Mark Done & Next</button>' : '') +
|
||||||
|
'</div>' +
|
||||||
'</div></div>';
|
'</div></div>';
|
||||||
});
|
});
|
||||||
document.getElementById('rpStopList').innerHTML = stopHtml;
|
document.getElementById('rpStopList').innerHTML = stopHtml;
|
||||||
@@ -6495,6 +6578,99 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Next Stop ──────────────────────────────────────────────────────────
|
||||||
|
function rpNextStop() {
|
||||||
|
const route = rpState.currentRoute?.route || [];
|
||||||
|
if (rpState.currentStopIndex >= route.length - 1) {
|
||||||
|
showToast('✅ All stops completed! Start a new route.', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rpState.currentStopIndex++;
|
||||||
|
rpDisplayRoute(rpState.currentRoute, null);
|
||||||
|
|
||||||
|
// Auto-navigate to next stop via Google Maps
|
||||||
|
const nextStop = route[rpState.currentStopIndex];
|
||||||
|
if (nextStop && nextStop.gps) {
|
||||||
|
const url = 'https://www.google.com/maps/dir/?api=1&destination=' +
|
||||||
|
nextStop.gps.lat + ',' + nextStop.gps.lng +
|
||||||
|
'&travelmode=driving';
|
||||||
|
showToast('→ Next: ' + (nextStop.name || 'Stop ' + (rpState.currentStopIndex + 1)), false);
|
||||||
|
// Offer to navigate
|
||||||
|
setTimeout(() => {
|
||||||
|
if (confirm('Navigate to ' + (nextStop.account_name || nextStop.name) + '?')) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Route Filters ──────────────────────────────────────────────────────
|
||||||
|
function rpToggleFilterPanel() {
|
||||||
|
const panel = document.getElementById('rpFilterPanel');
|
||||||
|
panel.style.display = panel.style.display === 'none' ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpGetFilters() {
|
||||||
|
return {
|
||||||
|
status: document.getElementById('rpFilterStatus').value,
|
||||||
|
priority: document.getElementById('rpFilterPriority').value,
|
||||||
|
work_type: document.getElementById('rpFilterType').value,
|
||||||
|
date_range: document.getElementById('rpFilterDate').value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpOnFilterChange() {
|
||||||
|
const f = rpGetFilters();
|
||||||
|
let count = 0;
|
||||||
|
if (f.status) count++;
|
||||||
|
if (f.priority) count++;
|
||||||
|
if (f.work_type) count++;
|
||||||
|
const badge = document.getElementById('rpFilterCount');
|
||||||
|
if (count > 0) { badge.textContent = count; badge.style.display = ''; }
|
||||||
|
else { badge.style.display = 'none'; }
|
||||||
|
|
||||||
|
// Show active filter chips
|
||||||
|
const chips = [];
|
||||||
|
if (f.status) chips.push('Status: ' + f.status);
|
||||||
|
if (f.priority) chips.push('Priority: ' + f.priority);
|
||||||
|
if (f.work_type) chips.push('Type: ' + f.work_type);
|
||||||
|
const container = document.getElementById('rpActiveFilters');
|
||||||
|
if (chips.length > 0) {
|
||||||
|
container.innerHTML = chips.map(c =>
|
||||||
|
'<span class="filter-chip">' + esc(c) + '</span>'
|
||||||
|
).join('');
|
||||||
|
container.style.display = '';
|
||||||
|
} else {
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpClearFilters() {
|
||||||
|
document.getElementById('rpFilterStatus').value = '';
|
||||||
|
document.getElementById('rpFilterPriority').value = '';
|
||||||
|
document.getElementById('rpFilterType').value = '';
|
||||||
|
document.getElementById('rpFilterDate').value = 'today';
|
||||||
|
rpOnFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rpLoadStatusOptions() {
|
||||||
|
// Load status options from WO tab or use defaults
|
||||||
|
const statusSelect = document.getElementById('rpFilterStatus');
|
||||||
|
if (!statusSelect) return;
|
||||||
|
// Match the WO tab options
|
||||||
|
const statuses = ['Scheduled', 'In Progress', 'Completed', 'Cancelled', 'On Hold'];
|
||||||
|
statuses.forEach(s => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
statusSelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Call on init
|
||||||
|
document.addEventListener('rpTabActivated', () => {
|
||||||
|
rpLoadStatusOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Game-icons SVG helper for dynamic content
|
// Game-icons SVG helper for dynamic content
|
||||||
function gi(name) {
|
function gi(name) {
|
||||||
|
|||||||
Reference in New Issue
Block a user