syuanteng315 commited on
Commit
666f0cd
·
verified ·
1 Parent(s): 0e04128

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +145 -106
index.html CHANGED
@@ -192,6 +192,7 @@
192
  const [activeTeacherSession, setActiveTeacherSession] = useState("MORNING");
193
  const [selectedReportDay, setSelectedReportDay] = useState(() => { const d = new Date().getDay(); const idx = d === 0 || d === 6 ? 2 : d - 1; return DAYS[idx] || DAYS[2]; });
194
  const [isGenerating, setIsGenerating] = useState(false);
 
195
  const [hideFormLevel, setHideFormLevel] = useState(false);
196
  const fileInputRef = useRef(null);
197
 
@@ -523,12 +524,71 @@
523
  reader.readAsText(file);
524
  };
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  const generateTimetable = async () => {
527
  if (!profile || profile.role !== ROLES.HEAD_PREFECT) return;
528
- if (!user || !user.email) {
529
- showToast("You must be signed in with Gmail to generate and email the timetable.", "error");
530
- return;
531
- }
532
  setIsGenerating(true);
533
  try {
534
  const deleteOps = [];
@@ -539,13 +599,13 @@
539
  const prefects = users.filter((u) => u.role === ROLES.PREFECT || u.role === ROLES.HEAD_PREFECT);
540
  if (prefects.length === 0) {
541
  showToast("No prefects registered to assign!", "error");
542
- setIsGenerating(false);
543
  return;
544
  }
545
 
546
  const assignmentCounts = {};
547
  prefects.forEach((p) => { assignmentCounts[p.id] = 0; });
548
  const createOps = [];
 
549
  let unassignedSlots = 0;
550
 
551
  DAYS.forEach((day) => {
@@ -569,19 +629,24 @@
569
  const chosen = candidates[Math.floor(Math.random() * candidates.length)];
570
  assignmentCounts[chosen.id]++;
571
  usedThisDay.add(chosen.id);
 
572
  const newRef = db.collection("shifts").doc();
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  createOps.push((batch) => {
574
- batch.set(newRef, {
575
- day,
576
- sessionId: session.id,
577
- sessionName: session.name,
578
- locationId: loc.id,
579
- locationName: loc.name,
580
- userId: chosen.id,
581
- userName: chosen.name,
582
- status: "pending",
583
- swapStatus: "none"
584
- });
585
  });
586
  });
587
  });
@@ -589,103 +654,71 @@
589
 
590
  if (createOps.length === 0) {
591
  showToast("Not enough prefects to fill any slots.", "error");
592
- setIsGenerating(false);
593
  return;
594
  }
595
- await commitInBatches(createOps);
596
 
597
- // ===== 0-cost PDF + Gmail workflow =====
598
- // The browser cannot silently attach a local file to Gmail.
599
- // We therefore: generate/download the PDF, then open a pre-filled Gmail compose window.
600
- const emailList = prefects.map((p) => p.email).filter((e) => e && e.includes("@"));
601
- const generatedAt = new Date();
602
- const fileName = "Prefect_Duty_Timetable_" + generatedAt.toISOString().slice(0, 10) + ".pdf";
603
-
604
- try {
605
- const jsPDF = window.jspdf && window.jspdf.jsPDF;
606
- if (!jsPDF) throw new Error("PDF library did not load. Please check your internet connection and try again.");
607
 
608
- const doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
609
- doc.setFont("helvetica", "bold");
610
- doc.setFontSize(18);
611
- doc.text("Prefect Duty Timetable", 14, 16);
612
- doc.setFont("helvetica", "normal");
613
- doc.setFontSize(9);
614
- doc.text("Generated by Prefect Duty Manager", 14, 22);
615
- doc.text("Generated: " + generatedAt.toLocaleString(), 14, 27);
 
 
616
 
617
- const headers = ["Day", "Session", "Location", "Prefect", "Status"];
618
- const rows = shifts
619
- .map((s) => [s.day || "", s.sessionName || "", s.locationName || "", s.userName || "Unassigned", s.status || "pending"])
620
- .sort((a, b) => (String(a[0]) + String(a[1]) + String(a[2])).localeCompare(String(b[0]) + String(b[1]) + String(b[2])));
 
 
621
 
622
- const colX = [14, 43, 92, 160, 232];
623
- const colW = [29, 49, 68, 72, 40];
624
- let y = 35;
625
- const rowH = 7;
 
 
 
 
 
626
 
627
- const drawHeader = () => {
628
- doc.setFont("helvetica", "bold");
629
- doc.setFontSize(8);
630
- headers.forEach((h, i) => doc.text(h, colX[i], y));
631
- doc.line(14, y + 2, 272, y + 2);
632
- y += rowH;
633
- doc.setFont("helvetica", "normal");
634
- };
635
 
636
- drawHeader();
637
- rows.forEach((row) => {
638
- if (y > 190) {
639
- doc.addPage();
640
- y = 15;
641
- drawHeader();
642
- }
643
- doc.setFontSize(7);
644
- row.forEach((value, i) => {
645
- let text = String(value);
646
- const maxChars = i === 0 ? 16 : i === 1 ? 27 : i === 2 ? 34 : i === 3 ? 34 : 12;
647
- if (text.length > maxChars) text = text.slice(0, maxChars - 1) + "…";
648
- doc.text(text, colX[i], y);
649
- });
650
- doc.setDrawColor(220);
651
- doc.line(14, y + 2, 272, y + 2);
652
- doc.setDrawColor(0);
653
- y += rowH;
654
- });
655
 
656
- const extra = unassignedSlots > 0 ? unassignedSlots + " slot(s) left empty (1 duty/person/day)." : "All generated slots were assigned.";
657
- if (y > 190) { doc.addPage(); y = 15; }
658
- doc.setFontSize(8);
659
- doc.text(extra, 14, y + 4);
660
- doc.save(fileName);
661
-
662
- if (emailList.length > 0) {
663
- const subject = encodeURIComponent("Prefect Duty Timetable");
664
- const body = encodeURIComponent(
665
- "Hi Prefects,\n\n" +
666
- "The latest prefect duty timetable has been generated.\n\n" +
667
- "Please download the PDF that was just generated by Prefect Duty Manager and attach it to this email before sending.\n\n" +
668
- "Generated by: " + (user.displayName || user.email) + "\n" +
669
- "Generated: " + generatedAt.toLocaleString() + "\n\n" +
670
- "Thank you!"
671
- );
672
- const to = encodeURIComponent(emailList.join(","));
673
- const gmailUrl = "https://mail.google.com/mail/?view=cm&fs=1&to=" + to + "&su=" + subject + "&body=" + body;
674
- window.open(gmailUrl, "_blank", "noopener,noreferrer");
675
- showToast("PDF downloaded. Gmail compose opened — attach " + fileName + " and press Send.", "success");
676
- } else {
677
- showToast("PDF downloaded as " + fileName + ". No prefect Gmail addresses are stored yet.", "info");
678
- }
679
- } catch (pdfError) {
680
- console.error("PDF/email preparation error:", pdfError);
681
- showToast("Timetable generated, but PDF preparation failed: " + (pdfError.message || "unknown error"), "error");
682
- } finally {
683
- setIsGenerating(false);
684
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
685
  } catch (error) {
686
- console.error("Error generating timetable:", error);
687
- showToast("Failed to generate timetable: " + (error.message || "unknown"), "error");
688
- setIsGenerating(false);
 
689
  }
690
  };
691
 
@@ -1262,10 +1295,16 @@
1262
  )}
1263
  </div>
1264
  {profile.role === ROLES.HEAD_PREFECT && activeTab === "timetable" && (
1265
- <button onClick={generateTimetable} disabled={isGenerating} className="w-full md:w-auto flex items-center justify-center gap-2 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-6 py-2.5 rounded-lg font-bold shadow-md disabled:opacity-70">
1266
- {isGenerating ? <Icon name="refresh-cw" size={18} className="animate-spin" /> : <Icon name="mail" size={18} />}
1267
- {isGenerating ? "Generating..." : "AI Generate & Email"}
1268
- </button>
 
 
 
 
 
 
1269
  )}
1270
  </div>
1271
 
 
192
  const [activeTeacherSession, setActiveTeacherSession] = useState("MORNING");
193
  const [selectedReportDay, setSelectedReportDay] = useState(() => { const d = new Date().getDay(); const idx = d === 0 || d === 6 ? 2 : d - 1; return DAYS[idx] || DAYS[2]; });
194
  const [isGenerating, setIsGenerating] = useState(false);
195
+ const [isEmailing, setIsEmailing] = useState(false);
196
  const [hideFormLevel, setHideFormLevel] = useState(false);
197
  const fileInputRef = useRef(null);
198
 
 
524
  reader.readAsText(file);
525
  };
526
 
527
+ const buildTimetablePdf = (scheduleShifts, generatedAt = new Date()) => {
528
+ const jsPDF = window.jspdf && window.jspdf.jsPDF;
529
+ if (!jsPDF) throw new Error("PDF library did not load. Please refresh the page and try again.");
530
+
531
+ const safeShifts = Array.isArray(scheduleShifts) ? scheduleShifts : [];
532
+ const fileName = "Prefect_Duty_Timetable_" + generatedAt.toISOString().slice(0, 10) + ".pdf";
533
+ const doc = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
534
+
535
+ doc.setFont("helvetica", "bold");
536
+ doc.setFontSize(18);
537
+ doc.text("Prefect Duty Timetable", 14, 16);
538
+ doc.setFont("helvetica", "normal");
539
+ doc.setFontSize(9);
540
+ doc.text("Generated by Prefect Duty Manager", 14, 22);
541
+ doc.text("Generated: " + generatedAt.toLocaleString(), 14, 27);
542
+
543
+ const headers = ["Day", "Session", "Location", "Prefect", "Status"];
544
+ const rows = safeShifts
545
+ .map((s) => [s.day || "", s.sessionName || s.sessionId || "", s.locationName || s.locationId || "", s.userName || "Unassigned", s.status || "pending"])
546
+ .sort((a, b) => (String(a[0]) + String(a[1]) + String(a[2])).localeCompare(String(b[0]) + String(b[1]) + String(b[2])));
547
+
548
+ const colX = [14, 43, 92, 160, 232];
549
+ const rowH = 7;
550
+ let y = 35;
551
+
552
+ const drawHeader = () => {
553
+ doc.setFont("helvetica", "bold");
554
+ doc.setFontSize(8);
555
+ headers.forEach((h, i) => doc.text(h, colX[i], y));
556
+ doc.line(14, y + 2, 272, y + 2);
557
+ y += rowH;
558
+ doc.setFont("helvetica", "normal");
559
+ };
560
+
561
+ drawHeader();
562
+ rows.forEach((row) => {
563
+ if (y > 190) {
564
+ doc.addPage();
565
+ y = 15;
566
+ drawHeader();
567
+ }
568
+ doc.setFontSize(7);
569
+ row.forEach((value, i) => {
570
+ let text = String(value);
571
+ const maxChars = i === 0 ? 16 : i === 1 ? 27 : i === 2 ? 34 : i === 3 ? 34 : 12;
572
+ if (text.length > maxChars) text = text.slice(0, maxChars - 1) + "…";
573
+ doc.text(text, colX[i], y);
574
+ });
575
+ doc.setDrawColor(220);
576
+ doc.line(14, y + 2, 272, y + 2);
577
+ doc.setDrawColor(0);
578
+ y += rowH;
579
+ });
580
+
581
+ if (rows.length === 0) {
582
+ doc.setFontSize(10);
583
+ doc.text("No duty timetable records found.", 14, y + 8);
584
+ }
585
+
586
+ doc.save(fileName);
587
+ return fileName;
588
+ };
589
+
590
  const generateTimetable = async () => {
591
  if (!profile || profile.role !== ROLES.HEAD_PREFECT) return;
 
 
 
 
592
  setIsGenerating(true);
593
  try {
594
  const deleteOps = [];
 
599
  const prefects = users.filter((u) => u.role === ROLES.PREFECT || u.role === ROLES.HEAD_PREFECT);
600
  if (prefects.length === 0) {
601
  showToast("No prefects registered to assign!", "error");
 
602
  return;
603
  }
604
 
605
  const assignmentCounts = {};
606
  prefects.forEach((p) => { assignmentCounts[p.id] = 0; });
607
  const createOps = [];
608
+ const generatedShifts = [];
609
  let unassignedSlots = 0;
610
 
611
  DAYS.forEach((day) => {
 
629
  const chosen = candidates[Math.floor(Math.random() * candidates.length)];
630
  assignmentCounts[chosen.id]++;
631
  usedThisDay.add(chosen.id);
632
+
633
  const newRef = db.collection("shifts").doc();
634
+ const shiftData = {
635
+ id: newRef.id,
636
+ day,
637
+ sessionId: session.id,
638
+ sessionName: session.name,
639
+ locationId: loc.id,
640
+ locationName: loc.name,
641
+ userId: chosen.id,
642
+ userName: chosen.name,
643
+ status: "pending",
644
+ swapStatus: "none"
645
+ };
646
+ generatedShifts.push(shiftData);
647
  createOps.push((batch) => {
648
+ const { id, ...data } = shiftData;
649
+ batch.set(newRef, data);
 
 
 
 
 
 
 
 
 
650
  });
651
  });
652
  });
 
654
 
655
  if (createOps.length === 0) {
656
  showToast("Not enough prefects to fill any slots.", "error");
 
657
  return;
658
  }
 
659
 
660
+ await commitInBatches(createOps);
661
+ showToast("New timetable generated and saved. No email was sent.", "success");
 
 
 
 
 
 
 
 
662
 
663
+ // The schedule generator ONLY generates/saves. Emailing is a separate action.
664
+ // Keep generatedShifts available so the UI can immediately reflect what was created.
665
+ console.info("Generated timetable:", generatedShifts.length, "assignments;", unassignedSlots, "unassigned slots.");
666
+ } catch (error) {
667
+ console.error("Error generating timetable:", error);
668
+ showToast("Failed to generate timetable: " + (error.message || "unknown"), "error");
669
+ } finally {
670
+ setIsGenerating(false);
671
+ }
672
+ };
673
 
674
+ const emailCurrentSchedule = async () => {
675
+ if (!profile || profile.role !== ROLES.HEAD_PREFECT) return;
676
+ if (!user || !user.email) {
677
+ showToast("You must be signed in with Gmail to email the timetable.", "error");
678
+ return;
679
+ }
680
 
681
+ setIsEmailing(true);
682
+ try {
683
+ // Read the current schedule directly from Firestore so this button NEVER regenerates it.
684
+ const snap = await db.collection("shifts").get();
685
+ const currentShifts = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
686
+ if (currentShifts.length === 0) {
687
+ showToast("There is no timetable to email yet. Generate a schedule first.", "error");
688
+ return;
689
+ }
690
 
691
+ const emailList = users
692
+ .filter((u) => u.role === ROLES.PREFECT || u.role === ROLES.HEAD_PREFECT)
693
+ .map((p) => p.email)
694
+ .filter((e) => e && e.includes("@"));
 
 
 
 
695
 
696
+ const generatedAt = new Date();
697
+ const fileName = buildTimetablePdf(currentShifts, generatedAt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
 
699
+ if (emailList.length === 0) {
700
+ showToast("PDF downloaded as " + fileName + ". No prefect Gmail addresses are stored yet.", "info");
701
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  }
703
+
704
+ const subject = encodeURIComponent("Current Prefect Duty Timetable");
705
+ const body = encodeURIComponent(
706
+ "Hi Prefects,\n\n" +
707
+ "Attached is the current prefect duty timetable. This email uses the schedule already saved in Prefect Duty Manager; it does NOT generate a new schedule.\n\n" +
708
+ "Please attach the downloaded PDF: " + fileName + "\n\n" +
709
+ "Sent by: " + (user.displayName || user.email) + "\n" +
710
+ "Prepared: " + generatedAt.toLocaleString() + "\n\n" +
711
+ "Thank you!"
712
+ );
713
+ const to = encodeURIComponent(emailList.join(","));
714
+ const gmailUrl = "https://mail.google.com/mail/?view=cm&fs=1&to=" + to + "&su=" + subject + "&body=" + body;
715
+ window.open(gmailUrl, "_blank", "noopener,noreferrer");
716
+ showToast("Current schedule PDF downloaded. Gmail opened — attach " + fileName + " and press Send.", "success");
717
  } catch (error) {
718
+ console.error("Error emailing current timetable:", error);
719
+ showToast("Failed to prepare email: " + (error.message || "unknown error"), "error");
720
+ } finally {
721
+ setIsEmailing(false);
722
  }
723
  };
724
 
 
1295
  )}
1296
  </div>
1297
  {profile.role === ROLES.HEAD_PREFECT && activeTab === "timetable" && (
1298
+ <div className="w-full md:w-auto flex flex-col sm:flex-row gap-2">
1299
+ <button onClick={generateTimetable} disabled={isGenerating || isEmailing} className="flex-1 md:flex-none flex items-center justify-center gap-2 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white px-5 py-2.5 rounded-lg font-bold shadow-md disabled:opacity-70">
1300
+ {isGenerating ? <Icon name="refresh-cw" size={18} className="animate-spin" /> : <Icon name="sparkles" size={18} />}
1301
+ {isGenerating ? "Generating..." : "Generate Schedule"}
1302
+ </button>
1303
+ <button onClick={emailCurrentSchedule} disabled={isGenerating || isEmailing} className="flex-1 md:flex-none flex items-center justify-center gap-2 bg-white hover:bg-slate-50 text-indigo-700 border border-indigo-200 px-5 py-2.5 rounded-lg font-bold shadow-sm disabled:opacity-70">
1304
+ {isEmailing ? <Icon name="refresh-cw" size={18} className="animate-spin" /> : <Icon name="mail" size={18} />}
1305
+ {isEmailing ? "Preparing Email..." : "Email Current Schedule"}
1306
+ </button>
1307
+ </div>
1308
  )}
1309
  </div>
1310