SMKBK1 commited on
Commit
8d46f82
·
verified ·
1 Parent(s): 68c68a0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +16 -44
index.html CHANGED
@@ -5,24 +5,19 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>E-Buku Kawalan Bersama</title>
7
 
8
- <!-- Tailwind CSS CDN -->
9
  <script src="https://cdn.tailwindcss.com"></script>
10
 
11
- <!-- React & ReactDOM CDN -->
12
  <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
13
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
14
 
15
- <!-- Babel Standalone for inline JSX parsing -->
16
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
17
 
18
- <!-- Inter Font -->
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
22
 
23
  <style>
24
  body { font-family: 'Inter', sans-serif; }
25
- /* Custom scrollbar styling for webkit */
26
  ::-webkit-scrollbar { width: 6px; height: 6px; }
27
  ::-webkit-scrollbar-track { background: #f1f5f9; }
28
  ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
@@ -36,7 +31,6 @@
36
  <script type="text/babel">
37
  const { useState, useEffect, useMemo, useCallback } = React;
38
 
39
- // --- SVG Icon Components for standard browser compatibility ---
40
  const Icon = ({ path, className = "w-5 h-5", viewBox = "0 0 24 24" }) => (
41
  <svg className={className} fill="none" stroke="currentColor" viewBox={viewBox} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
42
  {path}
@@ -60,13 +54,9 @@
60
  const ShieldAlert = (p) => <Icon {...p} path={<><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>} />;
61
  const ArrowRight = (p) => <Icon {...p} path={<><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>} />;
62
 
63
- // --- Constants ---
64
- // 静态数据(老师、科目、学生、考勤)— 你原来的 Web App
65
  const GOOGLE_SHEETS_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbyW8HovLImrWBDRqG_nL50DrotcTuHQbYAm5kW2HNd1_PKLwZPO4l6P9pjGCdk4T4M/exec';
66
 
67
- // 日志数据(老师签到 + 学生出班)— 请把下面换成你部署后的 Logs Web App URL
68
- // 部署步骤见 AppsScript_Logs.gs 文件顶部说明
69
- const LOGS_WEB_APP_URL = 'https://script.google.com/macros/s/YOUR_LOGS_DEPLOYMENT_ID/exec';
70
 
71
  const DEFAULT_CLASSES = [
72
  'PA',
@@ -124,12 +114,11 @@
124
  return null;
125
  };
126
 
127
- // 发送日志到 Google Sheet (Logs)
128
  const postLog = async (payload) => {
129
  try {
130
  await fetch(LOGS_WEB_APP_URL, {
131
  method: 'POST',
132
- mode: 'no-cors', // Apps Script Web App 需要 no-cors 才能成功(会收到 opaque response)
133
  headers: { 'Content-Type': 'text/plain' },
134
  body: JSON.stringify(payload)
135
  });
@@ -140,7 +129,6 @@
140
  }
141
  };
142
 
143
- // --- Main App Component ---
144
  function App() {
145
  const [isOnline, setIsOnline] = useState(false);
146
  const [activeTab, setActiveTab] = useState('class_view');
@@ -153,7 +141,6 @@
153
  const [error, setError] = useState(null);
154
  const [isLoading, setIsLoading] = useState(true);
155
 
156
- // 结构: { "5B": { teacherLogs: { "1": {name, subject} }, studentLogs: { "1": [{id, names, timeOut}] } }, ... }
157
  const [allClassLogs, setAllClassLogs] = useState({});
158
  const [activeSlot, setActiveSlot] = useState(null);
159
  const [isTeacherModalOpen, setIsTeacherModalOpen] = useState(false);
@@ -179,12 +166,10 @@
179
  const today = new Date();
180
  const dateId = `${today.getDate().toString().padStart(2, '0')}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getFullYear()}`;
181
 
182
- // 当前选中班级的日志
183
  const selectedClassData = allClassLogs[selectedClass] || {};
184
  const teacherLogs = selectedClassData.teacherLogs || {};
185
  const studentLogs = selectedClassData.studentLogs || {};
186
 
187
- // 拉取静态数据(老师、科目、学生、考勤)
188
  useEffect(() => {
189
  const fetchStaticData = async () => {
190
  setIsLoading(true);
@@ -209,13 +194,7 @@
209
  fetchStaticData();
210
  }, []);
211
 
212
- // 拉取今日日志(从第二个 Sheet)
213
  const fetchLogs = useCallback(async (showLoading = false) => {
214
- if (LOGS_WEB_APP_URL.includes('YOUR_LOGS_DEPLOYMENT_ID')) {
215
- // 还没填真实 URL,跳过
216
- setIsOnline(false);
217
- return;
218
- }
219
  try {
220
  const url = `${LOGS_WEB_APP_URL}?date=${dateId}&t=${Date.now()}`;
221
  const response = await fetch(url);
@@ -235,7 +214,6 @@
235
 
236
  useEffect(() => {
237
  fetchLogs();
238
- // 每 30 秒自动刷新一次(多设备同步)
239
  const interval = setInterval(() => fetchLogs(), 30000);
240
  return () => clearInterval(interval);
241
  }, [fetchLogs]);
@@ -248,7 +226,6 @@
248
  todaysAttendance = attendanceData[Object.keys(attendanceData)[Object.keys(attendanceData).length - 1]];
249
  }
250
 
251
- // Calculations
252
  const absenteesForClass = todaysAttendance ? (todaysAttendance[selectedClass] || []) : [];
253
  const classStudents = studentsData[selectedClass] || [];
254
  const totalStudents = classStudents.length || 24;
@@ -339,7 +316,6 @@
339
 
340
  const noTeacherClassesCount = classesTeacherStatus.filter(c => c.activeSlot && !c.isRecess && !c.hasTeacher).length;
341
 
342
- // ---------- Mutators (全部改写到 Google Sheets) ----------
343
  const openTeacherModal = (slot) => {
344
  if (slot.id === 'R') return;
345
  setActiveSlot(slot);
@@ -350,14 +326,12 @@
350
  const saveTeacherLog = async () => {
351
  if (!tempTeacher.name) return;
352
 
353
- // 乐观更新本地
354
  const updatedTeacherLogs = { ...teacherLogs, [activeSlot.id]: { ...tempTeacher } };
355
  setAllClassLogs(prev => ({
356
  ...prev,
357
  [selectedClass]: { ...selectedClassData, teacherLogs: updatedTeacherLogs }
358
  }));
359
 
360
- // 写入 Logs Sheet
361
  await postLog({
362
  className: selectedClass,
363
  type: 'teacher',
@@ -390,17 +364,15 @@
390
  const updatedSlotLogs = [...(studentLogs[activeSlot.id] || []), newLog];
391
  const updatedStudentLogs = { ...studentLogs, [activeSlot.id]: updatedSlotLogs };
392
 
393
- // 乐观更新
394
  setAllClassLogs(prev => ({
395
  ...prev,
396
  [selectedClass]: { ...selectedClassData, studentLogs: updatedStudentLogs }
397
  }));
398
 
399
- // 写入 Logs Sheet
400
  await postLog({
401
  className: selectedClass,
402
  type: 'student',
403
- name: '', // 学生出班不需要老师名
404
  period: activeSlot.id,
405
  subject: teacherLogs[activeSlot.id]?.subject || '',
406
  action: 'exit',
@@ -481,7 +453,7 @@
481
  <div className="min-h-screen bg-slate-100 p-2 sm:p-4 md:p-8 text-slate-800 font-sans">
482
  <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-200">
483
 
484
- {/* Main Application Header */}
485
  <div className="bg-indigo-700 text-white p-4 md:p-6 pb-2 md:pb-4 flex flex-col gap-4">
486
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
487
  <div>
@@ -527,7 +499,7 @@
527
  </div>
528
  </div>
529
 
530
- {/* Navigation Bar / Page Switcher Tabs */}
531
  <div className="flex items-center gap-2 border-t border-indigo-600/50 pt-3 mt-1">
532
  <button
533
  onClick={() => setActiveTab('class_view')}
@@ -567,11 +539,11 @@
567
  </div>
568
  )}
569
 
570
- {/* School Summary Tab */}
571
  {activeTab === 'school_summary' && (
572
  <div className="p-4 md:p-6 bg-slate-50 min-h-[600px] space-y-6">
573
 
574
- {/* School Overall Stats */}
575
  <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
576
  <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
577
  <div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Kadar Kehadiran Sekolah</div>
@@ -622,7 +594,7 @@
622
  </div>
623
  </div>
624
 
625
- {/* Live Teacher Presence Monitor */}
626
  <div className="bg-white rounded-xl border border-slate-200 shadow-sm p-4 md:p-6 space-y-4">
627
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-slate-100 pb-4">
628
  <div>
@@ -637,7 +609,7 @@
637
  </p>
638
  </div>
639
 
640
- {/* Filter Buttons */}
641
  <div className="flex flex-wrap items-center gap-2">
642
  <button
643
  onClick={() => setTeacherFilter('all')}
@@ -672,7 +644,7 @@
672
  </div>
673
  </div>
674
 
675
- {/* Alert Notification if missing teachers */}
676
  {noTeacherClassesCount > 0 ? (
677
  <div className="bg-red-50 border border-red-200 text-red-800 p-3.5 rounded-xl flex items-center justify-between gap-3 text-xs md:text-sm font-semibold">
678
  <div className="flex items-center gap-2">
@@ -693,7 +665,7 @@
693
  </div>
694
  )}
695
 
696
- {/* Grid of Class Teacher Statuses */}
697
  <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 pt-2">
698
  {classesTeacherStatus
699
  .filter(cStatus => {
@@ -798,7 +770,7 @@
798
  </div>
799
  )}
800
 
801
- {/* Class View Tab */}
802
  {activeTab === 'class_view' && (
803
  <>
804
  <div className="p-4 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-6 border-b border-slate-100 bg-white">
@@ -1037,7 +1009,7 @@
1037
 
1038
  </div>
1039
 
1040
- {/* Teacher Modal */}
1041
  {isTeacherModalOpen && (
1042
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
1043
  <div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-sm">
@@ -1071,7 +1043,7 @@
1071
  </div>
1072
  )}
1073
 
1074
- {/* Student Modal */}
1075
  {isStudentModalOpen && (
1076
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
1077
  <div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-md h-[85vh] sm:h-auto sm:max-h-[85vh] flex flex-col">
@@ -1116,7 +1088,7 @@
1116
  </div>
1117
  )}
1118
 
1119
- {/* Delete Confirmation Modal */}
1120
  {itemToDelete && (
1121
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
1122
  <div className="bg-white rounded-2xl p-6 w-full max-w-sm text-center">
@@ -1146,4 +1118,4 @@
1146
  root.render(<App />);
1147
  </script>
1148
  </body>
1149
- </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>E-Buku Kawalan Bersama</title>
7
 
 
8
  <script src="https://cdn.tailwindcss.com"></script>
9
 
 
10
  <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
11
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
12
 
 
13
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
14
 
 
15
  <link rel="preconnect" href="https://fonts.googleapis.com">
16
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
18
 
19
  <style>
20
  body { font-family: 'Inter', sans-serif; }
 
21
  ::-webkit-scrollbar { width: 6px; height: 6px; }
22
  ::-webkit-scrollbar-track { background: #f1f5f9; }
23
  ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
 
31
  <script type="text/babel">
32
  const { useState, useEffect, useMemo, useCallback } = React;
33
 
 
34
  const Icon = ({ path, className = "w-5 h-5", viewBox = "0 0 24 24" }) => (
35
  <svg className={className} fill="none" stroke="currentColor" viewBox={viewBox} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
36
  {path}
 
54
  const ShieldAlert = (p) => <Icon {...p} path={<><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></>} />;
55
  const ArrowRight = (p) => <Icon {...p} path={<><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>} />;
56
 
 
 
57
  const GOOGLE_SHEETS_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbyW8HovLImrWBDRqG_nL50DrotcTuHQbYAm5kW2HNd1_PKLwZPO4l6P9pjGCdk4T4M/exec';
58
 
59
+ const LOGS_WEB_APP_URL = 'https://script.google.com/macros/s/AKfycbw8neGdrM0tScM5A2bCK4DamtkA9A_-TvCIgQ8PrcXJPxqEZ5nudr9_FLjp9zyStWBm/exec';
 
 
60
 
61
  const DEFAULT_CLASSES = [
62
  'PA',
 
114
  return null;
115
  };
116
 
 
117
  const postLog = async (payload) => {
118
  try {
119
  await fetch(LOGS_WEB_APP_URL, {
120
  method: 'POST',
121
+ mode: 'no-cors',
122
  headers: { 'Content-Type': 'text/plain' },
123
  body: JSON.stringify(payload)
124
  });
 
129
  }
130
  };
131
 
 
132
  function App() {
133
  const [isOnline, setIsOnline] = useState(false);
134
  const [activeTab, setActiveTab] = useState('class_view');
 
141
  const [error, setError] = useState(null);
142
  const [isLoading, setIsLoading] = useState(true);
143
 
 
144
  const [allClassLogs, setAllClassLogs] = useState({});
145
  const [activeSlot, setActiveSlot] = useState(null);
146
  const [isTeacherModalOpen, setIsTeacherModalOpen] = useState(false);
 
166
  const today = new Date();
167
  const dateId = `${today.getDate().toString().padStart(2, '0')}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getFullYear()}`;
168
 
 
169
  const selectedClassData = allClassLogs[selectedClass] || {};
170
  const teacherLogs = selectedClassData.teacherLogs || {};
171
  const studentLogs = selectedClassData.studentLogs || {};
172
 
 
173
  useEffect(() => {
174
  const fetchStaticData = async () => {
175
  setIsLoading(true);
 
194
  fetchStaticData();
195
  }, []);
196
 
 
197
  const fetchLogs = useCallback(async (showLoading = false) => {
 
 
 
 
 
198
  try {
199
  const url = `${LOGS_WEB_APP_URL}?date=${dateId}&t=${Date.now()}`;
200
  const response = await fetch(url);
 
214
 
215
  useEffect(() => {
216
  fetchLogs();
 
217
  const interval = setInterval(() => fetchLogs(), 30000);
218
  return () => clearInterval(interval);
219
  }, [fetchLogs]);
 
226
  todaysAttendance = attendanceData[Object.keys(attendanceData)[Object.keys(attendanceData).length - 1]];
227
  }
228
 
 
229
  const absenteesForClass = todaysAttendance ? (todaysAttendance[selectedClass] || []) : [];
230
  const classStudents = studentsData[selectedClass] || [];
231
  const totalStudents = classStudents.length || 24;
 
316
 
317
  const noTeacherClassesCount = classesTeacherStatus.filter(c => c.activeSlot && !c.isRecess && !c.hasTeacher).length;
318
 
 
319
  const openTeacherModal = (slot) => {
320
  if (slot.id === 'R') return;
321
  setActiveSlot(slot);
 
326
  const saveTeacherLog = async () => {
327
  if (!tempTeacher.name) return;
328
 
 
329
  const updatedTeacherLogs = { ...teacherLogs, [activeSlot.id]: { ...tempTeacher } };
330
  setAllClassLogs(prev => ({
331
  ...prev,
332
  [selectedClass]: { ...selectedClassData, teacherLogs: updatedTeacherLogs }
333
  }));
334
 
 
335
  await postLog({
336
  className: selectedClass,
337
  type: 'teacher',
 
364
  const updatedSlotLogs = [...(studentLogs[activeSlot.id] || []), newLog];
365
  const updatedStudentLogs = { ...studentLogs, [activeSlot.id]: updatedSlotLogs };
366
 
 
367
  setAllClassLogs(prev => ({
368
  ...prev,
369
  [selectedClass]: { ...selectedClassData, studentLogs: updatedStudentLogs }
370
  }));
371
 
 
372
  await postLog({
373
  className: selectedClass,
374
  type: 'student',
375
+ name: '',
376
  period: activeSlot.id,
377
  subject: teacherLogs[activeSlot.id]?.subject || '',
378
  action: 'exit',
 
453
  <div className="min-h-screen bg-slate-100 p-2 sm:p-4 md:p-8 text-slate-800 font-sans">
454
  <div className="max-w-5xl mx-auto bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-200">
455
 
456
+
457
  <div className="bg-indigo-700 text-white p-4 md:p-6 pb-2 md:pb-4 flex flex-col gap-4">
458
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
459
  <div>
 
499
  </div>
500
  </div>
501
 
502
+
503
  <div className="flex items-center gap-2 border-t border-indigo-600/50 pt-3 mt-1">
504
  <button
505
  onClick={() => setActiveTab('class_view')}
 
539
  </div>
540
  )}
541
 
542
+
543
  {activeTab === 'school_summary' && (
544
  <div className="p-4 md:p-6 bg-slate-50 min-h-[600px] space-y-6">
545
 
546
+
547
  <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
548
  <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
549
  <div className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Kadar Kehadiran Sekolah</div>
 
594
  </div>
595
  </div>
596
 
597
+
598
  <div className="bg-white rounded-xl border border-slate-200 shadow-sm p-4 md:p-6 space-y-4">
599
  <div className="flex flex-col md:flex-row md:items-center justify-between gap-3 border-b border-slate-100 pb-4">
600
  <div>
 
609
  </p>
610
  </div>
611
 
612
+
613
  <div className="flex flex-wrap items-center gap-2">
614
  <button
615
  onClick={() => setTeacherFilter('all')}
 
644
  </div>
645
  </div>
646
 
647
+
648
  {noTeacherClassesCount > 0 ? (
649
  <div className="bg-red-50 border border-red-200 text-red-800 p-3.5 rounded-xl flex items-center justify-between gap-3 text-xs md:text-sm font-semibold">
650
  <div className="flex items-center gap-2">
 
665
  </div>
666
  )}
667
 
668
+
669
  <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 pt-2">
670
  {classesTeacherStatus
671
  .filter(cStatus => {
 
770
  </div>
771
  )}
772
 
773
+
774
  {activeTab === 'class_view' && (
775
  <>
776
  <div className="p-4 md:p-6 grid grid-cols-1 lg:grid-cols-2 gap-6 border-b border-slate-100 bg-white">
 
1009
 
1010
  </div>
1011
 
1012
+
1013
  {isTeacherModalOpen && (
1014
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
1015
  <div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-sm">
 
1043
  </div>
1044
  )}
1045
 
1046
+
1047
  {isStudentModalOpen && (
1048
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-end sm:items-center justify-center p-4 z-50">
1049
  <div className="bg-white rounded-t-2xl sm:rounded-2xl p-6 w-full max-w-md h-[85vh] sm:h-auto sm:max-h-[85vh] flex flex-col">
 
1088
  </div>
1089
  )}
1090
 
1091
+
1092
  {itemToDelete && (
1093
  <div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4 z-50">
1094
  <div className="bg-white rounded-2xl p-6 w-full max-w-sm text-center">
 
1118
  root.render(<App />);
1119
  </script>
1120
  </body>
1121
+ </html>