freealberta/map/app/public/admin.html

856 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Admin Panel - BNKops Map - Interactive canvassing web-app & viewer">
<title>Admin Panel</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="shortcut icon" href="/favicon.ico">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="" />
<!-- Custom CSS -->
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/admin.css">
</head>
<body>
<div id="app">
<!-- Header -->
<header class="header">
<button id="mobile-menu-toggle" class="mobile-menu-toggle">
<span></span>
<span></span>
<span></span>
</button>
<h1>Admin Panel</h1>
<div class="header-actions">
<a href="/" class="btn btn-secondary">← Back to Map</a>
<span id="admin-info" class="admin-info desktop-only"></span>
</div>
</header>
<!-- Main Content -->
<div class="admin-container">
<div class="admin-sidebar" id="admin-sidebar">
<div class="sidebar-header">
<h2>Admin Menu</h2>
<button id="close-sidebar" class="close-sidebar">×</button>
</div>
<nav class="admin-nav">
<a href="#dashboard">
<span class="nav-icon">📊</span>
<span class="nav-text">Dashboard</span>
</a>
<a href="#nocodb-links">
<span class="nav-icon">🗄️</span>
<span class="nav-text">NocoDB Links</span>
</a>
<a href="#start-location" class="active">
<span class="nav-icon">📍</span>
<span class="nav-text">Start Location</span>
</a>
<a href="#walk-sheet">
<span class="nav-icon">📄</span>
<span class="nav-text">Walk Sheet</span>
</a>
<a href="#shifts">
<span class="nav-icon">📅</span>
<span class="nav-text">Shifts</span>
</a>
<a href="#users">
<span class="nav-icon">👥</span>
<span class="nav-text">Users</span>
</a>
<a href="#cuts">
<span class="nav-icon">✂️</span>
<span class="nav-text">Map Cuts</span>
</a>
<a href="#convert-data">
<span class="nav-icon">📊</span>
<span class="nav-text">Convert Data</span>
</a>
</nav>
<div class="sidebar-footer">
<div id="mobile-admin-info" class="mobile-admin-info mobile-only"></div>
</div>
</div>
<div class="admin-content">
<!-- Dashboard Section -->
<section id="dashboard" class="admin-section" style="display: none;">
<h2>Campaign Dashboard</h2>
<p>Overview of campaign metrics and statistics</p>
<div class="dashboard-container">
<!-- Summary Cards -->
<div class="dashboard-cards">
<div class="dashboard-card">
<h3>Total Locations</h3>
<div class="card-value" id="total-locations">-</div>
</div>
<div class="dashboard-card">
<h3>Overall Score</h3>
<div class="card-value" id="overall-score">-</div>
<div class="card-subtitle">out of 4.0</div>
</div>
<div class="dashboard-card">
<h3>Signs Delivered</h3>
<div class="card-value" id="sign-delivered">-</div>
</div>
<div class="dashboard-card">
<h3>Total Users</h3>
<div class="card-value" id="total-users">-</div>
</div>
</div>
<!-- Charts -->
<div class="dashboard-charts">
<div class="chart-container">
<h3>Support Level Distribution</h3>
<canvas id="support-chart"></canvas>
</div>
<div class="chart-container">
<h3>Sign Sizes Requested</h3>
<canvas id="sign-sizes-chart"></canvas>
</div>
<div class="chart-container chart-full-width">
<h3>Daily Entries (Last 30 Days)</h3>
<canvas id="entries-chart"></canvas>
</div>
</div>
</div>
</section>
<!-- NocoDB Links Section -->
<section id="nocodb-links" class="admin-section" style="display: none;">
<h2>NocoDB Database Links</h2>
<p>Quick access to all NocoDB database views and sheets for data management.</p>
<div class="nocodb-info">
<div class="info-box">
<h4>💡 About NocoDB</h4>
<p>
Everything inside Map can be automated through NocoDB and <strong>N8N</strong>, which can be accessed as applications through the Homepage. You can expand the fucntions of Map extensively, including build whole new application functions.
</p>
<p>
NocoDB is the database backend that powers this application. Use these links to directly access and manage your data in the NocoDB interface.<br>
<a href="https://nocodb.com/docs/product-docs" target="_blank" rel="noopener" class="btn btn-link">NocoDB Documentation</a>
<a href="https://docs.n8n.io/" target="_blank" rel="noopener" class="btn btn-link">N8N Documentation</a>
</p>
</div>
</div>
<div class="nocodb-links-container">
<div class="nocodb-cards">
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>📊 Data View</h3>
<span class="nocodb-card-badge">Primary</span>
</div>
<p>Main database view for all location and campaign data</p>
<a href="#" id="admin-nocodb-view-link" class="btn btn-primary" target="_blank">
Open Data View
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>👥 Login Sheet</h3>
<span class="nocodb-card-badge">Users</span>
</div>
<p>Manage user accounts and authentication settings</p>
<a href="#" id="admin-nocodb-login-link" class="btn btn-secondary" target="_blank">
Open Login Sheet
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>⚙️ Settings Sheet</h3>
<span class="nocodb-card-badge">Config</span>
</div>
<p>Configure application settings and preferences</p>
<a href="#" id="admin-nocodb-settings-link" class="btn btn-secondary" target="_blank">
Open Settings Sheet
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>📅 Shifts Sheet</h3>
<span class="nocodb-card-badge">Schedule</span>
</div>
<p>Manage volunteer shifts and scheduling</p>
<a href="#" id="admin-nocodb-shifts-link" class="btn btn-secondary" target="_blank">
Open Shifts Sheet
</a>
</div>
<div class="nocodb-card">
<div class="nocodb-card-header">
<h3>📝 Shift Signups</h3>
<span class="nocodb-card-badge">Volunteers</span>
</div>
<p>View and manage volunteer shift signups</p>
<a href="#" id="admin-nocodb-signups-link" class="btn btn-secondary" target="_blank">
Open Shift Signups
</a>
</div>
</div>
</div>
</section>
<!-- Start Location Section -->
<section id="start-location" class="admin-section">
<h2>Map Start Location</h2>
<p>Set the default center point and zoom level for the map when users first load the application.</p>
<div class="admin-map-container">
<div id="admin-map" class="admin-map"></div>
<div class="location-controls">
<div class="form-group">
<label for="start-lat">Latitude</label>
<input type="number" id="start-lat" step="0.000001" min="-90" max="90">
</div>
<div class="form-group">
<label for="start-lng">Longitude</label>
<input type="number" id="start-lng" step="0.000001" min="-180" max="180">
</div>
<div class="form-group">
<label for="start-zoom">Zoom Level</label>
<input type="number" id="start-zoom" min="2" max="19" step="1">
</div>
<div class="form-actions">
<button id="use-current-view" class="btn btn-secondary">
Use Current Map View
</button>
<button id="save-start-location" class="btn btn-primary">
Save Start Location
</button>
</div>
<div class="help-text">
<p>💡 Tip: Navigate the map to your desired location and zoom level, then click "Use Current Map View" to capture the coordinates.</p>
</div>
</div>
</div>
</section>
<!-- Walk Sheet Section -->
<section id="walk-sheet" class="admin-section" style="display: none;">
<h2>Walk Sheet Configuration</h2>
<p>Design and configure printable walk sheets for door-to-door canvassing.</p>
<div class="walk-sheet-container">
<div class="walk-sheet-config">
<h3>Sheet Information</h3>
<div class="form-group">
<label for="walk-sheet-title">Sheet Title</label>
<input type="text" id="walk-sheet-title" placeholder="Campaign Walk Sheet">
</div>
<div class="form-group">
<label for="walk-sheet-subtitle">Subtitle</label>
<input type="text" id="walk-sheet-subtitle" placeholder="Door-to-Door Canvassing Form">
</div>
<div class="form-group">
<label for="walk-sheet-footer">Footer Text</label>
<textarea id="walk-sheet-footer" rows="3" placeholder="Contact info, legal text, etc."></textarea>
</div>
<h3>QR Codes</h3>
<p class="help-text-inline">Add up to 3 QR codes for quick access to digital resources.</p>
<!-- QR Code 1 -->
<div class="qr-code-group">
<h4>QR Code 1</h4>
<div class="form-row">
<div class="form-group">
<label for="qr-code-1-url">URL</label>
<input type="url" id="qr-code-1-url" placeholder="https://example.com/signup">
</div>
<div class="form-group">
<label for="qr-code-1-label">Label</label>
<input type="text" id="qr-code-1-label" placeholder="Sign Up">
</div>
</div>
</div>
<!-- QR Code 2 -->
<div class="qr-code-group">
<h4>QR Code 2</h4>
<div class="form-row">
<div class="form-group">
<label for="qr-code-2-url">URL</label>
<input type="url" id="qr-code-2-url" placeholder="https://example.com/donate">
</div>
<div class="form-group">
<label for="qr-code-2-label">Label</label>
<input type="text" id="qr-code-2-label" placeholder="Donate">
</div>
</div>
</div>
<!-- QR Code 3 -->
<div class="qr-code-group">
<h4>QR Code 3</h4>
<div class="form-row">
<div class="form-group">
<label for="qr-code-3-url">URL</label>
<input type="url" id="qr-code-3-url" placeholder="https://example.com/volunteer">
</div>
<div class="form-group">
<label for="qr-code-3-label">Label</label>
<input type="text" id="qr-code-3-label" placeholder="Volunteer">
</div>
</div>
</div>
<div class="form-actions">
<button id="save-walk-sheet" class="btn btn-primary">
Save Configuration
</button>
<button id="print-walk-sheet" class="btn btn-secondary">
🖨️ Print Sheet
</button>
</div>
</div>
<div class="walk-sheet-preview">
<h3>Preview</h3>
<div class="preview-controls">
<span class="preview-info">8.5" x 11" format</span>
</div>
<div id="walk-sheet-preview-content" class="walk-sheet-page">
<!-- Preview content will be generated here -->
</div>
</div>
</div>
</section>
<!-- Shifts Section -->
<section id="shifts" class="admin-section" style="display: none;">
<h2>Shift Management</h2>
<p>Create and manage volunteer shifts.</p>
<div class="shifts-admin-container">
<div class="shift-form">
<h3>Create New Shift</h3>
<form id="shift-form">
<div class="form-group">
<label for="shift-title">Title</label>
<input type="text" id="shift-title" required>
</div>
<div class="form-group">
<label for="shift-description">Description</label>
<textarea id="shift-description" rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="shift-date">Date</label>
<input type="date" id="shift-date" required>
</div>
<div class="form-group">
<label for="shift-start">Start Time</label>
<input type="time" id="shift-start" required>
</div>
<div class="form-group">
<label for="shift-end">End Time</label>
<input type="time" id="shift-end" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="shift-location">Location</label>
<input type="text" id="shift-location">
</div>
<div class="form-group">
<label for="shift-max-volunteers">Max Volunteers</label>
<input type="number" id="shift-max-volunteers" min="1" required>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Shift</button>
<button type="button" class="btn btn-secondary" id="clear-shift-form">Clear</button>
</div>
</form>
</div>
<div class="shifts-list">
<h3>Existing Shifts</h3>
<div id="admin-shifts-list">
<!-- Shifts will be loaded here -->
</div>
</div>
</div>
</section>
<!-- Users Section -->
<section id="users" class="admin-section" style="display: none;">
<h2>User Management</h2>
<p>Create and manage user accounts for the application.</p>
<div class="users-admin-container">
<div class="user-form">
<h3>Create New User</h3>
<form id="create-user-form">
<div class="form-group">
<label for="user-email">Email</label>
<input type="email" id="user-email" required>
</div>
<div class="form-group">
<label for="user-name">Name</label>
<input type="text" id="user-name" required>
</div>
<div class="form-group">
<label for="user-password">Password</label>
<input type="password" id="user-password" required>
</div>
<div class="form-group">
<label for="user-type">User Type</label>
<select id="user-type" required>
<option value="user">Regular User</option>
<option value="temp">Temporary User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group" id="expiration-group" style="display: none;">
<label for="user-expire-days">Expires After (days)</label>
<input type="number" id="user-expire-days" min="1" max="365" value="30">
<small class="help-text">Account will auto-delete after this many days</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="user-is-admin">
Is Admin (Legacy - use User Type instead)
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<button type="button" id="clear-user-form" class="btn btn-secondary">Clear</button>
</div>
</form>
</div>
<div class="users-list">
<!-- User table will be dynamically inserted here -->
<p id="users-loading" class="loading-message">Loading users...</p>
</div>
</div>
</section>
<!-- Map Cuts Section -->
<section id="cuts" class="admin-section" style="display: none;">
<h2>Map Cuts</h2>
<p>Create and manage polygon overlays for the map. Cuts can be used to define areas like wards, neighborhoods, or custom regions.</p>
<div class="cuts-container">
<!-- Map and Drawing Controls -->
<div class="cuts-map-section">
<div id="cuts-map" class="admin-map"></div>
<!-- Drawing Toolbar -->
<div id="cut-drawing-toolbar" class="cut-drawing-toolbar">
<div class="toolbar-content">
<div class="vertex-count" id="vertex-count">0</div>
<div class="style-controls">
<div class="color-control">
<label>Color:</label>
<input type="color" id="toolbar-color" value="#3388ff">
</div>
<div class="opacity-control">
<label>Opacity:</label>
<input type="range" id="toolbar-opacity" min="0" max="1" step="0.05" value="0.3">
<span class="opacity-value" id="toolbar-opacity-display">30%</span>
</div>
</div>
<div class="toolbar-buttons">
<button type="button" id="finish-cut-btn" class="primary" disabled>Finish</button>
<button type="button" id="undo-vertex-btn" class="secondary" disabled>Undo</button>
<button type="button" id="clear-vertices-btn" class="secondary" disabled>Clear</button>
<button type="button" id="cancel-cut-btn" class="danger">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Cut Form -->
<div class="cuts-form-section">
<div class="cuts-management-panel">
<div class="panel-header">
<h3 class="panel-title" id="cut-form-title">Cut Properties</h3>
<div class="panel-actions">
<button id="start-drawing-btn" class="btn btn-primary btn-sm">Start Drawing</button>
</div>
</div>
<div class="panel-content">
<form id="cut-form" class="cut-form">
<!-- Hidden fields moved to prevent duplicates -->
<input type="hidden" id="cut-id" name="id">
<input type="hidden" id="cut-geojson" name="geojson">
<input type="hidden" id="cut-bounds" name="bounds">
<div class="form-group">
<label for="cut-name">Name *</label>
<input type="text" id="cut-name" name="name" required>
</div>
<div class="form-group">
<label for="cut-description">Description</label>
<textarea id="cut-description" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="cut-category">Category</label>
<select id="cut-category" name="category">
<option value="Custom">Custom</option>
<option value="Ward">Ward</option>
<option value="Neighborhood">Neighborhood</option>
<option value="District">District</option>
</select>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="cut-public" name="is_public" checked>
<label for="cut-public">Make this cut visible on the public map</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="cut-official" name="is_official">
<label for="cut-official">Mark as official cut</label>
</div>
<div class="form-actions">
<button type="submit" id="save-cut-btn" class="btn btn-success" disabled>Save Cut</button>
<button type="button" id="reset-form-btn" class="btn btn-secondary">Reset</button>
<button type="button" id="cancel-edit-btn" class="btn btn-secondary" style="display: none;">Cancel Edit</button>
</div>
</form>
</div>
</div>
</div>
<!-- Cuts List -->
<div class="cuts-list-section">
<div class="cuts-management-panel">
<div class="panel-header">
<h3 class="panel-title">Existing Cuts</h3>
<div class="panel-actions">
<button id="refresh-cuts-btn" class="btn btn-secondary btn-sm">Refresh</button>
<button id="export-cuts-btn" class="btn btn-secondary btn-sm">Export All</button>
<label for="import-cuts-file" class="btn btn-secondary btn-sm" style="margin: 0;">
Import
<input type="file" id="import-cuts-file" accept=".json" style="display: none;">
</label>
</div>
</div>
<div class="panel-content">
<div class="cuts-filters">
<input type="text" id="cuts-search" placeholder="Search cuts..." class="form-control">
<select id="cuts-category-filter" class="form-control">
<option value="">All Categories</option>
<option value="Custom">Custom</option>
<option value="Ward">Ward</option>
<option value="Neighborhood">Neighborhood</option>
<option value="District">District</option>
</select>
</div>
<div id="cuts-list" class="cuts-list">
<!-- Cuts will be populated here -->
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Convert Data Section -->
<section id="convert-data" class="admin-section" style="display: none;">
<h2>Convert Data</h2>
<p>Upload a CSV file containing addresses to geocode and import into the map.</p>
<div class="data-convert-container">
<div class="upload-section" id="upload-section">
<h3>CSV Upload</h3>
<form id="csv-upload-form">
<div class="upload-area" id="upload-area">
<div class="upload-icon">📁</div>
<p>Drag and drop your CSV file here or click to browse</p>
<input type="file" id="csv-file-input" accept=".csv" style="display: none;">
<button type="button" class="btn btn-primary" id="browse-btn">Choose File</button>
</div>
<div class="file-info" id="file-info" style="display: none;">
<p><strong>Selected file:</strong> <span id="file-name"></span></p>
<p><strong>Size:</strong> <span id="file-size"></span></p>
</div>
<div class="csv-requirements">
<h4>CSV Requirements:</h4>
<div class="requirements-section">
<h5>Required Column:</h5>
<ul>
<li><strong>address</strong> - The street address to geocode (case-insensitive)</li>
</ul>
</div>
<div class="requirements-section">
<h5>Optional Columns (any of these names will work):</h5>
<div class="field-mapping-grid">
<div class="field-group">
<strong>First Name:</strong>
<ul>
<li>first name</li>
<li>firstname</li>
<li>first_name</li>
</ul>
</div>
<div class="field-group">
<strong>Last Name:</strong>
<ul>
<li>last name</li>
<li>lastname</li>
<li>last_name</li>
</ul>
</div>
<div class="field-group">
<strong>Email:</strong>
<ul>
<li>email</li>
</ul>
</div>
<div class="field-group">
<strong>Phone:</strong>
<ul>
<li>phone</li>
</ul>
</div>
<div class="field-group">
<strong>Unit Number:</strong>
<ul>
<li>unit</li>
<li>unit number</li>
<li>unit_number</li>
</ul>
</div>
<div class="field-group">
<strong>Support Level (1-4):</strong>
<ul>
<li>support level</li>
<li>support_level</li>
</ul>
</div>
<div class="field-group">
<strong>Sign (true/false):</strong>
<ul>
<li>sign</li>
</ul>
</div>
<div class="field-group">
<strong>Sign Size (Regular, Large, Unsure)</strong>
<ul>
<li>sign size</li>
<li>sign_size</li>
</ul>
</div>
<div class="field-group">
<strong>Notes:</strong>
<ul>
<li>notes</li>
</ul>
</div>
</div>
</div>
<div class="requirements-section">
<h5>File Specifications:</h5>
<ul>
<li>Maximum file size: 10MB</li>
<li>Column names are case-insensitive</li>
<li>Extra columns will be ignored</li>
</ul>
</div>
<div class="requirements-section">
<h5>Example CSV Format:</h5>
<pre class="csv-example">address,first name,last name,email,phone,support level,notes
"123 Main St, Edmonton, AB",John,Doe,john@example.com,780-555-0123,2,Interested in campaign
"456 Oak Ave, Edmonton, AB",Jane,Smith,jane@example.com,780-555-0456,1,Strong supporter</pre>
<p style="margin-top: 10px;">
<a href="data:text/csv;charset=utf-8,address%2Cfirst%20name%2Clast%20name%2Cemail%2Cphone%2Csupport%20level%2Cnotes%0A%22123%20Main%20St%2C%20Edmonton%2C%20AB%22%2CJohn%2CDoe%2Cjohn%40example.com%2C780-555-0123%2C2%2CInterested%20in%20campaign%0A%22456%20Oak%20Ave%2C%20Edmonton%2C%20AB%22%2CJane%2CSmith%2Cjane%40example.com%2C780-555-0456%2C1%2CStrong%20supporter"
download="sample-import.csv"
class="btn btn-secondary btn-sm">
📄 Download Sample CSV
</a>
</p>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="process-csv-btn" disabled>
Process CSV
</button>
<button type="button" class="btn btn-secondary" id="clear-upload-btn">
Clear
</button>
</div>
</form>
</div>
<div class="processing-section" id="processing-section" style="display: none;">
<h3>Processing Progress</h3>
<div class="progress-bar-container">
<div class="progress-bar">
<div class="progress-bar-fill" id="progress-bar-fill"></div>
</div>
<p class="progress-text"><span id="progress-current">0</span> / <span id="progress-total">0</span> addresses processed</p>
</div>
<div class="processing-status" id="processing-status">
<p id="current-address"></p>
</div>
<div class="results-preview" id="results-preview" style="display: none;">
<h4>Preview Results</h4>
<div class="results-map" id="results-map"></div>
<div class="results-table-container">
<table class="results-table" id="results-table">
<thead>
<tr>
<th>Status</th>
<th>Original Address</th>
<th>Geocoded Address</th>
<th>Coordinates</th>
</tr>
</thead>
<tbody id="results-tbody"></tbody>
</table>
</div>
</div>
<div class="processing-actions" id="processing-actions" style="display: none;">
<button type="button" class="btn btn-success" id="save-results-btn">
Add Data to Map
</button>
<button type="button" class="btn btn-secondary" id="new-upload-btn">
Upload New File
</button>
</div>
</div>
</div>
</section>
</div>
</div>
<!-- Status Messages -->
<div id="status-container" class="status-container"></div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<!-- Custom QR Code Implementation -->
<script>
// Simple QR Code implementation using our server
window.QRCode = {
toCanvas: function(canvas, text, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
const size = options.width || 200;
const qrUrl = `/api/qr?text=${encodeURIComponent(text)}&size=${size}`;
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, size, size);
if (callback) callback(null);
};
img.onerror = function() {
console.error('Failed to load QR code from server');
// Fallback: draw a simple placeholder
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = '#000000';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText('QR Code', size/2, size/2 - 10);
ctx.fillText('(Failed)', size/2, size/2 + 10);
if (callback) callback(new Error('Failed to load QR code'));
};
img.src = qrUrl;
}
};
console.log('Local QR Code implementation loaded');
</script>
<!-- Cache Management -->
<script src="js/cache-manager.js"></script>
<!-- Chart.js library -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- Dashboard JavaScript -->
<script src="js/dashboard.js"></script>
<!-- Admin Cuts JavaScript -->
<script src="js/admin-cuts.js"></script>
<!-- Data Convert JavaScript -->
<!-- Admin JavaScript -->
<script src="js/admin.js"></script>
<script src="js/data-convert.js"></script>
</body>
</html>