mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 00:38:44 +00:00
Latest docs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+953
@@ -0,0 +1,953 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Database ERD — Student Progress Tracker</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--surface: #181a24;
|
||||
--surface-hover: #1e2130;
|
||||
--border: #2a2d3e;
|
||||
--border-highlight: #3d4160;
|
||||
--text: #c9cdd8;
|
||||
--text-muted: #6b7084;
|
||||
--text-bright: #eef0f6;
|
||||
--pk: #f0b866;
|
||||
--pk-bg: rgba(240, 184, 102, 0.08);
|
||||
--fk: #6ea8fe;
|
||||
--fk-bg: rgba(110, 168, 254, 0.08);
|
||||
--col: #8b91a8;
|
||||
--type: #5a5f75;
|
||||
--line-color: #3d5a80;
|
||||
--line-highlight: #6ea8fe;
|
||||
--accent-green: #66d9a0;
|
||||
--accent-red: #f07178;
|
||||
--accent-purple: #c792ea;
|
||||
--shadow: 0 4px 24px rgba(0,0,0,0.4);
|
||||
--shadow-lg: 0 8px 40px rgba(0,0,0,0.6);
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 52px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
z-index: 1000;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#toolbar h1 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
#toolbar .separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
#toolbar .stat {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#toolbar .stat span { color: var(--text); font-weight: 600; }
|
||||
|
||||
.toolbar-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
background: var(--border);
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toolbar-btn:hover { background: var(--border-highlight); color: var(--text-bright); }
|
||||
|
||||
#canvas-container {
|
||||
position: absolute;
|
||||
top: 52px; left: 0; right: 0; bottom: 0;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
}
|
||||
#canvas-container:active { cursor: grabbing; }
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
svg#lines {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.table-node {
|
||||
position: absolute;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
min-width: 240px;
|
||||
box-shadow: var(--shadow);
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
transition: box-shadow 0.2s, border-color 0.2s;
|
||||
}
|
||||
.table-node:hover {
|
||||
border-color: var(--border-highlight);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 10;
|
||||
}
|
||||
.table-node.dragging {
|
||||
z-index: 100;
|
||||
border-color: var(--fk);
|
||||
box-shadow: 0 0 0 1px var(--fk), var(--shadow-lg);
|
||||
}
|
||||
|
||||
.table-header {
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.table-icon {
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-header .table-name {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.table-body { padding: 4px 0; }
|
||||
|
||||
.column-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 3px 14px;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.6;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.column-row:hover { background: var(--surface-hover); }
|
||||
|
||||
.col-badge {
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
letter-spacing: 0.05em;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.col-badge.pk { background: var(--pk-bg); color: var(--pk); }
|
||||
.col-badge.fk { background: var(--fk-bg); color: var(--fk); }
|
||||
.col-badge.empty { min-width: 20px; }
|
||||
|
||||
.col-name { color: var(--col); flex: 1; }
|
||||
.col-name.pk-name { color: var(--pk); }
|
||||
.col-name.fk-name { color: var(--fk); }
|
||||
|
||||
.col-type { color: var(--type); font-size: 10px; text-align: right; }
|
||||
|
||||
/* Legend */
|
||||
#legend {
|
||||
position: fixed;
|
||||
bottom: 16px; left: 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 16px;
|
||||
z-index: 1000;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 6px; color: var(--text-muted); }
|
||||
.legend-dot { width: 8px; height: 8px; border-radius: 2px; }
|
||||
|
||||
/* Zoom controls */
|
||||
#zoom-controls {
|
||||
position: fixed;
|
||||
bottom: 16px; right: 16px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
#zoom-controls button {
|
||||
width: 36px; height: 36px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
#zoom-controls button:hover { background: var(--surface-hover); border-color: var(--border-highlight); }
|
||||
|
||||
/* Minimap */
|
||||
#minimap {
|
||||
position: fixed;
|
||||
bottom: 60px; right: 16px;
|
||||
width: 180px; height: 120px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
#minimap canvas { width: 100%; height: 100%; }
|
||||
|
||||
/* Relationship line styles */
|
||||
.rel-line {
|
||||
fill: none;
|
||||
stroke: var(--line-color);
|
||||
stroke-width: 1.5;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s, stroke 0.2s;
|
||||
}
|
||||
.rel-line.highlighted {
|
||||
stroke: var(--line-highlight);
|
||||
opacity: 1;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.rel-marker {
|
||||
fill: var(--line-color);
|
||||
transition: fill 0.2s;
|
||||
}
|
||||
.rel-marker.highlighted { fill: var(--line-highlight); }
|
||||
|
||||
.rel-label {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 9px;
|
||||
fill: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="toolbar">
|
||||
<h1>⬡ ERD</h1>
|
||||
<div class="separator"></div>
|
||||
<div class="stat">Tables <span id="table-count">17</span></div>
|
||||
<div class="separator"></div>
|
||||
<div class="stat">Relations <span id="rel-count">0</span></div>
|
||||
<div class="toolbar-actions">
|
||||
<button class="toolbar-btn" onclick="resetView()">Reset View</button>
|
||||
<button class="toolbar-btn" onclick="toggleLabels()">Toggle Labels</button>
|
||||
<a href="technical.html" class="toolbar-btn" style="text-decoration:none;">← Technical Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="canvas-container">
|
||||
<div id="canvas">
|
||||
<svg id="lines" width="6000" height="4000"></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<div class="legend-item"><div class="legend-dot" style="background:var(--pk)"></div> Primary Key</div>
|
||||
<div class="legend-item"><div class="legend-dot" style="background:var(--fk)"></div> Foreign Key</div>
|
||||
<div class="legend-item">
|
||||
<svg width="30" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="var(--line-color)" stroke-width="1.5"/></svg>
|
||||
Relationship
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="zoom-controls">
|
||||
<button onclick="zoomIn()">+</button>
|
||||
<button onclick="zoomOut()">−</button>
|
||||
<button onclick="resetView()" style="font-size:12px">⌂</button>
|
||||
</div>
|
||||
|
||||
<div id="minimap"><canvas id="minimap-canvas"></canvas></div>
|
||||
|
||||
<script>
|
||||
// ─── Schema Data ───
|
||||
const schema = {
|
||||
tables: {
|
||||
user: {
|
||||
columns: [
|
||||
{ name: 'id_user', type: 'char(36)', pk: true },
|
||||
{ name: 'email', type: 'varchar(255)' },
|
||||
{ name: 'name', type: 'varchar(255)' },
|
||||
{ name: 'password_hash', type: 'varchar(255)' },
|
||||
{ name: 'password_salt', type: 'varchar(255)' },
|
||||
{ name: 'password_updated_at', type: 'timestamp' },
|
||||
{ name: 'failed_login_attempts', type: 'int' },
|
||||
{ name: 'locked_until', type: 'timestamp' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#6ea8fe'
|
||||
},
|
||||
student: {
|
||||
columns: [
|
||||
{ name: 'id_student', type: 'char(36)', pk: true },
|
||||
{ name: 'id_program', type: 'char(36)', fk: 'program' },
|
||||
{ name: 'identifier', type: 'varchar(50)' },
|
||||
{ name: 'program_year', type: 'int' },
|
||||
{ name: 'enrollment_date', type: 'date' },
|
||||
{ name: 'next_iep_date', type: 'date' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#66d9a0'
|
||||
},
|
||||
goal: {
|
||||
columns: [
|
||||
{ name: 'id_goal', type: 'char(36)', pk: true },
|
||||
{ name: 'id_goal_parent', type: 'char(36)', fk: 'goal' },
|
||||
{ name: 'id_student', type: 'char(36)', fk: 'student' },
|
||||
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'category', type: 'varchar(255)' },
|
||||
{ name: 'baseline', type: 'text' },
|
||||
{ name: 'target_completion_date', type: 'date' },
|
||||
{ name: 'close_date', type: 'date' },
|
||||
{ name: 'achieved', type: 'tinyint(1)' },
|
||||
{ name: 'close_notes', type: 'text' },
|
||||
{ name: 'created_at', type: 'timestamp' },
|
||||
{ name: 'updated_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#f0b866'
|
||||
},
|
||||
benchmark: {
|
||||
columns: [
|
||||
{ name: 'id_benchmark', type: 'char(36)', pk: true },
|
||||
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
|
||||
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'benchmark', type: 'text' },
|
||||
{ name: 'short_name', type: 'varchar(50)' },
|
||||
{ name: 'created_at', type: 'datetime' },
|
||||
{ name: 'updated_at', type: 'datetime' }
|
||||
],
|
||||
color: '#f0b866'
|
||||
},
|
||||
progress_event: {
|
||||
columns: [
|
||||
{ name: 'id_progress_event', type: 'char(36)', pk: true },
|
||||
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
|
||||
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'content', type: 'text' },
|
||||
{ name: 'is_sensitive', type: 'tinyint(1)' },
|
||||
{ name: 'created_at', type: 'timestamp' },
|
||||
{ name: 'updated_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#c792ea'
|
||||
},
|
||||
progress_event_benchmark: {
|
||||
columns: [
|
||||
{ name: 'id_progress_event_benchmark', type: 'char(36)', pk: true },
|
||||
{ name: 'id_progress_event', type: 'char(36)', fk: 'progress_event' },
|
||||
{ name: 'id_benchmark', type: 'char(36)', fk: 'benchmark' },
|
||||
{ name: 'created_at', type: 'datetime' }
|
||||
],
|
||||
color: '#c792ea'
|
||||
},
|
||||
progress_report: {
|
||||
columns: [
|
||||
{ name: 'id_progress_report', type: 'char(36)', pk: true },
|
||||
{ name: 'id_student', type: 'char(36)', fk: 'student' },
|
||||
{ name: 'id_goal', type: 'char(36)', fk: 'goal' },
|
||||
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'period', type: 'varchar(10)' },
|
||||
{ name: 'year', type: 'int' },
|
||||
{ name: 'summary', type: 'text' },
|
||||
{ name: 'generated_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#c792ea'
|
||||
},
|
||||
health_note: {
|
||||
columns: [
|
||||
{ name: 'id_health_note', type: 'char(36)', pk: true },
|
||||
{ name: 'id_student', type: 'char(36)', fk: 'student' },
|
||||
{ name: 'id_user_created', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'content', type: 'text' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#66d9a0'
|
||||
},
|
||||
program: {
|
||||
columns: [
|
||||
{ name: 'id_program', type: 'char(36)', pk: true },
|
||||
{ name: 'id_school_district', type: 'char(36)', fk: 'school_district' },
|
||||
{ name: 'name', type: 'varchar(255)' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#f07178'
|
||||
},
|
||||
school_district: {
|
||||
columns: [
|
||||
{ name: 'id_school_district', type: 'char(36)', pk: true },
|
||||
{ name: 'name', type: 'varchar(255)' },
|
||||
{ name: 'contact_email', type: 'varchar(255)' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#f07178'
|
||||
},
|
||||
role: {
|
||||
columns: [
|
||||
{ name: 'id_role', type: 'char(36)', pk: true },
|
||||
{ name: 'name', type: 'varchar(100)' },
|
||||
{ name: 'internal_name', type: 'varchar(100)' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#6ea8fe'
|
||||
},
|
||||
user_program: {
|
||||
columns: [
|
||||
{ name: 'id_user_program', type: 'char(36)', pk: true },
|
||||
{ name: 'id_user', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'id_program', type: 'char(36)', fk: 'program' },
|
||||
{ name: 'id_role', type: 'char(36)', fk: 'role' },
|
||||
{ name: 'is_primary', type: 'tinyint(1)' },
|
||||
{ name: 'status', type: 'varchar(20)' },
|
||||
{ name: 'joined_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#6ea8fe'
|
||||
},
|
||||
user_student: {
|
||||
columns: [
|
||||
{ name: 'id_user_student', type: 'char(36)', pk: true },
|
||||
{ name: 'id_user', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'id_student', type: 'char(36)', fk: 'student' },
|
||||
{ name: 'is_primary', type: 'tinyint(1)' }
|
||||
],
|
||||
color: '#6ea8fe'
|
||||
},
|
||||
password_history: {
|
||||
columns: [
|
||||
{ name: 'id_password_history', type: 'char(36)', pk: true },
|
||||
{ name: 'id_user', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'password_hash', type: 'varchar(255)' },
|
||||
{ name: 'created_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#5a5f75'
|
||||
},
|
||||
password_reset_token: {
|
||||
columns: [
|
||||
{ name: 'id_password_reset_token', type: 'char(36)', pk: true },
|
||||
{ name: 'id_user', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'token_hash', type: 'varchar(255)' },
|
||||
{ name: 'expires_at', type: 'timestamp' },
|
||||
{ name: 'created_at', type: 'timestamp' },
|
||||
{ name: 'used_at', type: 'timestamp' },
|
||||
{ name: 'invalidated_at', type: 'timestamp' },
|
||||
{ name: 'request_ip', type: 'varchar(45)' }
|
||||
],
|
||||
color: '#5a5f75'
|
||||
},
|
||||
refresh_token: {
|
||||
columns: [
|
||||
{ name: 'id_refresh_token', type: 'char(36)', pk: true },
|
||||
{ name: 'id_user', type: 'char(36)', fk: 'user' },
|
||||
{ name: 'id_program', type: 'char(36)', fk: 'program' },
|
||||
{ name: 'token_hash', type: 'varchar(512)' },
|
||||
{ name: 'token_salt', type: 'varchar(512)' },
|
||||
{ name: 'expires_at', type: 'timestamp' },
|
||||
{ name: 'last_used_at', type: 'timestamp' },
|
||||
{ name: 'revoked_at', type: 'timestamp' },
|
||||
{ name: 'device_info', type: 'varchar(255)' },
|
||||
{ name: 'user_agent', type: 'varchar(512)' },
|
||||
{ name: 'replaced_by_token_id', type: 'char(36)', fk: 'refresh_token' },
|
||||
{ name: 'created_at', type: 'timestamp' },
|
||||
{ name: 'updated_at', type: 'timestamp' }
|
||||
],
|
||||
color: '#5a5f75'
|
||||
},
|
||||
ReportPrompt: {
|
||||
columns: [
|
||||
{ name: 'id_ReportPrompt', type: 'char(36)', pk: true },
|
||||
{ name: 'prompt', type: 'text' },
|
||||
{ name: 'reportname', type: 'char(100)' },
|
||||
{ name: 'id_program', type: 'char(36)', fk: 'program' }
|
||||
],
|
||||
color: '#c792ea'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Explicit foreign key relationships from the SQL constraints
|
||||
const relationships = [
|
||||
{ from: 'goal', fromCol: 'id_goal_parent', to: 'goal', toCol: 'id_goal', label: 'parent' },
|
||||
{ from: 'goal', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
|
||||
{ from: 'goal', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'health_note', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
|
||||
{ from: 'health_note', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'password_history', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'password_reset_token', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'program', fromCol: 'id_school_district', to: 'school_district', toCol: 'id_school_district' },
|
||||
{ from: 'progress_event', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
|
||||
{ from: 'progress_event', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'progress_event_benchmark', fromCol: 'id_progress_event', to: 'progress_event', toCol: 'id_progress_event' },
|
||||
{ from: 'progress_event_benchmark', fromCol: 'id_benchmark', to: 'benchmark', toCol: 'id_benchmark' },
|
||||
{ from: 'progress_report', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
|
||||
{ from: 'progress_report', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
|
||||
{ from: 'progress_report', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'refresh_token', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'refresh_token', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
|
||||
{ from: 'refresh_token', fromCol: 'replaced_by_token_id', to: 'refresh_token', toCol: 'id_refresh_token', label: 'replaces' },
|
||||
{ from: 'benchmark', fromCol: 'id_goal', to: 'goal', toCol: 'id_goal' },
|
||||
{ from: 'benchmark', fromCol: 'id_user_created', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'student', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
|
||||
{ from: 'user_program', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'user_program', fromCol: 'id_program', to: 'program', toCol: 'id_program' },
|
||||
{ from: 'user_program', fromCol: 'id_role', to: 'role', toCol: 'id_role' },
|
||||
{ from: 'user_student', fromCol: 'id_user', to: 'user', toCol: 'id_user' },
|
||||
{ from: 'user_student', fromCol: 'id_student', to: 'student', toCol: 'id_student' },
|
||||
{ from: 'ReportPrompt', fromCol: 'id_program', to: 'program', toCol: 'id_program' }
|
||||
];
|
||||
|
||||
document.getElementById('rel-count').textContent = relationships.length;
|
||||
|
||||
// ─── Layout positions (hand-tuned for clarity) ───
|
||||
const positions = {
|
||||
// Core cluster
|
||||
user: { x: 100, y: 300 },
|
||||
role: { x: 100, y: 50 },
|
||||
user_program: { x: 380, y: 80 },
|
||||
user_student: { x: 380, y: 340 },
|
||||
// Org cluster
|
||||
school_district: { x: 680, y: 50 },
|
||||
program: { x: 680, y: 260 },
|
||||
student: { x: 680, y: 500 },
|
||||
// Goals & progress cluster
|
||||
goal: { x: 1050, y: 260 },
|
||||
benchmark: { x: 1050, y: 50 },
|
||||
progress_event: { x: 1380, y: 100 },
|
||||
progress_event_benchmark: { x: 1380, y: 340 },
|
||||
progress_report: { x: 1050, y: 570 },
|
||||
health_note: { x: 680, y: 760 },
|
||||
ReportPrompt: { x: 1380, y: 570 },
|
||||
// Auth cluster
|
||||
password_history: { x: 100, y: 620 },
|
||||
password_reset_token: { x: 100, y: 800 },
|
||||
refresh_token: { x: 380, y: 620 }
|
||||
};
|
||||
|
||||
// ─── Rendering ───
|
||||
const canvas = document.getElementById('canvas');
|
||||
const svgLines = document.getElementById('lines');
|
||||
const container = document.getElementById('canvas-container');
|
||||
const tableElements = {};
|
||||
let scale = 0.85;
|
||||
let panX = 50, panY = 20;
|
||||
let showLabels = true;
|
||||
|
||||
function renderTables() {
|
||||
for (const [tableName, table] of Object.entries(schema.tables)) {
|
||||
const pos = positions[tableName] || { x: 0, y: 0 };
|
||||
const el = document.createElement('div');
|
||||
el.className = 'table-node';
|
||||
el.id = `table-${tableName}`;
|
||||
el.style.left = pos.x + 'px';
|
||||
el.style.top = pos.y + 'px';
|
||||
|
||||
const headerColor = table.color || '#6ea8fe';
|
||||
let html = `
|
||||
<div class="table-header">
|
||||
<div class="table-icon" style="background:${headerColor}22;color:${headerColor}">⬡</div>
|
||||
<span class="table-name">${tableName}</span>
|
||||
</div>
|
||||
<div class="table-body">
|
||||
`;
|
||||
for (const col of table.columns) {
|
||||
const isPk = col.pk;
|
||||
const isFk = col.fk;
|
||||
let badge = '<span class="col-badge empty"></span>';
|
||||
if (isPk) badge = '<span class="col-badge pk">PK</span>';
|
||||
else if (isFk) badge = '<span class="col-badge fk">FK</span>';
|
||||
|
||||
let nameClass = 'col-name';
|
||||
if (isPk) nameClass += ' pk-name';
|
||||
else if (isFk) nameClass += ' fk-name';
|
||||
|
||||
html += `
|
||||
<div class="column-row" data-col="${col.name}">
|
||||
${badge}
|
||||
<span class="${nameClass}">${col.name}</span>
|
||||
<span class="col-type">${col.type}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
el.innerHTML = html;
|
||||
canvas.appendChild(el);
|
||||
tableElements[tableName] = el;
|
||||
makeDraggable(el, tableName);
|
||||
}
|
||||
}
|
||||
|
||||
function getColumnY(tableName, colName) {
|
||||
const el = tableElements[tableName];
|
||||
if (!el) return 0;
|
||||
const row = el.querySelector(`[data-col="${colName}"]`);
|
||||
if (!row) return el.offsetTop + 20;
|
||||
return el.offsetTop + row.offsetTop + row.offsetHeight / 2;
|
||||
}
|
||||
|
||||
function getTableCenter(tableName) {
|
||||
const el = tableElements[tableName];
|
||||
if (!el) return { x: 0, y: 0 };
|
||||
return {
|
||||
x: el.offsetLeft + el.offsetWidth / 2,
|
||||
y: el.offsetTop + el.offsetHeight / 2
|
||||
};
|
||||
}
|
||||
|
||||
function drawRelationships() {
|
||||
svgLines.innerHTML = '';
|
||||
|
||||
// Arrow marker defs
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
|
||||
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
||||
marker.setAttribute('id', 'arrowhead');
|
||||
marker.setAttribute('markerWidth', '8');
|
||||
marker.setAttribute('markerHeight', '6');
|
||||
marker.setAttribute('refX', '8');
|
||||
marker.setAttribute('refY', '3');
|
||||
marker.setAttribute('orient', 'auto');
|
||||
const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
||||
poly.setAttribute('points', '0 0, 8 3, 0 6');
|
||||
poly.setAttribute('class', 'rel-marker');
|
||||
marker.appendChild(poly);
|
||||
defs.appendChild(marker);
|
||||
|
||||
const markerH = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
||||
markerH.setAttribute('id', 'arrowhead-hl');
|
||||
markerH.setAttribute('markerWidth', '8');
|
||||
markerH.setAttribute('markerHeight', '6');
|
||||
markerH.setAttribute('refX', '8');
|
||||
markerH.setAttribute('refY', '3');
|
||||
markerH.setAttribute('orient', 'auto');
|
||||
const polyH = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
||||
polyH.setAttribute('points', '0 0, 8 3, 0 6');
|
||||
polyH.setAttribute('class', 'rel-marker highlighted');
|
||||
markerH.appendChild(polyH);
|
||||
defs.appendChild(markerH);
|
||||
|
||||
svgLines.appendChild(defs);
|
||||
|
||||
for (const rel of relationships) {
|
||||
const fromEl = tableElements[rel.from];
|
||||
const toEl = tableElements[rel.to];
|
||||
if (!fromEl || !toEl) continue;
|
||||
|
||||
const fromY = getColumnY(rel.from, rel.fromCol);
|
||||
const toY = getColumnY(rel.to, rel.toCol);
|
||||
|
||||
const fromLeft = fromEl.offsetLeft;
|
||||
const fromRight = fromEl.offsetLeft + fromEl.offsetWidth;
|
||||
const toLeft = toEl.offsetLeft;
|
||||
const toRight = toEl.offsetLeft + toEl.offsetWidth;
|
||||
|
||||
let x1, x2;
|
||||
// Self-referencing
|
||||
if (rel.from === rel.to) {
|
||||
x1 = fromRight;
|
||||
x2 = fromRight;
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
const loopSize = 40;
|
||||
const d = `M ${x1} ${fromY} C ${x1 + loopSize} ${fromY - loopSize}, ${x2 + loopSize} ${toY + loopSize}, ${x2} ${toY}`;
|
||||
path.setAttribute('d', d);
|
||||
path.setAttribute('class', 'rel-line');
|
||||
path.setAttribute('marker-end', 'url(#arrowhead)');
|
||||
path.dataset.from = rel.from;
|
||||
path.dataset.to = rel.to;
|
||||
svgLines.appendChild(path);
|
||||
|
||||
if (showLabels && rel.label) {
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', x1 + loopSize + 4);
|
||||
text.setAttribute('y', (fromY + toY) / 2);
|
||||
text.setAttribute('class', 'rel-label');
|
||||
text.textContent = rel.label;
|
||||
svgLines.appendChild(text);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which side to connect
|
||||
const fromCX = (fromLeft + fromRight) / 2;
|
||||
const toCX = (toLeft + toRight) / 2;
|
||||
|
||||
if (fromCX < toCX) {
|
||||
x1 = fromRight;
|
||||
x2 = toLeft;
|
||||
} else {
|
||||
x1 = fromLeft;
|
||||
x2 = toRight;
|
||||
}
|
||||
|
||||
const dx = Math.abs(x2 - x1);
|
||||
const cpOffset = Math.max(40, dx * 0.35);
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
const cp1x = x1 < x2 ? x1 + cpOffset : x1 - cpOffset;
|
||||
const cp2x = x1 < x2 ? x2 - cpOffset : x2 + cpOffset;
|
||||
const d = `M ${x1} ${fromY} C ${cp1x} ${fromY}, ${cp2x} ${toY}, ${x2} ${toY}`;
|
||||
path.setAttribute('d', d);
|
||||
path.setAttribute('class', 'rel-line');
|
||||
path.setAttribute('marker-end', 'url(#arrowhead)');
|
||||
path.dataset.from = rel.from;
|
||||
path.dataset.to = rel.to;
|
||||
svgLines.appendChild(path);
|
||||
|
||||
if (showLabels && rel.label) {
|
||||
const mx = (x1 + x2) / 2;
|
||||
const my = (fromY + toY) / 2 - 6;
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', mx);
|
||||
text.setAttribute('y', my);
|
||||
text.setAttribute('class', 'rel-label');
|
||||
text.setAttribute('text-anchor', 'middle');
|
||||
text.textContent = rel.label;
|
||||
svgLines.appendChild(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Interaction: Drag ───
|
||||
function makeDraggable(el, tableName) {
|
||||
let startX, startY, origX, origY;
|
||||
|
||||
el.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
e.stopPropagation();
|
||||
el.classList.add('dragging');
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
origX = el.offsetLeft;
|
||||
origY = el.offsetTop;
|
||||
|
||||
function onMove(e) {
|
||||
const dx = (e.clientX - startX) / scale;
|
||||
const dy = (e.clientY - startY) / scale;
|
||||
el.style.left = (origX + dx) + 'px';
|
||||
el.style.top = (origY + dy) + 'px';
|
||||
positions[tableName].x = origX + dx;
|
||||
positions[tableName].y = origY + dy;
|
||||
drawRelationships();
|
||||
updateMinimap();
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
el.classList.remove('dragging');
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
});
|
||||
|
||||
// Hover highlighting
|
||||
el.addEventListener('mouseenter', () => highlightTable(tableName));
|
||||
el.addEventListener('mouseleave', () => clearHighlights());
|
||||
}
|
||||
|
||||
function highlightTable(tableName) {
|
||||
const lines = svgLines.querySelectorAll('.rel-line');
|
||||
lines.forEach(line => {
|
||||
if (line.dataset.from === tableName || line.dataset.to === tableName) {
|
||||
line.classList.add('highlighted');
|
||||
line.setAttribute('marker-end', 'url(#arrowhead-hl)');
|
||||
} else {
|
||||
line.style.opacity = '0.15';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearHighlights() {
|
||||
const lines = svgLines.querySelectorAll('.rel-line');
|
||||
lines.forEach(line => {
|
||||
line.classList.remove('highlighted');
|
||||
line.setAttribute('marker-end', 'url(#arrowhead)');
|
||||
line.style.opacity = '';
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Pan & Zoom ───
|
||||
let isPanning = false, panStartX, panStartY;
|
||||
|
||||
container.addEventListener('mousedown', (e) => {
|
||||
if (e.target !== container && e.target !== canvas && e.target.tagName !== 'svg') return;
|
||||
isPanning = true;
|
||||
panStartX = e.clientX - panX;
|
||||
panStartY = e.clientY - panY;
|
||||
container.style.cursor = 'grabbing';
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isPanning) return;
|
||||
panX = e.clientX - panStartX;
|
||||
panY = e.clientY - panStartY;
|
||||
applyTransform();
|
||||
updateMinimap();
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isPanning = false;
|
||||
container.style.cursor = 'grab';
|
||||
});
|
||||
|
||||
container.addEventListener('wheel', (e) => {
|
||||
e.preventDefault();
|
||||
const delta = e.deltaY > 0 ? 0.92 : 1.08;
|
||||
const newScale = Math.max(0.2, Math.min(2, scale * delta));
|
||||
|
||||
// Zoom toward cursor
|
||||
const rect = container.getBoundingClientRect();
|
||||
const cx = e.clientX - rect.left;
|
||||
const cy = e.clientY - rect.top;
|
||||
panX = cx - (cx - panX) * (newScale / scale);
|
||||
panY = cy - (cy - panY) * (newScale / scale);
|
||||
|
||||
scale = newScale;
|
||||
applyTransform();
|
||||
updateMinimap();
|
||||
}, { passive: false });
|
||||
|
||||
function applyTransform() {
|
||||
canvas.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||
}
|
||||
|
||||
function zoomIn() { scale = Math.min(2, scale * 1.2); applyTransform(); updateMinimap(); }
|
||||
function zoomOut() { scale = Math.max(0.2, scale * 0.8); applyTransform(); updateMinimap(); }
|
||||
|
||||
function resetView() {
|
||||
scale = 0.85;
|
||||
panX = 50;
|
||||
panY = 20;
|
||||
applyTransform();
|
||||
updateMinimap();
|
||||
}
|
||||
|
||||
function toggleLabels() {
|
||||
showLabels = !showLabels;
|
||||
drawRelationships();
|
||||
}
|
||||
|
||||
// ─── Minimap ───
|
||||
const minimapCanvas = document.getElementById('minimap-canvas');
|
||||
const mCtx = minimapCanvas.getContext('2d');
|
||||
|
||||
function updateMinimap() {
|
||||
const mw = 180, mh = 120;
|
||||
minimapCanvas.width = mw * 2;
|
||||
minimapCanvas.height = mh * 2;
|
||||
minimapCanvas.style.width = mw + 'px';
|
||||
minimapCanvas.style.height = mh + 'px';
|
||||
mCtx.scale(2, 2);
|
||||
|
||||
mCtx.clearRect(0, 0, mw, mh);
|
||||
mCtx.fillStyle = '#0f1117';
|
||||
mCtx.fillRect(0, 0, mw, mh);
|
||||
|
||||
// Find bounds
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const [name, pos] of Object.entries(positions)) {
|
||||
const el = tableElements[name];
|
||||
if (!el) continue;
|
||||
minX = Math.min(minX, pos.x);
|
||||
minY = Math.min(minY, pos.y);
|
||||
maxX = Math.max(maxX, pos.x + el.offsetWidth);
|
||||
maxY = Math.max(maxY, pos.y + el.offsetHeight);
|
||||
}
|
||||
|
||||
const padding = 40;
|
||||
minX -= padding; minY -= padding;
|
||||
maxX += padding; maxY += padding;
|
||||
const scaleX = mw / (maxX - minX);
|
||||
const scaleY = mh / (maxY - minY);
|
||||
const ms = Math.min(scaleX, scaleY);
|
||||
|
||||
// Draw tables
|
||||
for (const [name, pos] of Object.entries(positions)) {
|
||||
const el = tableElements[name];
|
||||
if (!el) continue;
|
||||
const color = schema.tables[name]?.color || '#6ea8fe';
|
||||
mCtx.fillStyle = color + '60';
|
||||
mCtx.fillRect(
|
||||
(pos.x - minX) * ms,
|
||||
(pos.y - minY) * ms,
|
||||
el.offsetWidth * ms,
|
||||
el.offsetHeight * ms
|
||||
);
|
||||
}
|
||||
|
||||
// Draw viewport
|
||||
const rect = container.getBoundingClientRect();
|
||||
const vx = (-panX / scale - minX) * ms;
|
||||
const vy = (-panY / scale - minY) * ms;
|
||||
const vw = (rect.width / scale) * ms;
|
||||
const vh = (rect.height / scale) * ms;
|
||||
mCtx.strokeStyle = '#6ea8fe';
|
||||
mCtx.lineWidth = 1;
|
||||
mCtx.strokeRect(vx, vy, vw, vh);
|
||||
}
|
||||
|
||||
// ─── Init ───
|
||||
renderTables();
|
||||
drawRelationships();
|
||||
applyTransform();
|
||||
setTimeout(updateMinimap, 100);
|
||||
window.addEventListener('resize', updateMinimap);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -354,6 +354,7 @@
|
||||
<a href="ui.html" class="btn">UI Documentation</a>
|
||||
<a href="um.html" class="btn">User Manual Documentation</a>
|
||||
<a href="technical.html" class="btn">Technical Documentation</a>
|
||||
<a href="architecture.html" class="btn" style="background:#111827;">Architecture Overview</a>
|
||||
<a href="p6video.html" class="btn" style="background:#7816e9;">🎬 P6 Presentation Video</a>
|
||||
|
||||
</div>
|
||||
|
||||
+1511
-6
File diff suppressed because it is too large
Load Diff
+310
-52
@@ -193,7 +193,6 @@
|
||||
}
|
||||
|
||||
.callout{
|
||||
border-left:4px solid var(--primary);
|
||||
background:#eff6ff;
|
||||
padding:1rem;
|
||||
border-radius:12px;
|
||||
@@ -201,10 +200,13 @@
|
||||
}
|
||||
|
||||
.callout.success{
|
||||
border-left-color:var(--success);
|
||||
background:#f0fdf4;
|
||||
}
|
||||
|
||||
.callout.warn{
|
||||
background:#fffbeb;
|
||||
}
|
||||
|
||||
code{
|
||||
background:#eef2ff;
|
||||
padding:.15rem .4rem;
|
||||
@@ -244,6 +246,29 @@
|
||||
margin-bottom:.25rem;
|
||||
}
|
||||
|
||||
.role-table{
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
margin-top:1rem;
|
||||
font-size:.95rem;
|
||||
}
|
||||
|
||||
.role-table th,
|
||||
.role-table td{
|
||||
text-align:left;
|
||||
padding:.5rem .75rem;
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
|
||||
.role-table th{
|
||||
background:#f3f4f6;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.role-table tr:nth-child(even) td{
|
||||
background:#f9fafb;
|
||||
}
|
||||
|
||||
.footer{
|
||||
text-align:center;
|
||||
color:var(--muted);
|
||||
@@ -263,8 +288,8 @@
|
||||
<header>
|
||||
<h1>WIN Student Goal Tracker</h1>
|
||||
<p>
|
||||
User manual for teachers and program staff using the WIN platform to manage
|
||||
students, goals, benchmarks, and progress events.
|
||||
User manual for teachers, program staff, and administrators using the WIN
|
||||
platform to manage students, goals, benchmarks, and progress events.
|
||||
</p>
|
||||
|
||||
<div class="hero-actions">
|
||||
@@ -288,24 +313,28 @@
|
||||
<h2>Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#overview">1. Overview</a></li>
|
||||
<li><a href="#access">2. Accessing the Application</a></li>
|
||||
<li><a href="#program">3. Selecting a Program</a></li>
|
||||
<li><a href="#dashboard">4. Student Dashboard</a></li>
|
||||
<li><a href="#add-student">5. Adding & Editing Students</a></li>
|
||||
<li><a href="#goals">6. Managing Goals</a></li>
|
||||
<li><a href="#benchmarks">7. Managing Benchmarks</a></li>
|
||||
<li><a href="#events">8. Recording Progress Events</a></li>
|
||||
<li><a href="#deleting">9. Deleting Records</a></li>
|
||||
<li><a href="#reports">10. Reports</a></li>
|
||||
<li><a href="#mobile">11. Mobile Experience</a></li>
|
||||
<li><a href="#workflow">12. Typical Workflow</a></li>
|
||||
<li><a href="#tips">13. Usage Notes</a></li>
|
||||
<li><a href="#roles">2. User Roles</a></li>
|
||||
<li><a href="#registration">3. Registering a New District</a></li>
|
||||
<li><a href="#access">4. Accessing the Application</a></li>
|
||||
<li><a href="#program">5. Selecting a Program</a></li>
|
||||
<li><a href="#dashboard">6. Student Dashboard</a></li>
|
||||
<li><a href="#add-student">7. Adding & Editing Students</a></li>
|
||||
<li><a href="#goals">8. Managing Goals</a></li>
|
||||
<li><a href="#benchmarks">9. Managing Benchmarks</a></li>
|
||||
<li><a href="#events">10. Recording Progress Events</a></li>
|
||||
<li><a href="#deleting">11. Deleting Records</a></li>
|
||||
<li><a href="#reports">12. Reports</a></li>
|
||||
<li><a href="#admin">13. Administration</a></li>
|
||||
<li><a href="#mobile">14. Mobile Experience</a></li>
|
||||
<li><a href="#workflow">15. Typical Workflow</a></li>
|
||||
<li><a href="#tips">16. Usage Notes</a></li>
|
||||
</ul>
|
||||
|
||||
<p class="small">
|
||||
Demo login:<br />
|
||||
Prototype demo login:<br />
|
||||
<code>opelly@gmail.com</code><br />
|
||||
<code>1234</code>
|
||||
<code>1234</code><br />
|
||||
(Teacher role — for demo use only)
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
@@ -340,14 +369,128 @@
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
This manual covers the current application workflow as implemented
|
||||
in the deployed prototype. Features relating to district registration
|
||||
and administration are documented separately.
|
||||
This manual covers teacher and program staff workflows as well as
|
||||
district administration features including program management and
|
||||
user provisioning.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="roles" class="card">
|
||||
<h2 class="section-title">2. User Roles</h2>
|
||||
<p class="muted">
|
||||
Every user account is assigned a role when added to a program. The
|
||||
role determines which actions that user can perform. There are five
|
||||
roles in the system.
|
||||
</p>
|
||||
|
||||
<table class="role-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Description</th>
|
||||
<th>Key Permissions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Teacher</strong></td>
|
||||
<td>Standard instructional staff member</td>
|
||||
<td>
|
||||
Full access to their own students — create, edit, and delete
|
||||
students, goals, benchmarks, and progress events. Can run
|
||||
reports and use AI suggestions.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Paraeducator</strong></td>
|
||||
<td>Support staff assisting teachers</td>
|
||||
<td>
|
||||
Can view students and log progress events. Cannot create or
|
||||
delete students, goals, or benchmarks, and cannot run reports
|
||||
or use AI suggestions.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Program Admin</strong></td>
|
||||
<td>Staff member who manages an entire program</td>
|
||||
<td>
|
||||
Same as Teacher but scoped across all students in the program,
|
||||
not just their own. Can run reports.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>District Admin</strong></td>
|
||||
<td>Administrator who manages a school district</td>
|
||||
<td>
|
||||
All Program Admin abilities plus access to the Administration
|
||||
panel. Can create and edit programs and add users to any
|
||||
program within the district.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Super Admin</strong></td>
|
||||
<td>System-level administrator</td>
|
||||
<td>
|
||||
Full access across all districts and programs. Can assign any
|
||||
role, including Super Admin. Reserved for platform operators.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="callout">
|
||||
A user's role is set when their account is created by a District
|
||||
Admin. If you are unsure of your role, contact your district
|
||||
administrator.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="registration" class="card">
|
||||
<h2 class="section-title">3. Registering a New District</h2>
|
||||
<p class="muted">
|
||||
If your school district is not yet set up in the system, a district
|
||||
administrator can self-register to create the district account, an
|
||||
initial program, and their own login in one step.
|
||||
</p>
|
||||
|
||||
<div class="callout warn">
|
||||
Registration creates a new, independent district. If your district
|
||||
already exists, do not register again — contact your existing
|
||||
district administrator to have a user account created for you.
|
||||
</div>
|
||||
|
||||
<h3>How to register</h3>
|
||||
<ol>
|
||||
<li>
|
||||
Go to <code>https://win.opelly.me/register</code> or click
|
||||
<strong>Register</strong> from the login page.
|
||||
</li>
|
||||
<li>
|
||||
Fill in <strong>Your Details</strong>: name, email address, and
|
||||
password. This becomes your login.
|
||||
</li>
|
||||
<li>
|
||||
Fill in <strong>District Details</strong>: district name and an
|
||||
optional contact email for the district.
|
||||
</li>
|
||||
<li>
|
||||
Fill in <strong>Your First Program</strong>: the name and optional
|
||||
description of the first program in your district (e.g.
|
||||
"Special Education").
|
||||
</li>
|
||||
<li>Click <strong>Create District & Account</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout success">
|
||||
After registration you will be directed to the login page. Sign in
|
||||
with your new credentials. You will be assigned the District Admin
|
||||
role automatically and can add more programs and users from the
|
||||
Administration panel.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="access" class="card">
|
||||
<h2 class="section-title">2. Accessing the Application</h2>
|
||||
<h2 class="section-title">4. Accessing the Application</h2>
|
||||
<p>
|
||||
To begin, open the login page at
|
||||
<code>https://win.opelly.me/login</code>.
|
||||
@@ -365,15 +508,21 @@
|
||||
After a successful login, the system will take you to the program
|
||||
selection screen.
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
If your account is locked after repeated failed attempts, you will
|
||||
see a message indicating the account is temporarily unavailable.
|
||||
Wait a few minutes before trying again or contact your administrator.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="program" class="card">
|
||||
<h2 class="section-title">3. Selecting a Program</h2>
|
||||
<h2 class="section-title">5. Selecting a Program</h2>
|
||||
<p class="muted">
|
||||
After authentication, the system displays the list of programs
|
||||
available to the logged-in user. Each program card shows the
|
||||
program name, your role (e.g. Teacher, Admin), and whether it is
|
||||
your primary program.
|
||||
program name, your role (e.g. Teacher, District Admin), and whether
|
||||
it is your primary program.
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
@@ -381,10 +530,16 @@
|
||||
<li>Click the program you wish to work in.</li>
|
||||
<li>The application will open the main dashboard for that program.</li>
|
||||
</ol>
|
||||
|
||||
<div class="callout">
|
||||
A user can belong to multiple programs with different roles in each.
|
||||
Selecting a program loads a session scoped to that program — switch
|
||||
programs by logging out and logging back in.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="dashboard" class="card">
|
||||
<h2 class="section-title">4. Student Dashboard</h2>
|
||||
<h2 class="section-title">6. Student Dashboard</h2>
|
||||
<p>
|
||||
The main dashboard displays the students assigned to the current
|
||||
teacher or user.
|
||||
@@ -403,6 +558,10 @@
|
||||
between viewing only your own students and all students in
|
||||
the program.
|
||||
</li>
|
||||
<li>
|
||||
Students may be organized into labeled groups if a Program
|
||||
Admin or District Admin has configured groupings.
|
||||
</li>
|
||||
<li>Click a student to open their goal workspace.</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -410,9 +569,14 @@
|
||||
<h3>Navigation</h3>
|
||||
<ul class="info-list">
|
||||
<li>
|
||||
The sidebar also provides links to <strong>Reports</strong>
|
||||
The sidebar provides links to <strong>Reports</strong>
|
||||
and <strong>Log Out</strong>.
|
||||
</li>
|
||||
<li>
|
||||
District Admins and Super Admins also see an
|
||||
<strong>Admin</strong> link that opens the Administration
|
||||
panel (see <a href="#admin">Section 13</a>).
|
||||
</li>
|
||||
<li>Click the edit icon next to a student to update their name or IEP date.</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -420,10 +584,11 @@
|
||||
</section>
|
||||
|
||||
<section id="add-student" class="card">
|
||||
<h2 class="section-title">5. Adding & Editing Students</h2>
|
||||
<h2 class="section-title">7. Adding & Editing Students</h2>
|
||||
<p class="muted">
|
||||
Teachers can add a new student record directly from the dashboard,
|
||||
or edit an existing student's information.
|
||||
Teachers and Program Admins can add a new student record directly
|
||||
from the dashboard, or edit an existing student's information.
|
||||
Paraeducators cannot add or edit students.
|
||||
</p>
|
||||
|
||||
<h3>Adding a student</h3>
|
||||
@@ -447,7 +612,7 @@
|
||||
</section>
|
||||
|
||||
<section id="goals" class="card">
|
||||
<h2 class="section-title">6. Managing Goals</h2>
|
||||
<h2 class="section-title">8. Managing Goals</h2>
|
||||
<p>
|
||||
Selecting a student opens the goal workspace for that student.
|
||||
Goals are the main tracking objects used to measure student
|
||||
@@ -509,13 +674,18 @@
|
||||
</section>
|
||||
|
||||
<section id="benchmarks" class="card">
|
||||
<h2 class="section-title">7. Managing Benchmarks</h2>
|
||||
<h2 class="section-title">9. Managing Benchmarks</h2>
|
||||
<p>
|
||||
Benchmarks break a goal into smaller, measurable steps. This makes it
|
||||
easier to see incremental progress over time. Each benchmark has a
|
||||
full description and an optional short name used for compact display.
|
||||
</p>
|
||||
|
||||
<p class="muted">
|
||||
Teachers and Program Admins can add and edit benchmarks.
|
||||
Paraeducators can view benchmarks but cannot create or modify them.
|
||||
</p>
|
||||
|
||||
<h3>To add a benchmark</h3>
|
||||
<ol>
|
||||
<li>Select the goal tab for the relevant goal.</li>
|
||||
@@ -530,7 +700,8 @@
|
||||
When adding a new benchmark, you can click the
|
||||
<strong>✦ Suggest with AI</strong> button. The system will analyze
|
||||
the student's goal and generate a recommended benchmark that you
|
||||
can accept, modify, or discard before saving.
|
||||
can accept, modify, or discard before saving. This feature is
|
||||
available to Teachers and Program Admins only.
|
||||
</p>
|
||||
|
||||
<h3>Editing a benchmark</h3>
|
||||
@@ -545,10 +716,11 @@
|
||||
</section>
|
||||
|
||||
<section id="events" class="card">
|
||||
<h2 class="section-title">8. Recording Progress Events</h2>
|
||||
<h2 class="section-title">10. Recording Progress Events</h2>
|
||||
<p class="muted">
|
||||
Progress events document meaningful activities, milestones, or notes
|
||||
related to a student's goal.
|
||||
related to a student's goal. All roles (including Paraeducators) can
|
||||
log progress events.
|
||||
</p>
|
||||
|
||||
<h3>To log a progress event</h3>
|
||||
@@ -595,9 +767,10 @@
|
||||
</section>
|
||||
|
||||
<section id="deleting" class="card">
|
||||
<h2 class="section-title">9. Deleting Records</h2>
|
||||
<h2 class="section-title">11. Deleting Records</h2>
|
||||
<p class="muted">
|
||||
Students, goals, benchmarks, and progress events can all be deleted.
|
||||
Students, goals, benchmarks, and progress events can all be deleted
|
||||
by Teachers and Program Admins. Paraeducators cannot delete records.
|
||||
Deletions are permanent and cannot be undone.
|
||||
</p>
|
||||
|
||||
@@ -632,11 +805,12 @@
|
||||
</section>
|
||||
|
||||
<section id="reports" class="card">
|
||||
<h2 class="section-title">10. Reports</h2>
|
||||
<h2 class="section-title">12. Reports</h2>
|
||||
<p class="muted">
|
||||
The Reports section provides tools for extracting and summarizing
|
||||
student progress data. Access it from the <strong>Reports</strong>
|
||||
link in the sidebar.
|
||||
link in the sidebar. Reports are available to Teachers and Program
|
||||
Admins. Paraeducators do not have access to the Reports section.
|
||||
</p>
|
||||
|
||||
<h3>Student Progress Report</h3>
|
||||
@@ -665,8 +839,73 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="admin" class="card">
|
||||
<h2 class="section-title">13. Administration</h2>
|
||||
<p class="muted">
|
||||
The Administration panel is available to District Admins and Super
|
||||
Admins. It is accessed via the <strong>Admin</strong> link in the
|
||||
sidebar, which is only visible to users with those roles.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The Admin panel has two tabs: <strong>Programs</strong> and
|
||||
<strong>Users</strong>.
|
||||
</p>
|
||||
|
||||
<h3>Programs tab</h3>
|
||||
<p>
|
||||
Lists all programs in your district. District Admins can create new
|
||||
programs and edit existing ones.
|
||||
</p>
|
||||
|
||||
<h4>Adding a program</h4>
|
||||
<ol>
|
||||
<li>Click <strong>+ Add Program</strong>.</li>
|
||||
<li>Enter a <strong>Program Name</strong> (required) and an optional <strong>Description</strong>.</li>
|
||||
<li>Click <strong>Create</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<h4>Editing a program</h4>
|
||||
<ol>
|
||||
<li>Click <strong>Edit</strong> next to the program.</li>
|
||||
<li>Update the name or description.</li>
|
||||
<li>Click <strong>Save</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<h3>Users tab</h3>
|
||||
<p>
|
||||
Lists all users across all programs in your district, showing each
|
||||
user's name, email, role, and assigned program.
|
||||
</p>
|
||||
|
||||
<h4>Adding a user</h4>
|
||||
<ol>
|
||||
<li>Click <strong>+ Add User</strong>.</li>
|
||||
<li>Enter the user's <strong>Name</strong>, <strong>Email</strong>, and a temporary <strong>Password</strong>.</li>
|
||||
<li>Select the <strong>Program</strong> to assign the user to.</li>
|
||||
<li>Select the user's <strong>Role</strong> (see <a href="#roles">Section 2</a> for role descriptions).</li>
|
||||
<li>Click <strong>Create User</strong>.</li>
|
||||
</ol>
|
||||
|
||||
<p>
|
||||
The new user can immediately log in with the email and password you
|
||||
provided. Share the credentials with them securely.
|
||||
</p>
|
||||
|
||||
<div class="callout">
|
||||
District Admins cannot assign the Super Admin role. Only a Super
|
||||
Admin can elevate another user to Super Admin.
|
||||
</div>
|
||||
|
||||
<div class="callout warn">
|
||||
There is currently no in-app password reset. If a user forgets their
|
||||
password, a District Admin will need to create a new account or
|
||||
contact the platform operator for assistance.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="mobile" class="card">
|
||||
<h2 class="section-title">11. Mobile Experience</h2>
|
||||
<h2 class="section-title">14. Mobile Experience</h2>
|
||||
<p class="muted">
|
||||
The application automatically detects whether you are using a
|
||||
touch-based mobile device and presents a touch-optimized interface.
|
||||
@@ -691,37 +930,56 @@
|
||||
|
||||
<div class="callout">
|
||||
Some management features — such as adding students, creating goals,
|
||||
editing benchmarks, running reports, and deleting records — are
|
||||
only available in the desktop interface. Use a desktop browser for
|
||||
full administrative access.
|
||||
editing benchmarks, running reports, accessing the Admin panel, and
|
||||
deleting records — are only available in the desktop interface. Use
|
||||
a desktop browser for full administrative access.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="workflow" class="card">
|
||||
<h2 class="section-title">12. Typical Workflow</h2>
|
||||
<h2 class="section-title">15. Typical Workflow</h2>
|
||||
|
||||
<h3>For a Teacher or Program Admin</h3>
|
||||
<ol>
|
||||
<li>Log into the application.</li>
|
||||
<li>Select the appropriate program.</li>
|
||||
<li>Review the list of assigned students.</li>
|
||||
<li>Log into the application and select your program.</li>
|
||||
<li>Review the list of assigned students in the sidebar.</li>
|
||||
<li>Add a student if needed.</li>
|
||||
<li>Open a student record.</li>
|
||||
<li>Review or create goals.</li>
|
||||
<li>Open a student record and review or create goals.</li>
|
||||
<li>Add benchmarks to define milestones (use AI suggestions when helpful).</li>
|
||||
<li>Record progress events as the student advances, linking relevant benchmarks.</li>
|
||||
<li>Run reports as needed for IEP meetings or program reviews.</li>
|
||||
</ol>
|
||||
|
||||
<h3>For a Paraeducator</h3>
|
||||
<ol>
|
||||
<li>Log into the application and select your program.</li>
|
||||
<li>Browse students using the sidebar.</li>
|
||||
<li>Open a student record and select a goal.</li>
|
||||
<li>Switch to the <strong>Progress Events</strong> sub-tab and log a new event.</li>
|
||||
</ol>
|
||||
|
||||
<h3>For a District Admin (initial setup)</h3>
|
||||
<ol>
|
||||
<li>Register at <code>/register</code> to create your district and first program.</li>
|
||||
<li>Log in and open the <strong>Admin</strong> panel from the sidebar.</li>
|
||||
<li>Create additional programs if needed.</li>
|
||||
<li>Add teacher and paraeducator accounts under the Users tab.</li>
|
||||
<li>Share login credentials with each user.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section id="tips" class="card">
|
||||
<h2 class="section-title">13. Usage Notes</h2>
|
||||
<h2 class="section-title">16. Usage Notes</h2>
|
||||
<ul>
|
||||
<li>Keep student names consistent to avoid duplicate entries.</li>
|
||||
<li>Keep student names consistent (use initials or a non-identifying label) to avoid duplicate entries and to protect student privacy.</li>
|
||||
<li>Update progress events regularly so the record stays current.</li>
|
||||
<li>Use benchmarks to make large goals easier to track.</li>
|
||||
<li>Link progress events to benchmarks for richer reporting.</li>
|
||||
<li>Review student cards often to spot inactivity or missing updates.</li>
|
||||
<li>Use the mobile app for quick progress logging in the field.</li>
|
||||
<li>Log out when finished using the system on a shared device.</li>
|
||||
<li>If your login is locked, wait a few minutes and try again, or contact your district administrator.</li>
|
||||
<li>District Admins: share newly created passwords with users through a secure channel and advise them not to share their credentials.</li>
|
||||
</ul>
|
||||
|
||||
<a href="index.html" class="btn btn-primary back-link">Back to Project Home</a>
|
||||
@@ -731,7 +989,7 @@
|
||||
</main>
|
||||
|
||||
<div class="footer">
|
||||
© 2026 WIN Student Goal Tracker.
|
||||
© 2026 WIN Student Goal Tracker.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user