Last active
June 3, 2026 10:04
-
-
Save mohammedsalem97/3c4641b1e7e7bb3f55d1a832da3e6e7e to your computer and use it in GitHub Desktop.
Sakinah Islamic App — Implementation Progress Dashboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Sakinah Islamic App — Progress Dashboard</title> | |
| <style> | |
| :root { | |
| --bg: #0a0f0d; | |
| --panel: #141a17; | |
| --panel-2: #1c231f; | |
| --gold: #d4af37; | |
| --gold-soft: rgba(212, 175, 55, 0.1); | |
| --green: #88d982; | |
| --green-soft: rgba(136, 217, 130, 0.08); | |
| --amber: #f2ca50; | |
| --amber-soft: rgba(242, 202, 80, 0.08); | |
| --gray: #94a3b8; | |
| --gray-soft: rgba(148, 163, 184, 0.06); | |
| --text: #dfe4e0; | |
| --text-dim: #8a8270; | |
| --border: rgba(255,255,255,0.06); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: "Manrope", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.6; | |
| padding: 32px 16px 64px; | |
| } | |
| .wrap { max-width: 1100px; margin: 0 auto; } | |
| .header { | |
| background: linear-gradient(135deg, #1a201d 0%, #1f2823 100%); | |
| border-radius: 14px; | |
| padding: 28px 30px; | |
| border: 1px solid var(--border); | |
| margin-bottom: 20px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .header::after { | |
| content: ""; | |
| position: absolute; | |
| top: -50%; | |
| right: -10%; | |
| width: 300px; | |
| height: 300px; | |
| background: radial-gradient(circle, var(--gold-soft) 0%, transparent 70%); | |
| pointer-events: none; | |
| } | |
| .header h1 { font-size: 24px; font-weight: 700; color: var(--gold); margin-bottom: 2px; } | |
| .header .sub { color: var(--text-dim); font-size: 13px; } | |
| .header .meta { margin-top: 10px; display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--text-dim); } | |
| .header .meta strong { color: var(--text); font-weight: 500; } | |
| .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 20px; } | |
| .stat { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 16px 18px; text-align: center; | |
| } | |
| .stat .val { font-size: 26px; font-weight: 700; letter-spacing: -0.3px; } | |
| .stat .lbl { font-size: 11px; color: var(--text-dim); margin-top: 2px; } | |
| .stat.gold .val { color: var(--gold); } | |
| .stat.green .val { color: var(--green); } | |
| .stat.amber .val { color: var(--amber); } | |
| .phase-bar-section { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 18px 20px; margin-bottom: 20px; | |
| } | |
| .phase-bar-section h2 { font-size: 15px; font-weight: 600; margin-bottom: 14px; } | |
| .bar-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } | |
| .bar-row .lbl { width: 200px; font-size: 12px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } | |
| .bar-row .bar { flex: 1; height: 6px; background: var(--gray-soft); border-radius: 4px; overflow: hidden; } | |
| .bar-row .bar > div { height: 100%; border-radius: 4px; transition: width 0.5s; } | |
| .bar-row .bar > div.gold { background: linear-gradient(90deg, var(--gold) 0%, #e0bc4a 100%); } | |
| .bar-row .bar > div.green { background: linear-gradient(90deg, var(--green) 0%, #6cbf65 100%); } | |
| .bar-row .pct { width: 50px; text-align: right; font-size: 12px; color: var(--text); font-variant-numeric: tabular-nums; } | |
| .phase-card { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| margin-bottom: 10px; overflow: hidden; | |
| } | |
| .phase-head { | |
| padding: 14px 18px; | |
| display: flex; align-items: center; gap: 10px; | |
| background: var(--panel-2); | |
| cursor: pointer; user-select: none; | |
| transition: background 0.15s; | |
| } | |
| .phase-head:hover { background: #232b26; } | |
| .phase-head .arrow { font-size: 10px; color: var(--text-dim); transition: transform 0.2s; } | |
| .phase-head .arrow.open { transform: rotate(90deg); } | |
| .phase-head .name { font-size: 14px; font-weight: 600; flex: 1; } | |
| .phase-head .name small { color: var(--text-dim); font-weight: 400; font-size: 11px; margin-left: 8px; } | |
| .pill { padding: 2px 10px; border-radius: 8px; font-size: 11px; font-weight: 600; white-space: nowrap; } | |
| .pill.done { background: var(--green-soft); color: var(--green); } | |
| .pill.partial { background: var(--amber-soft); color: var(--amber); } | |
| .pill.pending { background: var(--gray-soft); color: var(--gray); } | |
| .pill.zero { background: var(--gray-soft); color: #666; } | |
| .task-table { width: 100%; border-collapse: collapse; display: none; } | |
| .task-table.open { display: table; } | |
| .task-table th { | |
| padding: 8px 14px; border-bottom: 1px solid var(--border); | |
| font-size: 11px; font-weight: 600; color: var(--text-dim); | |
| text-align: left; letter-spacing: 0.3px; | |
| background: rgba(255,255,255,0.02); | |
| } | |
| .task-table td { | |
| padding: 8px 14px; border-bottom: 1px solid var(--border); | |
| font-size: 12px; vertical-align: top; | |
| } | |
| .task-table tr:last-child td { border-bottom: 0; } | |
| .task-table td.num { width: 44px; color: var(--text-dim); font-variant-numeric: tabular-nums; white-space: nowrap; } | |
| .task-table td.name { min-width: 160px; } | |
| .task-table td.type { width: 58px; font-size: 11px; color: var(--text-dim); } | |
| .task-table td.hours { width: 36px; color: var(--green); font-weight: 600; text-align: center; } | |
| .task-table td.desc { font-size: 11px; color: var(--text-dim); line-height: 1.5; } | |
| .task-table td.stat { width: 70px; text-align: center; } | |
| .status-badge { | |
| display: inline-block; padding: 1px 8px; border-radius: 6px; | |
| font-size: 11px; font-weight: 500; | |
| } | |
| .status-badge.completed { background: var(--green-soft); color: var(--green); } | |
| .status-badge.pending { background: var(--gray-soft); color: var(--gray); } | |
| .status-badge.in-progress { background: var(--amber-soft); color: var(--amber); } | |
| .status-badge.blocked { background: rgba(255,107,107,0.1); color: #ff6b6b; } | |
| h2.section-title { | |
| font-size: 16px; font-weight: 600; margin: 28px 0 14px; | |
| padding-left: 10px; border-left: 3px solid var(--gold); | |
| } | |
| .completed-summary { | |
| background: var(--panel); border: 1px solid var(--border); border-radius: 10px; | |
| padding: 18px 20px; margin-bottom: 10px; | |
| } | |
| .completed-summary h3 { font-size: 14px; color: var(--green); margin-bottom: 10px; } | |
| .completed-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | |
| .task-card { | |
| background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; | |
| padding: 10px 12px; | |
| } | |
| .task-card .num { font-size: 10px; color: var(--gold); font-weight: 600; } | |
| .task-card .title { font-size: 13px; font-weight: 500; margin: 2px 0 4px; } | |
| .task-card .desc { font-size: 11px; color: var(--text-dim); line-height: 1.4; } | |
| .task-card .meta-row { font-size: 10px; color: var(--text-dim); display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; } | |
| .task-card .tag { background: var(--gold-soft); color: var(--gold); padding: 1px 6px; border-radius: 4px; font-size: 10px; } | |
| footer { margin-top: 28px; padding-top: 16px; border-top: 1px solid var(--border); color: var(--text-dim); font-size: 11px; text-align: center; } | |
| @media (max-width: 720px) { | |
| .stats { grid-template-columns: 1fr 1fr; } | |
| .completed-grid { grid-template-columns: 1fr; } | |
| .bar-row .lbl { width: 120px; font-size: 11px; } | |
| .header { padding: 20px 18px; } | |
| .header h1 { font-size: 20px; } | |
| .task-table th, .task-table td { padding: 6px 8px; } | |
| .task-table td.desc { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <div class="header"> | |
| <h1>سكينة · Sakinah Islamic App</h1> | |
| <p class="sub">Implementation Progress Dashboard — <span id="overallPct">0%</span> complete · <span id="overallCount">0/72</span> tasks</p> | |
| <div class="meta"> | |
| <span><strong>Date:</strong> <span id="currentDate"></span></span> | |
| <span><strong>Phases:</strong> 10 (P0–P9)</span> | |
| <span><strong>Hours:</strong> <span id="hoursDelivered">0</span> / <span id="totalHours">0</span></span> | |
| </div> | |
| </div> | |
| <div class="stats" id="statsRow"></div> | |
| <div class="phase-bar-section"> | |
| <h2>Phase Progress</h2> | |
| <div id="phaseBars"></div> | |
| </div> | |
| <h2 class="section-title">📋 All Tasks by Phase</h2> | |
| <div id="phaseDetails"></div> | |
| <h2 class="section-title">✅ Completed Tasks</h2> | |
| <div class="completed-summary"> | |
| <h3 id="completedSummaryTitle">All 72 tasks completed</h3> | |
| <div class="completed-grid" id="completedGrid"><div style="color:var(--text-dim);font-size:13px;grid-column:1/-1;text-align:center">Loading...</div></div> | |
| </div> | |
| <footer> | |
| Generated <span id="generatedDate"></span> | |
| </footer> | |
| </div> | |
| <script> | |
| const PHASE_NAMES = [ | |
| "Project Init & Architecture", "Core Widgets & Navigation", | |
| "Onboarding & Home Dashboard", "Quran Feature", "Azkar Feature", | |
| "Digital Tasbeeh", "Settings", "Audio Player", "Prayer Times", "Qibla Finder" | |
| ]; | |
| const PHASE_SHORT = ["P0","P1","P2","P3","P4","P5","P6","P7","P8","P9"]; | |
| const PHASE_HOURS = [19.5, 12.5, 20.5, 24, 15, 9.5, 8.5, 12, 9.5, 9.5]; | |
| const TASKS = [ | |
| [0,"0.1","Add dependencies","Setup",1,"Critical","—","Add all required packages: flutter_bloc, get_it, injectable, go_router, dio, flutter_secure_storage, cached_network_image, intl, easy_localization, json_annotation, json_serializable, build_runner, hive, just_audio, audio_service, location, sensors_plus, shimmer.","completed"], | |
| [0,"0.2","Create directory structure","Setup",0.5,"Critical","0.1","Create all folders: lib/config/theme/, lib/config/routes/, lib/config/di/, lib/core/widgets/, lib/core/network/, lib/core/storage/, lib/core/errors/, lib/shared/, and each feature with data/domain/presentation.","completed"], | |
| [0,"0.3","Design tokens & theme","Arch",4,"Critical","0.2","app_colors.dart, app_typography.dart (Manrope + Literata), app_spacing.dart, app_radius.dart, app_elevation.dart, app_theme.dart.","completed"], | |
| [0,"0.4","Dependency injection","Arch",2,"Critical","0.2","injection.dart with GetIt manual registration.","completed"], | |
| [0,"0.5","GoRouter setup","Arch",2,"Critical","0.2","app_router.dart with ShellRoute, bottom nav, SplashScreen.","completed"], | |
| [0,"0.6","Networking layer","Arch",3,"Critical","0.2","Dio client + 4 interceptors (Auth, Language, Logging, Error) + API constants.","completed"], | |
| [0,"0.7","Error handling","Arch",1.5,"Critical","0.2","failures.dart, exceptions.dart, error_handler.dart.","completed"], | |
| [0,"0.8","Storage layer","Arch",1.5,"Critical","0.2","flutter_secure_storage wrapper + SharedPreferences/Hive wrappers.","completed"], | |
| [0,"0.9","Localization","Arch",2,"Critical","0.2","easy_localization + en.json + ar.json.","completed"], | |
| [0,"0.10","Build scripts","Setup",1,"Critical","0.1","build.yaml + build scripts for gen/analyze/format/test.","completed"], | |
| [0,"0.11","main.dart entry point","Setup",1,"Critical","0.3–0.10","Remove counter template. Initialize EasyLocalization + DI + GoRouter + dark theme.","completed"], | |
| [1,"1.1","GlassCard widget","UI",2,"Critical","0.3","Translucent charcoal bg, 20px blur, 1px border, 24px radius. Configurable padding/elevation/onTap.","completed"], | |
| [1,"1.2","AppPrimaryButton","UI",1.5,"Critical","0.3","Solid gold #D4AF37, dark text, 16px radius, loading state, disabled state.","completed"], | |
| [1,"1.3","AppSecondaryButton","UI",1,"Critical","0.3","Ghost style: emerald stroke + 10% fill.","completed"], | |
| [1,"1.4","SectionHeader","UI",0.5,"Critical","0.3","Title left + action button right.","completed"], | |
| [1,"1.5","AppChip","UI",0.5,"Critical","0.3","Pill chip, turquoise bg 15%, for Meccan/Medinan tags.","completed"], | |
| [1,"1.6","AppAppBar","UI",1,"Critical","0.3","Dynamic safe-area, glassmorphism, configurable title/actions.","completed"], | |
| [1,"1.7","State widgets","UI",1.5,"Critical","0.3","3 reusable state widgets with gold/emerald accents.","completed"], | |
| [1,"1.8","ProgressIndicator","UI",0.5,"Critical","0.3","Emerald linear indicator for Azkar + Prayer Times.","completed"], | |
| [1,"1.9","AppBottomNav","UI",2,"Critical","0.3","Floating pill: 4 tabs, backdrop-blur, gold active icons.","completed"], | |
| [1,"1.10","AppShell","Arch",1.5,"Critical","1.9","Scaffold + AppBottomNav wired to GoRouter ShellRoute.","completed"], | |
| [1,"1.11","Wire AppShell","Arch",0.5,"Critical","1.10","Replace placeholder routes with ShellRoute.","completed"], | |
| [2,"2.1","Onboarding domain","Arch",1,"High","0.4","OnboardingRepository interface with isCompleted/setCompleted/reset.","completed"], | |
| [2,"2.2","Onboarding data","Stor",1.5,"High","2.1","RepositoryImpl + LocalDataSource (SharedPreferences).","completed"], | |
| [2,"2.3","Onboarding cubit","Arch",1.5,"High","2.1","OnboardingCubit: states Initial/Loaded/Completed. Events Next/Skip/Complete.","completed"], | |
| [2,"2.4","Onboarding screens","UI",4,"High","2.3","3-slide PageView with glassmorphism cards, dot indicators, animations.","completed"], | |
| [2,"2.5","Onboarding routing","Arch",0.5,"High","2.4","/onboarding route + SplashScreen redirect logic.","completed"], | |
| [2,"2.6","Home domain","Arch",1,"High","0.4","DailyVerse/QuickAccessItem entities + HomeRepository interface.","completed"], | |
| [2,"2.7","Home data","API",2,"High","2.6","RepositoryImpl via DioClient fetching daily verse.","completed"], | |
| [2,"2.8","Home cubit","Arch",2,"High","2.6","HomeCubit: states Initial/Loading/Loaded/Error.","completed"], | |
| [2,"2.9","Home screens","UI",6,"High","2.8","Dashboard with DailyVerseCard, QuickAccessGrid, RecentActivity.","completed"], | |
| [2,"2.10","Home routing","Arch",0.5,"High","2.9","/home as default tab.","completed"], | |
| [3,"3.1","Quran domain","Arch",2,"High","0.4","Surah, Verse, Bookmark, Favorite entities + repository interfaces.","completed"], | |
| [3,"3.2","Quran data (Surahs/Verses)","API",3,"High","3.1","RemoteDataSource via DioClient + JSON serialization.","completed"], | |
| [3,"3.3","Quran data (Bookmarks/Favs)","DB",2,"High","3.1","Hive boxes + BookmarkRepositoryImpl.","completed"], | |
| [3,"3.4","Surah List cubit","Arch",1.5,"High","3.2","SurahListCubit: load all surahs, search by name.","completed"], | |
| [3,"3.5","Surah List screen","UI",5,"High","3.4","Search bar + SurahTile list with gold Arabic name, type chip, verse count.","completed"], | |
| [3,"3.6","Quran Reader cubit","Arch",2,"High","3.2","QuranReaderCubit: load surah, toggle bookmark/favorite.","completed"], | |
| [3,"3.7","Quran Reader screen","UI",8,"High","3.6","AyahWidget list with verse number badges, Arabic text, translation, bookmark/favorite.","completed"], | |
| [3,"3.8","Quran routing","Arch",0.5,"High","3.5,3.7","Routes: /quran, /quran/reader/:surahId, /quran/audio/:surahId.","completed"], | |
| [4,"4.1","Azkar domain","Arch",1.5,"High","0.4","ZikrCategory, Zikr entities + repository interfaces.","completed"], | |
| [4,"4.2","Azkar data","API",2.5,"High","4.1","RemoteDataSource via DioClient.","completed"], | |
| [4,"4.3","Azkar Categories cubit","Arch",1,"High","4.2","AzkarCategoriesCubit: load categories.","completed"], | |
| [4,"4.4","Azkar Categories screen","UI",3,"High","4.3","2-column grid of GlassCard tiles with icon, name, count.","completed"], | |
| [4,"4.5","Azkar Reader cubit","Arch",1.5,"High","4.2","AzkarReaderCubit: load azkar, increment repeat counter.","completed"], | |
| [4,"4.6","Azkar Reader screen","UI",5,"High","4.5","ZikrCard list with Arabic + translation, repeat counter, progress bar.","completed"], | |
| [4,"4.7","Azkar routing","Arch",0.5,"High","4.4,4.6","Routes: /azkar, /azkar/reader/:categoryId.","completed"], | |
| [5,"5.1","Tasbeeh domain","Arch",1,"High","0.4","TasbeehPreset, TasbeehSession entities + repository interface.","completed"], | |
| [5,"5.2","Tasbeeh data","DB",1.5,"High","5.1","Hive boxes + 4 built-in presets.","completed"], | |
| [5,"5.3","Tasbeeh cubit","Arch",1.5,"High","5.2","TasbeehCubit: count, preset selection, session management.","completed"], | |
| [5,"5.4","Tasbeeh screen","UI",5,"High","5.3","Gold circular counter with pulse animation, progress bar, completion screen.","completed"], | |
| [5,"5.5","Tasbeeh routing","Arch",0.5,"High","5.4","Route: /tasbeeh.","completed"], | |
| [6,"6.1","Settings domain","Arch",1,"Med","0.4","AppSettings entity + SettingsRepository interface.","completed"], | |
| [6,"6.2","Settings data","Stor",1.5,"Med","6.1","SharedPreferences implementation.","completed"], | |
| [6,"6.3","Settings cubit","Arch",1.5,"Med","6.2","SettingsCubit: load/update each setting.","completed"], | |
| [6,"6.4","Settings screen","UI",4,"Med","6.3","Grouped sections: Appearance, Notifications, Audio, Prayer, Experience, About.","completed"], | |
| [6,"6.5","Settings routing","Arch",0.5,"Med","6.4","Route: /settings.","completed"], | |
| [7,"7.1","Audio domain","Arch",1.5,"Med","0.4","Reciter, AudioPlayState, AudioTrack entities + AudioPlayerService interface.","completed"], | |
| [7,"7.2","Audio data","API",2,"Med","7.1","Reciter list from API + just_audio wrapper with streams.","completed"], | |
| [7,"7.3","Audio Player cubit","Arch",2,"Med","7.2","AudioPlayerCubit: play/pause/seek/speed/reciter selection.","completed"], | |
| [7,"7.4","Audio Player screen","UI",6,"Med","7.3","Player controls, progress slider, speed chips, reciter selector.","completed"], | |
| [7,"7.5","Audio routing","Arch",0.5,"Med","7.4","Route: /quran/audio/:surahId.","completed"], | |
| [8,"8.1","Prayer Times domain","Arch",1,"Med","0.4","PrayerTime, PrayerDay entities + repository interface.","completed"], | |
| [8,"8.2","Prayer Times data","API",2.5,"Med","8.1","RemoteDataSource via DioClient.","completed"], | |
| [8,"8.3","Prayer Times cubit","Arch",1.5,"Med","8.2","PrayerTimesCubit: load today, countdown timer.","completed"], | |
| [8,"8.4","Prayer Times screen","UI",4,"Med","8.3","Hijri header, next prayer card, 5 prayer rows, live countdown.","completed"], | |
| [8,"8.5","Prayer Times routing","Arch",0.5,"Med","8.4","Route: /prayer-times.","completed"], | |
| [9,"9.1","Qibla domain","Arch",0.5,"Med","0.4","QiblaData entity + calculateBearing() math + repository interface.","completed"], | |
| [9,"9.2","Qibla data","Dev",2,"Med","9.1","sensors_plus gyroscope + location GPS + bearing calc to Kaaba.","completed"], | |
| [9,"9.3","Qibla cubit","Arch",1.5,"Med","9.2","QiblaCubit: start/stop sensor stream, permission handling.","completed"], | |
| [9,"9.4","Qibla screen","UI",5,"Med","9.3","Full-screen compass with rotating ring, cardinal labels, gold arrow, alignment status.","completed"], | |
| [9,"9.5","Qibla routing","Arch",0.5,"Med","9.4","Route: /qibla.","completed"], | |
| ]; | |
| // ─── Computed ─── | |
| function phTasks(p) { return TASKS.filter(t => t[0] === p); } | |
| function phStats(p) { | |
| const pts = phTasks(p); | |
| const total = pts.length; | |
| const done = pts.filter(t => t[8] === 'completed').length; | |
| const pct = total > 0 ? Math.round(done / total * 100) : 0; | |
| return { total, done, pct }; | |
| } | |
| function allStats() { | |
| let total = 0, done = 0, hours = 0; | |
| TASKS.forEach(t => { total++; if (t[8] === 'completed') { done++; hours += t[3]; } }); | |
| const pct = total > 0 ? Math.round(done / total * 100) : 0; | |
| return { total, done, pct, hours, totalHours: PHASE_HOURS.reduce((a,b)=>a+b,0) }; | |
| } | |
| // ─── Render ─── | |
| function renderStats() { | |
| const s = allStats(); | |
| document.getElementById('overallPct').textContent = s.pct + '%'; | |
| document.getElementById('overallCount').textContent = s.done + '/' + s.total; | |
| document.getElementById('hoursDelivered').textContent = s.hours.toFixed(1); | |
| document.getElementById('totalHours').textContent = s.totalHours.toFixed(0); | |
| document.getElementById('statsRow').innerHTML = ` | |
| <div class="stat gold"><div class="val">${s.pct}%</div><div class="lbl">Overall Progress</div></div> | |
| <div class="stat green"><div class="val">${s.done}<span style="color:var(--text-dim);font-size:15px">/${s.total}</span></div><div class="lbl">Tasks Done</div></div> | |
| <div class="stat amber"><div class="val">${s.hours.toFixed(1)}<span style="color:var(--text-dim);font-size:15px">h</span></div><div class="lbl">Hours Delivered</div></div> | |
| <div class="stat"><div class="val">${s.totalHours.toFixed(0)}<span style="color:var(--text-dim);font-size:15px">h</span></div><div class="lbl">Total Planned</div></div> | |
| `; | |
| } | |
| function renderBars() { | |
| let html = ''; | |
| for (let p = 0; p <= 9; p++) { | |
| const pts = phTasks(p); | |
| if (pts.length === 0) continue; | |
| const s = phStats(p); | |
| const isFull = s.done === s.total; | |
| html += '<div class="bar-row">' | |
| + '<div class="lbl">' + PHASE_SHORT[p] + ' ' + PHASE_NAMES[p] + '</div>' | |
| + '<div class="bar"><div class="' + (isFull ? 'green' : 'gold') + '" style="width:' + s.pct + '%"></div></div>' | |
| + '<div class="pct">' + s.done + '/' + s.total + '</div>' | |
| + '</div>'; | |
| } | |
| document.getElementById('phaseBars').innerHTML = html; | |
| } | |
| function renderPhases() { | |
| let html = ''; | |
| for (let p = 0; p <= 9; p++) { | |
| const pts = phTasks(p); | |
| if (pts.length === 0) continue; | |
| const s = phStats(p); | |
| let pillClass = 'done', pillText = '100% · Complete'; | |
| if (s.done === 0) { pillClass = 'zero'; pillText = '0% · Not Started'; } | |
| else if (s.pct < 100) { pillClass = 'partial'; pillText = s.pct + '% · In Progress'; } | |
| html += '<div class="phase-card">' | |
| + '<div class="phase-head" onclick="togglePhase(this)">' | |
| + '<span class="arrow">▶</span>' | |
| + '<span class="name">' + PHASE_SHORT[p] + ' ' + PHASE_NAMES[p] + ' <small>' + s.done + '/' + s.total + ' tasks · ' + s.pct + '%</small></span>' | |
| + '<span class="pill ' + pillClass + '">' + pillText + '</span></div>' | |
| + '<table class="task-table">' | |
| + '<tr><th>#</th><th>Task</th><th>Type</th><th>Hrs</th><th>Description</th><th>Status</th></tr>'; | |
| pts.forEach(function(t) { | |
| var status = t[8]; | |
| html += '<tr>' | |
| + '<td class="num">' + t[1] + '</td>' | |
| + '<td class="name">' + t[2] + '</td>' | |
| + '<td class="type">' + t[4] + '</td>' | |
| + '<td class="hours">' + t[3] + '</td>' | |
| + '<td class="desc">' + t[7] + '</td>' | |
| + '<td class="stat"><span class="status-badge ' + status + '">' + status + '</span></td>' | |
| + '</tr>'; | |
| }); | |
| html += '</table></div>'; | |
| } | |
| document.getElementById('phaseDetails').innerHTML = html; | |
| } | |
| function togglePhase(el) { | |
| el.querySelector('.arrow').classList.toggle('open'); | |
| el.nextElementSibling.classList.toggle('open'); | |
| } | |
| function renderCompleted() { | |
| var grid = document.getElementById('completedGrid'); | |
| var html = ''; | |
| TASKS.forEach(function(t) { | |
| if (t[8] !== 'completed') return; | |
| html += '<div class="task-card">' | |
| + '<div class="num">' + t[1] + ' · ' + PHASE_SHORT[t[0]] + '</div>' | |
| + '<div class="title">' + t[2] + '</div>' | |
| + '<div class="desc">' + t[7] + '</div>' | |
| + '<div class="meta-row"><span class="tag">' + t[4] + '</span><span>' + t[3] + 'h</span></div>' | |
| + '</div>'; | |
| }); | |
| grid.innerHTML = html; | |
| document.getElementById('completedSummaryTitle').textContent = 'All ' + TASKS.length + ' tasks completed'; | |
| } | |
| function renderAll() { | |
| renderStats(); | |
| renderBars(); | |
| renderPhases(); | |
| renderCompleted(); | |
| } | |
| document.getElementById('currentDate').textContent = new Date().toLocaleDateString('en-US', { year:'numeric', month:'long', day:'numeric' }); | |
| document.getElementById('generatedDate').textContent = new Date().toLocaleString(); | |
| renderAll(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment