Skip to content

Commit d1e2ef8

Browse files
author
calvin
committed
feature: added web app files to new directory
1 parent c2ce197 commit d1e2ef8

File tree

8 files changed

+658
-2
lines changed

8 files changed

+658
-2
lines changed

lesson_05/calvinrobinson/README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
## User Story - Daily Medicine Reminder App
22

3-
# As a Medical Care Giver, I want a medicine reminder app to give notifications for when medicines are to be taken
4-
#
3+
## User Story ##
4+
As a Medical Care Giver, I want a medicine reminder app to send me notifications when medicines are due for my clients so that I can ensure timely administration and avoid missed doses. Also so that my clients maintain their medication schedules even when I'm not physically present, improving their treatment adherence and health outcomes.
5+
6+
As a Medical Care Giver, I want to receive alerts when clients miss their medications
7+
So that I can follow up immediately and ensure they take missed doses safely.
8+
9+
As a Medical Care Giver, I want to customize notification messages for each client
10+
So that the reminders are personalized and include any specific instructions relevant to their condition.
11+
12+
## Acceptance Criteria
13+
Given I’ve configured a client’s medication schedule, when a dose time arrives, then I receive a push notification on my device.
14+
15+
Given a notification appears, when I tap “Taken or Missed” then the app logs the administration time against that client’s record.
16+
17+
Given a notification isn’t acknowledged within 10 minutes, when the grace period elapses, then the app escalates with a repeating alert until I mark it as taken.
18+
19+
Given I update a client’s dosage or timing, when I save changes, then future notifications reflect the updated schedule.

lesson_05/calvinrobinson/app.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Simple medicine reminder demo
2+
const STORAGE_KEY = 'medicine_reminder_v1';
3+
4+
const clientForm = document.getElementById('clientForm');
5+
const clientId = document.getElementById('clientId');
6+
const clientName = document.getElementById('clientName');
7+
const medication = document.getElementById('medication');
8+
const dosage = document.getElementById('dosage');
9+
const doseTime = document.getElementById('doseTime');
10+
const addTime = document.getElementById('addTime');
11+
const timesList = document.getElementById('timesList');
12+
const clientsEl = document.getElementById('clients');
13+
const logEl = document.getElementById('log');
14+
15+
const modal = document.getElementById('reminderModal');
16+
const reminderMsg = document.getElementById('reminderMsg');
17+
const takenBtn = document.getElementById('takenBtn');
18+
const missedBtn = document.getElementById('missedBtn');
19+
20+
let store = { clients: [], log: [], pending: [] };
21+
let currentReminder = null; // {clientId, time, timerId, escalateId}
22+
23+
function saveStore() { localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); }
24+
function loadStore() { const raw = localStorage.getItem(STORAGE_KEY); if (raw) store = JSON.parse(raw); }
25+
26+
function requestNotificationPermission() {
27+
if (!('Notification' in window)) return;
28+
if (Notification.permission === 'default') Notification.requestPermission();
29+
}
30+
31+
function formatTime(t) { return t; }
32+
33+
function renderTimes(client) {
34+
// returns HTML for times
35+
return client.times.map(t => `<li>${t} <button data-remove='${t}' class='muted'>Remove</button></li>`).join('')
36+
}
37+
38+
function render() {
39+
// clients
40+
clientsEl.innerHTML = '';
41+
store.clients.forEach(c => {
42+
const li = document.createElement('li');
43+
li.className = 'client-card';
44+
li.innerHTML = `
45+
<div class='client-meta'>
46+
<strong>${c.name}</strong> — ${c.medication}<br>
47+
<small>${c.dosage || ''}</small>
48+
<div>Times: ${c.times.join(', ')}</div>
49+
</div>
50+
<div class='client-actions'>
51+
<button data-edit='${c.id}'>Edit</button>
52+
<button data-delete='${c.id}' class='muted'>Delete</button>
53+
</div>`;
54+
clientsEl.appendChild(li);
55+
});
56+
57+
// log
58+
logEl.innerHTML = '';
59+
store.log.slice().reverse().forEach(entry => {
60+
const li = document.createElement('li');
61+
li.className = 'log-item';
62+
li.textContent = `${new Date(entry.time).toLocaleString()}: ${entry.clientName}${entry.medication}${entry.action}`;
63+
logEl.appendChild(li);
64+
});
65+
}
66+
67+
function addClient(obj) {
68+
store.clients.push(obj);
69+
scheduleClientNotifications(obj);
70+
saveStore();
71+
render();
72+
}
73+
74+
function updateClient(id, patch) {
75+
const idx = store.clients.findIndex(c => c.id === id);
76+
if (idx === -1) return;
77+
// cancel existing scheduled jobs for that client
78+
cancelScheduledForClient(id);
79+
store.clients[idx] = { ...store.clients[idx], ...patch };
80+
scheduleClientNotifications(store.clients[idx]);
81+
saveStore();
82+
render();
83+
}
84+
85+
function removeClient(id) {
86+
cancelScheduledForClient(id);
87+
store.clients = store.clients.filter(c => c.id !== id);
88+
saveStore();
89+
render();
90+
}
91+
92+
function logAction({ clientId, clientName, medication, action }) {
93+
store.log.push({ clientId, clientName, medication, action, time: Date.now() });
94+
saveStore();
95+
render();
96+
}
97+
98+
function scheduleClientNotifications(client) {
99+
// for each time, compute next occurrence and set timeout
100+
client.times.forEach(time => {
101+
const next = nextOccurrenceTodayOrTomorrow(time);
102+
const ms = next - Date.now();
103+
const timerId = setTimeout(() => triggerReminder(client, time), ms);
104+
store.pending.push({ clientId: client.id, time, timerId });
105+
});
106+
}
107+
108+
function cancelScheduledForClient(clientId) {
109+
store.pending.filter(p => p.clientId === clientId).forEach(p => clearTimeout(p.timerId));
110+
store.pending = store.pending.filter(p => p.clientId !== clientId);
111+
}
112+
113+
function nextOccurrenceTodayOrTomorrow(timeStr) {
114+
const [hh, mm] = timeStr.split(':').map(Number);
115+
const now = new Date();
116+
const candidate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hh, mm, 0, 0);
117+
if (candidate.getTime() <= Date.now()) candidate.setDate(candidate.getDate() + 1);
118+
return candidate.getTime();
119+
}
120+
121+
function triggerReminder(client, time) {
122+
const msg = `${client.name}: ${client.medication}${client.dosage || ''}`;
123+
// show notification
124+
if (Notification.permission === 'granted') {
125+
new Notification('Medication due', { body: msg, tag: `${client.id}-${time}` });
126+
}
127+
// show in-app modal
128+
currentReminder = { client, time, escalateId: null };
129+
reminderMsg.textContent = msg;
130+
modal.classList.remove('hidden');
131+
132+
// start escalation after 10 minutes (600k ms): repeat every minute until acknowledged
133+
const grace = 10 * 60 * 1000;
134+
const escalate = setTimeout(() => {
135+
currentReminder.escalateId = setInterval(() => {
136+
if (Notification.permission === 'granted') new Notification('Medication NOT acknowledged', { body: msg });
137+
}, 60 * 1000);
138+
}, grace);
139+
currentReminder.escalateId = escalate;
140+
}
141+
142+
function acknowledgeReminder(action) {
143+
if (!currentReminder) return;
144+
const { client, time } = currentReminder;
145+
logAction({ clientId: client.id, clientName: client.name, medication: client.medication, action });
146+
// clear escalation
147+
if (currentReminder.escalateId) {
148+
clearTimeout(currentReminder.escalateId);
149+
clearInterval(currentReminder.escalateId);
150+
}
151+
currentReminder = null;
152+
modal.classList.add('hidden');
153+
}
154+
155+
// wire modal buttons
156+
takenBtn.addEventListener('click', () => acknowledgeReminder('Taken'));
157+
missedBtn.addEventListener('click', () => acknowledgeReminder('Missed'));
158+
159+
// form handling
160+
addTime.addEventListener('click', () => {
161+
if (!doseTime.value) return;
162+
const li = document.createElement('li');
163+
li.textContent = doseTime.value;
164+
const btn = document.createElement('button'); btn.textContent = 'Remove'; btn.className = 'muted';
165+
btn.addEventListener('click', () => li.remove());
166+
li.appendChild(btn);
167+
timesList.appendChild(li);
168+
doseTime.value = '';
169+
});
170+
171+
clientForm.addEventListener('submit', (e) => {
172+
e.preventDefault();
173+
const times = Array.from(timesList.children).map(li => li.firstChild.textContent.trim());
174+
if (!clientName.value || !medication.value || times.length === 0) {
175+
alert('Please provide name, medication, and at least one time.');
176+
return;
177+
}
178+
const id = clientId.value || `c_${Date.now()}`;
179+
const obj = { id, name: clientName.value.trim(), medication: medication.value.trim(), dosage: dosage.value.trim(), times };
180+
if (clientId.value) updateClient(id, obj); else addClient(obj);
181+
// reset
182+
clientForm.reset(); timesList.innerHTML = '';
183+
});
184+
185+
document.getElementById('clearForm').addEventListener('click', () => { clientForm.reset(); timesList.innerHTML = ''; clientId.value = ''; });
186+
187+
// clients list click handlers (edit/delete)
188+
clientsEl.addEventListener('click', (e) => {
189+
const edit = e.target.closest('[data-edit]');
190+
const del = e.target.closest('[data-delete]');
191+
if (edit) {
192+
const id = edit.getAttribute('data-edit');
193+
const c = store.clients.find(x => x.id === id);
194+
if (!c) return;
195+
clientId.value = c.id; clientName.value = c.name; medication.value = c.medication; dosage.value = c.dosage;
196+
timesList.innerHTML = '';
197+
c.times.forEach(t => { const li = document.createElement('li'); li.textContent = t; const btn = document.createElement('button'); btn.textContent='Remove'; btn.className='muted'; btn.addEventListener('click',()=>li.remove()); li.appendChild(btn); timesList.appendChild(li); });
198+
}
199+
if (del) {
200+
const id = del.getAttribute('data-delete');
201+
if (confirm('Delete client?')) removeClient(id);
202+
}
203+
});
204+
205+
// initialize
206+
loadStore();
207+
requestNotificationPermission();
208+
// schedule existing clients
209+
store.clients.forEach(c => scheduleClientNotifications(c));
210+
render();
211+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width,initial-scale=1">
6+
<title>Daily Medicine Reminder</title>
7+
<link rel="stylesheet" href="style.css">
8+
</head>
9+
<body>
10+
<main class="container">
11+
<h1>Daily Medicine Reminder</h1>
12+
13+
<section class="panel">
14+
<h2>Add / Edit Client Medication</h2>
15+
<form id="clientForm">
16+
<input type="hidden" id="clientId">
17+
<label>
18+
Client name
19+
<input id="clientName" required placeholder="e.g. John Doe">
20+
</label>
21+
<label>
22+
Medication
23+
<input id="medication" required placeholder="e.g. Atenolol">
24+
</label>
25+
<label>
26+
Dosage / Instructions
27+
<input id="dosage" placeholder="e.g. 50mg, with food">
28+
</label>
29+
<div class="times">
30+
<label>
31+
Dose time
32+
<input id="doseTime" type="time">
33+
</label>
34+
<button id="addTime" type="button">Add time</button>
35+
</div>
36+
<ul id="timesList" class="times-list" aria-live="polite"></ul>
37+
38+
<div class="form-actions">
39+
<button type="submit">Save Client</button>
40+
<button id="clearForm" type="button" class="muted">Clear</button>
41+
</div>
42+
</form>
43+
</section>
44+
45+
<section class="panel">
46+
<h2>Clients & Schedules</h2>
47+
<ul id="clients" class="clients-list" aria-live="polite"></ul>
48+
</section>
49+
50+
<section class="panel">
51+
<h2>Administration Log</h2>
52+
<ul id="log" class="log-list" aria-live="polite"></ul>
53+
</section>
54+
55+
<footer class="footer">
56+
<small>This demo uses local browser storage and browser Notifications (permission required).</small>
57+
</footer>
58+
</main>
59+
60+
<!-- Reminder modal (in-app) -->
61+
<div id="reminderModal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="reminderTitle">
62+
<div class="modal-content">
63+
<h3 id="reminderTitle">Medication Reminder</h3>
64+
<p id="reminderMsg"></p>
65+
<div class="modal-actions">
66+
<button id="takenBtn">Taken</button>
67+
<button id="missedBtn" class="danger">Missed</button>
68+
</div>
69+
</div>
70+
</div>
71+
72+
<script src="app.js"></script>
73+
</body>
74+
</html>

