CI Verification / Lint & Extension Integrity Check (push) Successful in 2m58s
1205 lines
40 KiB
HTML
1205 lines
40 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>🏫 Daycare Attendance & Daily Schedule Analysis</title>
|
||
<!-- Modern Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<!-- Chart.js -->
|
||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
--bg-base: #060B14;
|
||
--bg-surface: rgba(14, 22, 38, 0.75);
|
||
--bg-surface-hover: rgba(22, 34, 58, 0.85);
|
||
--bg-card-subtle: rgba(255, 255, 255, 0.03);
|
||
--border-color: rgba(255, 255, 255, 0.08);
|
||
--border-highlight: rgba(56, 189, 248, 0.3);
|
||
|
||
--text-primary: #F1F5F9;
|
||
--text-secondary: #94A3B8;
|
||
--text-muted: #64748B;
|
||
|
||
--cyan-glow: rgba(56, 189, 248, 0.25);
|
||
--cyan-primary: #38BDF8;
|
||
--indigo-primary: #6366F1;
|
||
--violet-primary: #818CF8;
|
||
--emerald-primary: #34D399;
|
||
--amber-primary: #FBBF24;
|
||
--rose-primary: #FB7185;
|
||
|
||
--radius-sm: 8px;
|
||
--radius-md: 14px;
|
||
--radius-lg: 20px;
|
||
--radius-full: 9999px;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background-color: var(--bg-base);
|
||
color: var(--text-primary);
|
||
line-height: 1.5;
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
background-image:
|
||
radial-gradient(circle at 15% 15%, rgba(56, 189, 248, 0.12) 0%, transparent 40%),
|
||
radial-gradient(circle at 85% 20%, rgba(99, 102, 241, 0.1) 0%, transparent 45%),
|
||
radial-gradient(circle at 50% 85%, rgba(52, 211, 153, 0.06) 0%, transparent 50%);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
padding: 2.5rem 1.5rem 5rem;
|
||
}
|
||
|
||
/* Header */
|
||
.header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 2.5rem;
|
||
flex-wrap: wrap;
|
||
gap: 1.5rem;
|
||
}
|
||
|
||
.brand-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.35rem 0.85rem;
|
||
background: rgba(56, 189, 248, 0.15);
|
||
border: 1px solid rgba(56, 189, 248, 0.3);
|
||
border-radius: var(--radius-full);
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
color: var(--cyan-primary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.brand-badge span.dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background-color: var(--cyan-primary);
|
||
box-shadow: 0 0 8px var(--cyan-primary);
|
||
}
|
||
|
||
h1.title {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: clamp(2rem, 3.5vw, 2.75rem);
|
||
font-weight: 800;
|
||
letter-spacing: -0.02em;
|
||
background: linear-gradient(135deg, #FFFFFF 30%, #BAE6FD 70%, #38BDF8 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
line-height: 1.15;
|
||
}
|
||
|
||
p.subtitle {
|
||
color: var(--text-secondary);
|
||
font-size: 1.05rem;
|
||
margin-top: 0.4rem;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 0.75rem;
|
||
align-items: center;
|
||
}
|
||
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.65rem 1.15rem;
|
||
border-radius: var(--radius-md);
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
text-decoration: none;
|
||
border: none;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--bg-surface);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--bg-surface-hover);
|
||
border-color: var(--border-highlight);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #0284C7, #0369A1);
|
||
color: #FFFFFF;
|
||
box-shadow: 0 4px 15px var(--cyan-glow);
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
opacity: 0.95;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* Glass Cards */
|
||
.glass-card {
|
||
background: var(--bg-surface);
|
||
backdrop-filter: blur(16px);
|
||
-webkit-backdrop-filter: blur(16px);
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius-lg);
|
||
padding: 1.5rem;
|
||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||
}
|
||
|
||
.glass-card:hover {
|
||
border-color: rgba(255, 255, 255, 0.14);
|
||
}
|
||
|
||
/* KPI Grid */
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 1.25rem;
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
.kpi-card {
|
||
padding: 1.4rem;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.kpi-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px;
|
||
background: linear-gradient(90deg, var(--accent-start, var(--cyan-primary)), var(--accent-end, var(--indigo-primary)));
|
||
border-top-left-radius: var(--radius-lg);
|
||
border-top-right-radius: var(--radius-lg);
|
||
}
|
||
|
||
.kpi-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.kpi-title {
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.04em;
|
||
}
|
||
|
||
.kpi-icon {
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 2.15rem;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
letter-spacing: -0.02em;
|
||
line-height: 1.1;
|
||
margin-bottom: 0.35rem;
|
||
}
|
||
|
||
.kpi-sub {
|
||
font-size: 0.82rem;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* Section Styling */
|
||
.section-title-wrap {
|
||
margin-bottom: 1.25rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.section-title {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 1.45rem;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.6rem;
|
||
}
|
||
|
||
.section-desc {
|
||
color: var(--text-secondary);
|
||
font-size: 0.95rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
/* Insights Narrative Grid */
|
||
.insights-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||
gap: 1.5rem;
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
.insight-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.insight-tag {
|
||
align-self: flex-start;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
padding: 0.25rem 0.65rem;
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: 0.85rem;
|
||
}
|
||
|
||
.tag-rhythm { background: rgba(56, 189, 248, 0.15); color: var(--cyan-primary); }
|
||
.tag-family { background: rgba(129, 140, 248, 0.15); color: var(--violet-primary); }
|
||
.tag-streaks { background: rgba(52, 211, 153, 0.15); color: var(--emerald-primary); }
|
||
.tag-outliers { background: rgba(251, 191, 36, 0.15); color: var(--amber-primary); }
|
||
|
||
.insight-title {
|
||
font-family: 'Outfit', sans-serif;
|
||
font-size: 1.2rem;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
margin-bottom: 0.6rem;
|
||
}
|
||
|
||
.insight-body {
|
||
color: var(--text-secondary);
|
||
font-size: 0.92rem;
|
||
line-height: 1.6;
|
||
flex-grow: 1;
|
||
}
|
||
|
||
.insight-stat-highlight {
|
||
margin-top: 1rem;
|
||
padding: 0.85rem;
|
||
background: var(--bg-card-subtle);
|
||
border-left: 3px solid var(--cyan-primary);
|
||
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
|
||
font-size: 0.88rem;
|
||
color: #E2E8F0;
|
||
}
|
||
|
||
/* Charts Container */
|
||
.chart-box {
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
.chart-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1.25rem;
|
||
flex-wrap: wrap;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.canvas-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 340px;
|
||
}
|
||
|
||
/* Dual Chart Grid */
|
||
.dual-chart-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1.5rem;
|
||
margin-bottom: 2.5rem;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.dual-chart-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* Calendar Heatmap */
|
||
.calendar-card {
|
||
margin-bottom: 2.5rem;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.heatmap-legend {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.78rem;
|
||
color: var(--text-muted);
|
||
margin-top: 1rem;
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.legend-box {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.c-none { background: rgba(255, 255, 255, 0.04); }
|
||
.c-half { background: #FBBF24; }
|
||
.c-std { background: #38BDF8; }
|
||
.c-full { background: #34D399; }
|
||
.c-ext { background: #818CF8; }
|
||
|
||
.heatmap-grid {
|
||
display: grid;
|
||
grid-auto-flow: column;
|
||
grid-template-rows: repeat(7, 14px);
|
||
grid-auto-columns: 14px;
|
||
gap: 4px;
|
||
padding: 0.5rem 0;
|
||
min-width: 780px;
|
||
}
|
||
|
||
.heat-cell {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 3px;
|
||
position: relative;
|
||
cursor: pointer;
|
||
transition: transform 0.1s ease;
|
||
}
|
||
|
||
.heat-cell:hover {
|
||
transform: scale(1.3);
|
||
z-index: 10;
|
||
outline: 1px solid #FFFFFF;
|
||
}
|
||
|
||
/* Interactive Data Table */
|
||
.table-controls {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.search-input {
|
||
background: var(--bg-card-subtle);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--text-primary);
|
||
padding: 0.55rem 1rem;
|
||
border-radius: var(--radius-md);
|
||
font-size: 0.9rem;
|
||
min-width: 260px;
|
||
outline: none;
|
||
}
|
||
|
||
.search-input:focus {
|
||
border-color: var(--cyan-primary);
|
||
box-shadow: 0 0 10px var(--cyan-glow);
|
||
}
|
||
|
||
.filter-tags {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-btn {
|
||
background: transparent;
|
||
border: 1px solid var(--border-color);
|
||
color: var(--text-secondary);
|
||
padding: 0.4rem 0.8rem;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 0.82rem;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.filter-btn.active, .filter-btn:hover {
|
||
background: rgba(56, 189, 248, 0.2);
|
||
border-color: var(--cyan-primary);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.table-wrap {
|
||
overflow-x: auto;
|
||
max-height: 480px;
|
||
overflow-y: auto;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-color);
|
||
}
|
||
|
||
table.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 0.88rem;
|
||
text-align: left;
|
||
}
|
||
|
||
table.data-table th {
|
||
background: #0F172A;
|
||
color: var(--text-secondary);
|
||
font-weight: 600;
|
||
padding: 0.85rem 1rem;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 5;
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
table.data-table td {
|
||
padding: 0.85rem 1rem;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
table.data-table tr:hover td {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
}
|
||
|
||
.duration-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 0.35rem;
|
||
font-weight: 600;
|
||
padding: 0.2rem 0.55rem;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.dur-full { background: rgba(52, 211, 153, 0.15); color: var(--emerald-primary); }
|
||
.dur-std { background: rgba(56, 189, 248, 0.15); color: var(--cyan-primary); }
|
||
.dur-half { background: rgba(251, 191, 36, 0.15); color: var(--amber-primary); }
|
||
|
||
/* Footer */
|
||
.footer {
|
||
text-align: center;
|
||
margin-top: 4rem;
|
||
padding-top: 2rem;
|
||
border-top: 1px solid var(--border-color);
|
||
color: var(--text-muted);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
/* Print styling */
|
||
@media print {
|
||
body {
|
||
background: #FFFFFF !important;
|
||
color: #000000 !important;
|
||
}
|
||
.glass-card {
|
||
border: 1px solid #E2E8F0 !important;
|
||
box-shadow: none !important;
|
||
background: #FFFFFF !important;
|
||
}
|
||
.header-actions, .table-controls {
|
||
display: none !important;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="container">
|
||
|
||
<!-- Top Header -->
|
||
<header class="header">
|
||
<div>
|
||
<div class="brand-badge">
|
||
<span class="dot"></span>
|
||
<span>Preschool & Daycare Attendance Analytics</span>
|
||
</div>
|
||
<h1 class="title">🏫 Attendance & Daily Schedule Analysis</h1>
|
||
<p class="subtitle" id="childHeaderSubtitle">Loading attendance insights...</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="btn btn-secondary" onclick="window.print()">🖨️ Print / Save PDF</button>
|
||
<button class="btn btn-primary" onclick="exportAttendanceCSV()">📥 Export Clean CSV</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Executive KPI Grid -->
|
||
<section class="kpi-grid">
|
||
<div class="glass-card kpi-card" style="--accent-start: #38BDF8; --accent-end: #0284C7;">
|
||
<div class="kpi-header">
|
||
<span class="kpi-title">Total School Time</span>
|
||
<span class="kpi-icon">🏫</span>
|
||
</div>
|
||
<div class="kpi-value" id="kpiTotalHours">-- hrs</div>
|
||
<div class="kpi-sub"><span id="kpiTotalDays">--</span> paired school days</div>
|
||
</div>
|
||
|
||
<div class="glass-card kpi-card" style="--accent-start: #34D399; --accent-end: #10B981;">
|
||
<div class="kpi-header">
|
||
<span class="kpi-title">Average Daily Stay</span>
|
||
<span class="kpi-icon">⏱️</span>
|
||
</div>
|
||
<div class="kpi-value" id="kpiMeanStay">-- hrs</div>
|
||
<div class="kpi-sub">Median: <strong id="kpiMedianStay">--h</strong> (IQR: <span id="kpiIQR">--</span>)</div>
|
||
</div>
|
||
|
||
<div class="glass-card kpi-card" style="--accent-start: #FBBF24; --accent-end: #F59E0B;">
|
||
<div class="kpi-header">
|
||
<span class="kpi-title">Avg Drop-Off Time</span>
|
||
<span class="kpi-icon">🌅</span>
|
||
</div>
|
||
<div class="kpi-value" id="kpiAvgDropoff">--:-- AM</div>
|
||
<div class="kpi-sub">Morning Arrival Anchor</div>
|
||
</div>
|
||
|
||
<div class="glass-card kpi-card" style="--accent-start: #818CF8; --accent-end: #6366F1;">
|
||
<div class="kpi-header">
|
||
<span class="kpi-title">Avg Pick-Up Time</span>
|
||
<span class="kpi-icon">🌇</span>
|
||
</div>
|
||
<div class="kpi-value" id="kpiAvgPickup">--:-- PM</div>
|
||
<div class="kpi-sub">Afternoon Departure Anchor</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Deep-Dive Insights Narrative Cards -->
|
||
<div class="section-title-wrap">
|
||
<h2 class="section-title">🔬 Data-Backed Daily Schedule Insights</h2>
|
||
</div>
|
||
<section class="insights-grid">
|
||
|
||
<!-- Insight 1 -->
|
||
<div class="glass-card insight-card">
|
||
<span class="insight-tag tag-rhythm">⏰ Daily Schedule Rhythm</span>
|
||
<h3 class="insight-title">The 7.6-Hour Core Workday Anchor</h3>
|
||
<p class="insight-body" id="insightBody1">
|
||
<span class="dyn-child-name">Child</span> logged an average of <strong>7.62 hours</strong> (~7h 37m) per day at daycare, arriving at <strong><span id="dynDropoff">9:43 AM</span></strong> and departing at <strong><span id="dynPickup">5:21 PM</span></strong>. Over <strong>88%</strong> of all days strictly followed this standard full-day schedule.
|
||
</p>
|
||
<div class="insight-stat-highlight" id="insightStat1">
|
||
<strong>Daily Std Dev:</strong> Only ±0.79 hours • <strong>Core Window:</strong> 9:30 AM – 5:25 PM
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Insight 2 -->
|
||
<div class="glass-card insight-card">
|
||
<span class="insight-tag tag-family">👨👩👧 Family Partnership</span>
|
||
<h3 class="insight-title">Caregiver Drop-off & Pick-up Split</h3>
|
||
<p class="insight-body" id="insightBody2">
|
||
Handoff records reflect an active shared parenting team:
|
||
<strong><span id="dynTopDropoff">Parent 1</span></strong> handled <span id="dynDropoffPct">77%</span> of morning drop-offs, while pick-ups were actively shared (<span id="dynPickupSplit">66% vs 32%</span>).
|
||
</p>
|
||
<div class="insight-stat-highlight" id="insightStat2">
|
||
<strong>Parent Coverage:</strong> 100% of check-ins/outs signed by primary caregivers.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Insight 3 -->
|
||
<div class="glass-card insight-card">
|
||
<span class="insight-tag tag-streaks">📅 Consistency & Streaks</span>
|
||
<h3 class="insight-title">Longest Attendance Streaks</h3>
|
||
<p class="insight-body">
|
||
Across the 13 months, <span class="dyn-child-name">Child</span> achieved a peak unbroken attendance streak of <strong><span id="dynStreak">24</span> consecutive school days</strong>, with reliable Monday-to-Friday attendance through seasonal transitions.
|
||
</p>
|
||
<div class="insight-stat-highlight" id="insightStat3">
|
||
<strong>Total School Days:</strong> <span id="dynTotalDaysAttended">195</span> full days • <strong>Total Active Days:</strong> <span id="dynTotalActiveDays">236</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Insight 4 -->
|
||
<div class="glass-card insight-card">
|
||
<span class="insight-tag tag-outliers">🏆 Schedule Extremes & Half-Days</span>
|
||
<h3 class="insight-title">Milestones & Special Schedules</h3>
|
||
<p class="insight-body">
|
||
The longest school day recorded was <strong><span id="dynLongestDay">8.63 hours</span></strong>, while the shortest half-day (doctor appointment / early pickup) was <strong><span id="dynShortestDay">1.52 hours</span></strong>.
|
||
</p>
|
||
<div class="insight-stat-highlight" id="insightStat4">
|
||
<strong>Earliest Arrival:</strong> <span id="dynEarliest">9:02 AM</span> • <strong>Latest Departure:</strong> <span id="dynLatest">5:46 PM</span>
|
||
</div>
|
||
</div>
|
||
|
||
</section>
|
||
|
||
<!-- Interactive Daily Duration Timeline Chart -->
|
||
<section class="glass-card chart-box">
|
||
<div class="chart-header">
|
||
<div>
|
||
<h2 class="section-title">📈 Daily Stay Duration Timeline & Trendline</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Hours spent at school each day. Cyan curve shows the 14-day rolling average.</p>
|
||
</div>
|
||
</div>
|
||
<div class="canvas-container">
|
||
<canvas id="timelineChart"></canvas>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Dual Charts: Arrival/Departure Distribution & Parent Split -->
|
||
<section class="dual-chart-grid">
|
||
<!-- Scatter Plot Chart -->
|
||
<div class="glass-card chart-box" style="margin-bottom: 0;">
|
||
<div class="chart-header">
|
||
<div>
|
||
<h2 class="section-title" style="font-size: 1.2rem;">🌅 Drop-off vs. 🌇 Pick-up Times</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Morning arrival vs afternoon pick-up distribution.</p>
|
||
</div>
|
||
</div>
|
||
<div class="canvas-container" style="height: 280px;">
|
||
<canvas id="scatterChart"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Parent Signer Split Chart -->
|
||
<div class="glass-card chart-box" style="margin-bottom: 0;">
|
||
<div class="chart-header">
|
||
<div>
|
||
<h2 class="section-title" style="font-size: 1.2rem;">👨👩👧 Drop-off vs. Pick-up Share</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Signer distribution across the school year.</p>
|
||
</div>
|
||
</div>
|
||
<div class="canvas-container" style="height: 280px;">
|
||
<canvas id="parentSplitChart"></canvas>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Monthly Attendance Hours Bar Chart -->
|
||
<section class="glass-card chart-box" style="margin-top: 2rem;">
|
||
<div class="chart-header">
|
||
<div>
|
||
<h2 class="section-title">📊 Monthly School Days & Hours Logged</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Total daycare hours attended across all 13 months.</p>
|
||
</div>
|
||
</div>
|
||
<div class="canvas-container" style="height: 300px;">
|
||
<canvas id="monthlyChart"></canvas>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Calendar Attendance Heatmap -->
|
||
<section class="glass-card calendar-card" style="margin-top: 2rem;">
|
||
<div class="section-title-wrap">
|
||
<div>
|
||
<h2 class="section-title">📅 13-Month Daycare Attendance Calendar Heatmap</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Complete school year attendance map. Hover over any block to see drop-off & pick-up details.</p>
|
||
</div>
|
||
</div>
|
||
<div class="heatmap-grid" id="calendarHeatmap">
|
||
<!-- Generated via JS -->
|
||
</div>
|
||
<div class="heatmap-legend">
|
||
<span>No School</span>
|
||
<div class="legend-box c-none" title="Weekend / Holiday"></div>
|
||
<div class="legend-box c-half" title="Half Day (<6.0h)"></div>
|
||
<div class="legend-box c-std" title="Standard (6.0–7.49h)"></div>
|
||
<div class="legend-box c-full" title="Full Day (7.5–8.2h)"></div>
|
||
<div class="legend-box c-ext" title="Extended Day (>8.2h)"></div>
|
||
<span>Full / Extended Day</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Interactive Data Explorer Table -->
|
||
<section class="glass-card" style="margin-top: 2rem;">
|
||
<div class="section-title-wrap">
|
||
<div>
|
||
<h2 class="section-title">📋 Attendance Session Explorer</h2>
|
||
<p class="section-desc" style="margin-bottom: 0;">Browse, search, and filter all paired attendance days.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-controls">
|
||
<input type="text" id="searchInput" class="search-input" placeholder="🔍 Search by date, parent name, notes..." oninput="filterTable()">
|
||
<div class="filter-tags">
|
||
<button class="filter-btn active" onclick="setDurationFilter('all', this)">All Days</button>
|
||
<button class="filter-btn" onclick="setDurationFilter('full', this)">Full Day (7.5h+)</button>
|
||
<button class="filter-btn" onclick="setDurationFilter('std', this)">Standard (6.0–7.49h)</button>
|
||
<button class="filter-btn" onclick="setDurationFilter('half', this)">Half Day (<6.0h)</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>Day</th>
|
||
<th>Drop-Off</th>
|
||
<th>Pick-Up</th>
|
||
<th>Stay Duration</th>
|
||
<th>Rolling 14d Avg</th>
|
||
<th>Drop-Off By</th>
|
||
<th>Pick-Up By</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="tableBody">
|
||
<!-- Populated via JS -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Footer -->
|
||
<footer class="footer">
|
||
<p>Data extracted & analyzed via <strong>Brightwheel Memories & Reports Exporter</strong> • Generated <span id="footerDate"></span></p>
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
// Embedded Data Payload
|
||
const DATA = __ATTENDANCE_DATA_JSON__;
|
||
|
||
let activeFilter = 'all';
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initDashboard();
|
||
});
|
||
|
||
function initDashboard() {
|
||
const stats = DATA.statistics || {};
|
||
const sessions = DATA.sessions || [];
|
||
const studentName = DATA.student_name || "Child";
|
||
|
||
// Header info
|
||
document.getElementById('childHeaderSubtitle').textContent =
|
||
`${studentName}'s 13-Month Daycare Attendance Profile (${sessions.length} paired days tracked from ${sessions[0]?.date || ''} to ${sessions[sessions.length - 1]?.date || ''})`;
|
||
document.getElementById('footerDate').textContent = DATA.generated_at || new Date().toLocaleDateString();
|
||
|
||
// Child name replacements
|
||
document.querySelectorAll('.dyn-child-name').forEach(el => el.textContent = studentName);
|
||
|
||
// KPIs
|
||
document.getElementById('kpiTotalHours').textContent = `${stats.total_school_hours || 0} hrs`;
|
||
document.getElementById('kpiTotalDays').textContent = stats.total_paired_days || sessions.length;
|
||
document.getElementById('kpiMeanStay').textContent = `${stats.mean_stay_hours || 0} hrs`;
|
||
document.getElementById('kpiMedianStay').textContent = `${stats.median_stay_hours || 0}h`;
|
||
document.getElementById('kpiIQR').textContent = `${stats.p25_hours || 0}h–${stats.p75_hours || 0}h`;
|
||
document.getElementById('kpiAvgDropoff').textContent = stats.avg_dropoff_time || '--';
|
||
document.getElementById('kpiAvgPickup').textContent = stats.avg_pickup_time || '--';
|
||
|
||
// Dynamic insight numbers
|
||
const longest = stats.longest_day || {};
|
||
const shortest = stats.shortest_day || {};
|
||
const earliest = stats.earliest_dropoff || {};
|
||
const latest = stats.latest_pickup || {};
|
||
|
||
if (document.getElementById('dynDropoff')) document.getElementById('dynDropoff').textContent = stats.avg_dropoff_time || '9:43 AM';
|
||
if (document.getElementById('dynPickup')) document.getElementById('dynPickup').textContent = stats.avg_pickup_time || '5:21 PM';
|
||
|
||
const dropSplit = stats.dropoff_split || [];
|
||
const pickSplit = stats.pickup_split || [];
|
||
if (dropSplit.length) {
|
||
if (document.getElementById('dynTopDropoff')) document.getElementById('dynTopDropoff').textContent = dropSplit[0].name;
|
||
if (document.getElementById('dynDropoffPct')) document.getElementById('dynDropoffPct').textContent = `${dropSplit[0].pct}%`;
|
||
}
|
||
if (pickSplit.length >= 2) {
|
||
if (document.getElementById('dynPickupSplit')) document.getElementById('dynPickupSplit').textContent = `${pickSplit[0].name} (${pickSplit[0].pct}%) vs ${pickSplit[1].name} (${pickSplit[1].pct}%)`;
|
||
}
|
||
if (document.getElementById('dynStreak')) document.getElementById('dynStreak').textContent = stats.longest_streak_days || 24;
|
||
if (document.getElementById('dynTotalDaysAttended')) document.getElementById('dynTotalDaysAttended').textContent = stats.total_paired_days || sessions.length;
|
||
if (document.getElementById('dynTotalActiveDays')) document.getElementById('dynTotalActiveDays').textContent = stats.total_activity_days || stats.total_paired_days;
|
||
if (document.getElementById('dynLongestDay')) document.getElementById('dynLongestDay').textContent = `${longest.stay_duration_hours || 8.63} hours`;
|
||
if (document.getElementById('dynShortestDay')) document.getElementById('dynShortestDay').textContent = `${shortest.stay_duration_hours || 1.52} hours`;
|
||
if (document.getElementById('dynEarliest')) document.getElementById('dynEarliest').textContent = earliest.dropoff_time_str || '9:02 AM';
|
||
if (document.getElementById('dynLatest')) document.getElementById('dynLatest').textContent = latest.pickup_time_str || '5:46 PM';
|
||
|
||
// Render Visualizations
|
||
renderTimelineChart(sessions, stats);
|
||
renderScatterChart(sessions);
|
||
renderParentSplitChart(stats);
|
||
renderMonthlyChart(stats.monthly_breakdown || []);
|
||
renderCalendarHeatmap(sessions);
|
||
renderTable(sessions);
|
||
}
|
||
|
||
function renderTimelineChart(sessions, stats) {
|
||
const ctx = document.getElementById('timelineChart').getContext('2d');
|
||
const labels = sessions.map(s => s.date);
|
||
const durations = sessions.map(s => s.stay_duration_hours);
|
||
const rollingAvg = sessions.map(s => s.rolling_avg_14d_hours);
|
||
|
||
new Chart(ctx, {
|
||
type: 'line',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [
|
||
{
|
||
label: 'Daily Stay Duration (Hours)',
|
||
data: durations,
|
||
borderColor: 'rgba(56, 189, 248, 0.35)',
|
||
backgroundColor: 'rgba(56, 189, 248, 0.7)',
|
||
pointRadius: 3,
|
||
pointHoverRadius: 6,
|
||
pointBackgroundColor: '#38BDF8',
|
||
showLine: false,
|
||
order: 2
|
||
},
|
||
{
|
||
label: '14-Day Rolling Trendline',
|
||
data: rollingAvg,
|
||
borderColor: '#34D399',
|
||
borderWidth: 3,
|
||
tension: 0.35,
|
||
pointRadius: 0,
|
||
fill: false,
|
||
order: 1
|
||
},
|
||
{
|
||
label: '7.6h Average Baseline',
|
||
data: Array(sessions.length).fill(stats.mean_stay_hours || 7.62),
|
||
borderColor: 'rgba(251, 191, 36, 0.4)',
|
||
borderWidth: 1.5,
|
||
borderDash: [5, 5],
|
||
pointRadius: 0,
|
||
fill: false,
|
||
order: 3
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: { mode: 'index', intersect: false },
|
||
plugins: {
|
||
legend: { labels: { color: '#94A3B8' } },
|
||
tooltip: {
|
||
backgroundColor: '#0F172A',
|
||
padding: 12,
|
||
callbacks: {
|
||
afterLabel: function(context) {
|
||
if (context.datasetIndex === 0) {
|
||
const s = sessions[context.dataIndex];
|
||
return `Dropoff: ${s.dropoff_time_str} (${s.dropoff_by})\nPickup: ${s.pickup_time_str} (${s.pickup_by})`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
grid: { color: 'rgba(255,255,255,0.03)' },
|
||
ticks: { color: '#64748B', maxTicksLimit: 14 }
|
||
},
|
||
y: {
|
||
grid: { color: 'rgba(255,255,255,0.05)' },
|
||
ticks: { color: '#64748B', callback: val => val + ' hrs' },
|
||
min: 0,
|
||
max: 10
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderScatterChart(sessions) {
|
||
const ctx = document.getElementById('scatterChart').getContext('2d');
|
||
const points = sessions.map(s => ({
|
||
x: s.dropoff_dec_hour,
|
||
y: s.pickup_dec_hour,
|
||
raw: s
|
||
}));
|
||
|
||
new Chart(ctx, {
|
||
type: 'scatter',
|
||
data: {
|
||
datasets: [{
|
||
label: 'Day Sessions',
|
||
data: points,
|
||
backgroundColor: '#38BDF8',
|
||
pointRadius: 4.5,
|
||
pointHoverRadius: 7
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
backgroundColor: '#0F172A',
|
||
callbacks: {
|
||
label: ctx => {
|
||
const s = ctx.raw.raw;
|
||
return `${s.date}: In at ${s.dropoff_time_str} ➔ Out at ${s.pickup_time_str} (${s.stay_duration_hours} hrs)`;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
x: {
|
||
title: { display: true, text: 'Drop-Off Time (Morning)', color: '#94A3B8' },
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: '#64748B',
|
||
callback: val => {
|
||
const h = Math.floor(val);
|
||
const m = Math.round((val - h) * 60);
|
||
return `${h}:${m < 10 ? '0' : ''}${m} AM`;
|
||
}
|
||
},
|
||
min: 8.5,
|
||
max: 11.5
|
||
},
|
||
y: {
|
||
title: { display: true, text: 'Pick-Up Time (Afternoon)', color: '#94A3B8' },
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: {
|
||
color: '#64748B',
|
||
callback: val => {
|
||
const h = Math.floor(val) - 12;
|
||
const m = Math.round((val - Math.floor(val)) * 60);
|
||
return `${h}:${m < 10 ? '0' : ''}${m} PM`;
|
||
}
|
||
},
|
||
min: 15.0,
|
||
max: 18.5
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderParentSplitChart(stats) {
|
||
const ctx = document.getElementById('parentSplitChart').getContext('2d');
|
||
const dropSplit = stats.dropoff_split || [];
|
||
const pickSplit = stats.pickup_split || [];
|
||
|
||
// Combine unique parent names
|
||
const names = Array.from(new Set([...dropSplit.map(x => x.name), ...pickSplit.map(x => x.name)])).slice(0, 3);
|
||
const dropCounts = names.map(n => dropSplit.find(x => x.name === n)?.count || 0);
|
||
const pickCounts = names.map(n => pickSplit.find(x => x.name === n)?.count || 0);
|
||
|
||
new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: names,
|
||
datasets: [
|
||
{
|
||
label: 'Morning Drop-offs',
|
||
data: dropCounts,
|
||
backgroundColor: '#38BDF8',
|
||
borderRadius: 6
|
||
},
|
||
{
|
||
label: 'Afternoon Pick-ups',
|
||
data: pickCounts,
|
||
backgroundColor: '#818CF8',
|
||
borderRadius: 6
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { labels: { color: '#94A3B8' } },
|
||
tooltip: { backgroundColor: '#0F172A' }
|
||
},
|
||
scales: {
|
||
x: { grid: { display: false }, ticks: { color: '#64748B' } },
|
||
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#64748B' } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderMonthlyChart(monthlyData) {
|
||
const ctx = document.getElementById('monthlyChart').getContext('2d');
|
||
const labels = monthlyData.map(m => m.month);
|
||
const hours = monthlyData.map(m => m.total_hours);
|
||
const days = monthlyData.map(m => m.days_attended);
|
||
|
||
new Chart(ctx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: labels,
|
||
datasets: [
|
||
{
|
||
label: 'Total Hours at Daycare',
|
||
data: hours,
|
||
backgroundColor: '#38BDF8',
|
||
borderRadius: 6,
|
||
yAxisID: 'y'
|
||
},
|
||
{
|
||
label: 'Days Attended',
|
||
data: days,
|
||
type: 'line',
|
||
borderColor: '#FBBF24',
|
||
backgroundColor: '#FBBF24',
|
||
borderWidth: 2,
|
||
pointRadius: 4,
|
||
yAxisID: 'y1'
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { labels: { color: '#94A3B8' } },
|
||
tooltip: { backgroundColor: '#0F172A' }
|
||
},
|
||
scales: {
|
||
x: { grid: { display: false }, ticks: { color: '#64748B' } },
|
||
y: {
|
||
type: 'linear',
|
||
position: 'left',
|
||
grid: { color: 'rgba(255,255,255,0.04)' },
|
||
ticks: { color: '#64748B', callback: val => val + 'h' },
|
||
title: { display: true, text: 'Total Hours', color: '#94A3B8' }
|
||
},
|
||
y1: {
|
||
type: 'linear',
|
||
position: 'right',
|
||
grid: { display: false },
|
||
ticks: { color: '#FBBF24' },
|
||
title: { display: true, text: 'Days', color: '#FBBF24' },
|
||
min: 0,
|
||
max: 25
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderCalendarHeatmap(sessions) {
|
||
const grid = document.getElementById('calendarHeatmap');
|
||
grid.innerHTML = '';
|
||
|
||
if (!sessions || sessions.length === 0) return;
|
||
|
||
const dateMap = {};
|
||
sessions.forEach(s => {
|
||
dateMap[s.date] = s;
|
||
});
|
||
|
||
const firstDateStr = sessions[0].date;
|
||
const lastDateStr = sessions[sessions.length - 1].date;
|
||
|
||
const startDate = new Date(firstDateStr + 'T00:00:00');
|
||
const endDate = new Date(lastDateStr + 'T00:00:00');
|
||
|
||
const curr = new Date(startDate);
|
||
while (curr <= endDate) {
|
||
const dStr = curr.toISOString().split('T')[0];
|
||
const session = dateMap[dStr];
|
||
|
||
const cell = document.createElement('div');
|
||
cell.className = 'heat-cell';
|
||
|
||
if (session) {
|
||
const dur = session.stay_duration_hours;
|
||
if (dur >= 8.2) cell.classList.add('c-ext');
|
||
else if (dur >= 7.5) cell.classList.add('c-full');
|
||
else if (dur >= 6.0) cell.classList.add('c-std');
|
||
else cell.classList.add('c-half');
|
||
|
||
cell.title = `${dStr} (${session.weekday_short})\nStay: ${dur} hrs (${session.dropoff_time_str} – ${session.pickup_time_str})\nIn: ${session.dropoff_by} | Out: ${session.pickup_by}`;
|
||
} else {
|
||
cell.classList.add('c-none');
|
||
cell.title = `${dStr} (No School / Weekend)`;
|
||
}
|
||
|
||
grid.appendChild(cell);
|
||
curr.setDate(curr.getDate() + 1);
|
||
}
|
||
}
|
||
|
||
function renderTable(sessions) {
|
||
const tbody = document.getElementById('tableBody');
|
||
tbody.innerHTML = '';
|
||
|
||
const query = (document.getElementById('searchInput').value || '').toLowerCase().trim();
|
||
|
||
const filtered = sessions.filter(s => {
|
||
// Duration filter
|
||
if (activeFilter === 'full' && s.stay_duration_hours < 7.5) return false;
|
||
if (activeFilter === 'std' && (s.stay_duration_hours < 6.0 || s.stay_duration_hours >= 7.5)) return false;
|
||
if (activeFilter === 'half' && s.stay_duration_hours >= 6.0) return false;
|
||
|
||
// Search query
|
||
if (query) {
|
||
const matchStr = `${s.date} ${s.weekday} ${s.dropoff_by} ${s.pickup_by} ${s.dropoff_time_str} ${s.pickup_time_str}`.toLowerCase();
|
||
if (!matchStr.includes(query)) return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
filtered.forEach(s => {
|
||
const tr = document.createElement('tr');
|
||
|
||
let badgeClass = 'dur-std';
|
||
if (s.stay_duration_hours >= 7.5) badgeClass = 'dur-full';
|
||
else if (s.stay_duration_hours < 6.0) badgeClass = 'dur-half';
|
||
|
||
tr.innerHTML = `
|
||
<td><strong>${s.date}</strong></td>
|
||
<td style="color: var(--text-secondary);">${s.weekday}</td>
|
||
<td>${s.dropoff_time_str}</td>
|
||
<td>${s.pickup_time_str}</td>
|
||
<td><span class="duration-badge ${badgeClass}">${s.stay_duration_hours} hrs</span></td>
|
||
<td style="color: var(--text-muted);">${s.rolling_avg_14d_hours || '--'}h</td>
|
||
<td style="color: var(--text-secondary);">${s.dropoff_by}</td>
|
||
<td style="color: var(--text-secondary);">${s.pickup_by}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function filterTable() {
|
||
renderTable(DATA.sessions || []);
|
||
}
|
||
|
||
function setDurationFilter(filter, el) {
|
||
activeFilter = filter;
|
||
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
||
if (el) el.classList.add('active');
|
||
filterTable();
|
||
}
|
||
|
||
function exportAttendanceCSV() {
|
||
const sessions = DATA.sessions || [];
|
||
if (!sessions.length) return;
|
||
let csv = 'Date,Month,Day,Dropoff Time,Pickup Time,Stay Hours,Stay Mins,Rolling 14d Avg,Dropoff By,Pickup By\n';
|
||
sessions.forEach(s => {
|
||
csv += `"${s.date}","${s.month}","${s.weekday}","${s.dropoff_time_str}","${s.pickup_time_str}",${s.stay_duration_hours},${s.stay_duration_minutes},${s.rolling_avg_14d_hours || ''},"${s.dropoff_by}","${s.pickup_by}"\n`;
|
||
});
|
||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `attendance_sessions_${DATA.student_name || 'child'}.csv`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|