lesson_05/calvinrobinson/style.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* Basic layout and components for the medicine reminder demo */
2+
:root{--bg:#f6f8fa;--card:#fff;--accent:#0b5fff;--danger:#dc2626;--muted:#6b7280}
3+
*{box-sizing:border-box;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial}
4+
body{margin:0;padding:28px;background:var(--bg);display:flex;justify-content:center}
5+
.container{max-width:980px;background:var(--card);padding:22px;border-radius:10px;box-shadow:0 8px 30px rgba(2,6,23,0.06)}
6+
h1{margin:0 0 8px}
7+
.panel{margin-top:18px;padding:14px;border-radius:8px;border:1px solid #eef2f7}
8+
label{display:block;margin:8px 0;font-size:14px}
9+
input[type=text],input[type=time],input[type=number],input[type=email]{width:100%;padding:8px;border:1px solid #e6e9ef;border-radius:6px}
10+
.times{display:flex;gap:8px;align-items:center}
11+
.times-list{list-style:none;padding:0;margin:10px 0}
12+
.times-list li{background:#f8fafc;padding:6px 8px;border-radius:6px;margin-bottom:6px;display:flex;justify-content:space-between}
13+
.form-actions{display:flex;gap:8px;margin-top:10px}
14+
button{background:var(--accent);color:#fff;border:none;padding:8px 12px;border-radius:6px;cursor:pointer}
15+
button.muted{background:transparent;color:var(--muted);border:1px solid #e6e9ef}
16+
button.danger{background:var(--danger)}
17+
.clients-list,.log-list{list-style:none;padding:0;margin:0}
18+
.client-card{padding:10px;border-radius:8px;background:#fff;border:1px solid #eef2f7;margin-bottom:8px;display:flex;justify-content:space-between}
19+
.client-meta{max-width:70%}
20+
.client-actions{display:flex;gap:8px}
21+
.log-item{padding:8px;border-radius:6px;background:#f8fafc;margin-bottom:6px}
22+
.footer{margin-top:12px;color:var(--muted);font-size:13px}
23+
.hidden{display:none}
24+
.modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(2,6,23,0.45)}
25+
.modal-content{background:#fff;padding:18px;border-radius:8px;min-width:320px}
26+
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:12px}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## User Story - Daily Medicine Reminder App
2+
3+
## User Story ##
4+
As a Medical Care Giver, I want a medicine reminder app to send me notifications when medicines are due for my clients so that I can ensure timely administration and avoid missed doses. Also so that my clients maintain their medication schedules even when I'm not physically present, improving their treatment adherence and health outcomes.
5+
6+
As a Medical Care Giver, I want to receive alerts when clients miss their medications
7+
So that I can follow up immediately and ensure they take missed doses safely.
8+
9+
As a Medical Care Giver, I want to customize notification messages for each client
10+
So that the reminders are personalized and include any specific instructions relevant to their condition.
11+
12+
## Acceptance Criteria
13+
Given I’ve configured a client’s medication schedule, when a dose time arrives, then I receive a push notification on my device.
14+
15+
Given a notification appears, when I tap “Taken or Missed” then the app logs the administration time against that client’s record.
16+
17+
Given a notification isn’t acknowledged within 10 minutes, when the grace period elapses, then the app escalates with a repeating alert until I mark it as taken.
18+
19+
Given I update a client’s dosage or timing, when I save changes, then future notifications reflect the updated schedule.

0 commit comments

Comments
 (0)