Skip to content

Conversation

@ronryv
Copy link
Collaborator

@ronryv ronryv commented Jan 29, 2026

Add justified absences, comments, recorded status, and Excel export to attendance

Summary

This PR expands the training program attendance page with four features:

  1. Justified absences - Admins can mark missed training days as "justified" (e.g., for sick leave). Justified absences display with a clipboard icon instead of the red missed icon.

  2. Comments - Admins can add comments to any attendance record. A speech bubble icon indicates when a comment exists.

  3. Recorded status - Admins can mark individual student attendance as "recorded" (room/screen recording). Displays with a video camera icon.

  4. Excel export - Admins can export the filtered attendance data to an Excel file. The export includes subcolumns for each training day: Status, Location, Recorded, Delay Reasons, and Comments. Status shows "Missed", "Justified Absent", "On Time", or "Delayed (Xm)".

UI Implementation:

  • Clicking any attendance cell opens a modal to edit justified/recorded/comment
  • Changing justified status requires confirmation to prevent accidental changes
  • The "justified" option only appears for missed entries; "recorded" only appears for non-missed entries
  • Icons update after saving without page reload
  • Combined ranking page also shows the new justified and recorded badges
  • Export button appears above the attendance table and respects current filters

Updates since last revision

  • Added Excel export feature using openpyxl library
  • Added openpyxl>=3.1,<4.0 as a new dependency in pyproject.toml
  • Export respects all current filters (date range, training day types, student tags)

Review & Testing Checklist for Human

  • Migration file issue - The migration file (update_from_1.5.sql) has both CREATE TABLE with the new columns AND ALTER TABLE statements that add them again (lines 666-668 and 701-705). This will cause errors if run on a fresh database. One set should be removed.
  • New dependency - Verify openpyxl is properly installed in your deployment environment after updating dependencies
  • Database migration required - New columns (justified, comment, recorded) were added to ArchivedAttendance. Verify the migration works correctly on your environment.
  • Test the Excel export: apply filters, click "Export to Excel", verify the downloaded file contains only filtered data with correct subcolumns
  • Test the attendance modal: click a cell, verify "justified" checkbox only appears for missed entries, "recorded" only for non-missed

Recommended test plan:

  1. Archive a training day with some missed students and some on-time students
  2. Go to the attendance page, click on a missed entry - verify only "justified" checkbox appears
  3. Click on an on-time entry - verify only "recorded" checkbox appears
  4. Mark a missed entry as justified, add a comment, save
  5. Mark an on-time entry as recorded, save
  6. Verify icons appear correctly in both attendance and combined ranking pages
  7. Apply a date filter, click "Export to Excel", open the file and verify it only contains filtered training days
  8. Verify the Excel file has proper headers with training day names and subcolumns (Status, Location, Recorded, Delay Reasons, Comments)

Notes

  • The delay time display was improved to show minutes for delays under 1 hour
  • Used single-click + confirmation dialog instead of double-click for the justified toggle
  • Excel export URL passes current filter parameters to ensure exported data matches displayed data

Link to Devin run: https://app.devin.ai/sessions/f79a67e65ba544ad929cf98ca980fbf7
Requested by: Ron Ryvchin (@ronryv)

Summary by CodeRabbit

  • New Features

    • Admins can edit archived attendance via a modal: mark justified, add comments, and mark as recorded.
    • Export attendance and combined rankings to Excel with filters (date range, day types, tags).
  • Style

    • New attendance UI: badges for justified/recorded/comment indicator, modal, and improved table/cell interactions.
  • Bug Fixes

    • Safer defaults to prevent rendering issues when training day types are absent.
  • Chores

    • DB migration adds justified/comment/recorded fields; Excel export support added.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough

Walkthrough

Adds three fields (justified, comment, recorded) to ArchivedAttendance with migration, POST handler to update them, Excel export handlers for attendance and combined rankings, client-side modal UI/JS to edit/save attendance, template/badge updates, CSS for modal/badges, and openpyxl dependency.

Changes

Cohort / File(s) Summary
DB model & migration
cms/db/archived_attendance.py, cmscontrib/updaters/update_from_1.5.sql
Adds justified (Boolean NOT NULL DEFAULT false), comment (nullable text), and recorded (Boolean NOT NULL DEFAULT false) to the ArchivedAttendance model and DB migration.
Backend handlers & routing
cms/server/admin/handlers/archive.py, cms/server/admin/handlers/__init__.py
Adds UpdateAttendanceHandler (POST) to validate and update attendance fields, ExportAttendanceHandler and ExportCombinedRankingHandler to generate XLSX exports; exposes new handlers in routing/exports.
Templates & client JS
cms/server/admin/templates/training_program_attendance.html, cms/server/admin/templates/training_program_combined_ranking.html, cms/server/admin/templates/training_program_training_days.html
Adds per-cell data attributes, clickable cells opening an Attendance Edit modal, modal HTML/JS (openAttendanceModal, closeAttendanceModal, saveAttendance, updateAttendanceCell), optimistic UI updates, and badges for justified/recorded/comment.
Styling
cms/server/admin/static/aws_tp_styles.css
Adds comprehensive attendance badge and modal styles (note: large duplicated CSS blocks present).
Excel generation & deps
pyproject.toml
Adds openpyxl>=3.1,<4.0 and implements Excel-building helpers for attendance and combined ranking exports in handlers.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser as Client(JS)
    participant Server as API
    participant DB as Database

    User->>Browser: Click attendance cell
    Browser->>Browser: openAttendanceModal(fill fields)
    User->>Browser: Edit fields and Save
    Browser->>Server: POST /training_program/{tp}/attendance/{id} (JSON + CSRF)
    Server->>DB: SELECT archived_attendance BY id
    Server->>Server: validate ownership and input
    Server->>DB: UPDATE archived_attendance (justified, comment, recorded)
    DB-->>Server: commit
    Server-->>Browser: {"success": true}
    Browser->>Browser: update cell UI / close modal
Loading
sequenceDiagram
    participant User
    participant Browser as Client
    participant Server as API
    participant DB as Database
    participant XLSX as ExcelGenerator

    User->>Browser: Click "Export archived attendance"
    Browser->>Server: GET /training_program/{tp}/attendance/export?filters
    Server->>DB: Query program, training days, archived_attendances, students
    Server->>XLSX: Build workbook (rows per student, columns per session, styles)
    XLSX-->>Server: .xlsx binary
    Server-->>Browser: Response with downloadable .xlsx
    Browser->>User: Prompt download
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Command-Master

Poem

🐇
I hop the grid from row to row,
A tick, a note, a ribbon show,
Click, save, a spreadsheet sings,
Rows aligned on tiny springs,
Hooray — attendance ready to go! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main features added: justified absences, comments, recorded status, and Excel export for attendance records.
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from ec41fd7 to bfb8eb6 Compare January 29, 2026 11:24
- Add justified, comment, and proctored fields to ArchivedAttendance model
- Add UpdateAttendanceHandler for updating attendance records via AJAX
- Update attendance page with modal for editing justified/proctored/comment
- Show justified status with green checkmark, proctored with camera icon
- Add comment indicator icon when comments exist
- Update combined ranking page to show justified and proctored badges
- Add CSS styles for new badges

Co-Authored-By: Ron Ryvchin <[email protected]>
@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from bfb8eb6 to 215155e Compare January 29, 2026 11:40
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View issue and 5 additional flags in Devin Review.

Open in Devin Review

@ronryv
Copy link
Collaborator Author

ronryv commented Jan 29, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cms/server/admin/templates/training_program_attendance.html (1)

148-199: Justified status should show a green checkmark.

Right now it uses a memo emoji; the requirement calls for a green checkmark to distinguish justified absences from missed.

🔧 Proposed update
-                    <span>{% if att.justified %}&#128221; Justified{% else %}&#x26D4; Missed{% endif %}</span>
+                    <span>{% if att.justified %}&#9989; Justified{% else %}&#x26D4; Missed{% endif %}</span>
🤖 Fix all issues with AI agents
In `@cms/server/admin/handlers/archive.py`:
- Around line 1142-1157: The handler currently converts inputs with bool(...)
which treats strings like "false" as True and also allows justifying any
attendance; change this to (1) validate and coerce incoming boolean-like values
from data["justified"] and data["recorded"] explicitly (accept True/False,
integers 1/0, and case-insensitive "true"/"false", "yes"/"no") instead of using
bool(...), and (2) enforce a server-side guard before setting
attendance.justified so True may only be applied when the attendance represents
a missed record (e.g. check attendance.status == "missed" or attendance.missed
is True) — otherwise reject or set justified to False; update the same explicit
boolean parsing for attendance.recorded; operate on the existing variables
attendance and data in this handler.

In `@cms/server/admin/templates/training_program_attendance.html`:
- Around line 334-347: The success branch currently does a full page reload
(window.location.reload()) after the fetch in the save handler for attendance
(the block using currentAttendanceId); instead, locate the attendance
cell/element that was edited (e.g., via a data-attendance-id attribute or the
element that opened the modal), update its badge text/classes and any dataset
attributes to reflect the new attendance state from result (and update any
counts/totals shown), then programmatically close the modal (call the modal
close routine used in this file) and optionally trigger any small UI refresh
events — remove the window.location.reload() and perform these in-place DOM
updates in the same .then(result.success) path.

In `@cms/server/admin/templates/training_program_combined_ranking.html`:
- Around line 367-387: The justified badge currently shows the red cross icon in
both template branches; update both occurrences where the condition "attendance
and attendance.status == 'missed' and attendance.justified" renders the
justified-badge to use a green checkmark (e.g. ✅ or HTML entity &#9989;) instead
of the red cross (&#10060;&#128221;), keeping the class "justified-badge" and
the title "Justified absence" intact so both instances render the correct green
check icon.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cms/db/archived_attendance.py`:
- Around line 91-107: Add explicit ORM defaults for the non-nullable Boolean
columns to match DB defaults: import text from sqlalchemy and update the Column
definitions for justified and recorded (the attributes named "justified" and
"recorded" on ArchivedAttendance) to include server_default=text('false') and a
Python-side default (default=False) so SQLAlchemy will not try to insert NULL
when instances are created without those fields; ensure the import statement for
text is added at the top of the file.
🧹 Nitpick comments (4)
cms/server/admin/static/aws_tp_styles.css (1)

1234-1240: Scope the duplicate .location-row styles to avoid global overrides.
The later block overrides the earlier .location-row definition for all contexts. Consider scoping it to the attendance modal.

♻️ Suggested adjustment
-.location-row {
+.attendance-modal .location-row {
     display: flex;
     align-items: center;
     gap: 6px;
     margin-top: 4px;
     flex-wrap: wrap;
 }
cms/server/admin/handlers/__init__.py (1)

210-216: Align UpdateAttendanceHandler with the try_commit error-handling convention.
Before exposing this handler, please remove the explicit error response on try_commit failure and rely on centralized handling.

Based on learnings: "do not emit explicit error notifications when try_commit() fails. Rely on centralized error handling and logging instead."

cms/server/admin/handlers/archive.py (1)

1124-1124: Remove redundant import.

json is already imported at the module level (line 24). This local import is unnecessary.

Suggested fix
     def post(self, training_program_id: str, attendance_id: str):
         """Update an attendance record's justified status, comment, and/or recorded."""
-        import json
-
         training_program = self.safe_get_item(TrainingProgram, training_program_id)
cms/server/admin/templates/training_program_attendance.html (1)

148-149: Consider using tojson filter for the comment data attribute.

While Jinja2 auto-escapes HTML entities, comments containing quotes, newlines, or other special characters could potentially break the HTML attribute parsing. Using |tojson ensures proper encoding for embedding in attributes.

Suggested fix
-            <td data-value="{{ data_value }}" class="attendance-cell" data-attendance-id="{{ att.id }}" data-status="{{ att.status }}" data-justified="{{ 'true' if att.justified else 'false' }}" data-recorded="{{ 'true' if att.recorded else 'false' }}" data-comment="{{ att.comment or '' }}" onclick="openAttendanceModal(this)">
+            <td data-value="{{ data_value }}" class="attendance-cell" data-attendance-id="{{ att.id }}" data-status="{{ att.status }}" data-justified="{{ 'true' if att.justified else 'false' }}" data-recorded="{{ 'true' if att.recorded else 'false' }}" data-comment="{{ (att.comment or '') | tojson }}" onclick="openAttendanceModal(this)">

Then in JavaScript, parse it:

comment: JSON.parse(ds.comment)

@devin-ai-integration devin-ai-integration bot changed the title Add justified absences, comments, and proctored status to attendance Add justified absences, comments, and recorded status to attendance Jan 29, 2026
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 6 additional flags in Devin Review.

Open in Devin Review

@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from 41320fc to 215155e Compare January 29, 2026 11:59
Without default=False, archiving training days fails with IntegrityError
because the ORM doesn't have default values for these NOT NULL columns.

Co-Authored-By: Ron Ryvchin <[email protected]>
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 8 additional flags in Devin Review.

Open in Devin Review

@devin-ai-integration
Copy link

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 8 additional flags in Devin Review.

Open in Devin Review

@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from 7beb6ab to 5aad17f Compare January 29, 2026 13:13
@ronryv
Copy link
Collaborator Author

ronryv commented Jan 29, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@cms/server/admin/templates/training_program_attendance.html`:
- Around line 303-365: The updateAttendanceCell function updates visual badges
but never updates the numeric sorting key in the data-value attribute, so change
updateAttendanceCell to recompute and set cell.dataset.value whenever
status/justified/recorded/comment change; specifically use the same scoring
logic as the table sorter (e.g., missed => "3", justified missed => "2.5",
present/recorded => appropriate lower value) and assign to cell.dataset.value
after you set cell.dataset.status/justified/recorded/comment so sorting uses the
new value; update the logic around cell.dataset.status, cell.dataset.justified,
cell.dataset.recorded and cell.dataset.comment inside updateAttendanceCell to
compute and write the correct data-value string.
🧹 Nitpick comments (4)
cms/db/archived_attendance.py (1)

92-110: LGTM!

The new columns are correctly defined with server_default=text("false") for the boolean fields, which ensures the database provides default values during INSERT operations. This addresses the previously identified issue where ArchivedAttendance instantiation wasn't passing these fields.

Optional enhancement: Consider adding default=False alongside server_default for Python-side convenience, allowing immediate access to these fields after instantiation (before flush):

justified: bool = Column(
    Boolean,
    nullable=False,
    default=False,
    server_default=text("false"),
)

This is optional since the current implementation works correctly at the database level.

cms/server/admin/static/aws_tp_styles.css (1)

1095-1256: Minor: Consolidate duplicate CSS selectors.

Several selectors in this block conflict with or duplicate earlier definitions:

  1. .recorded-badge (lines 1241-1243) overrides the earlier definition (lines 1091-1093) with !important
  2. .location-row (lines 1245-1251) duplicates the earlier definition (lines 693-699) with different gap and added margin-top, flex-wrap

This fragmentation makes the CSS harder to maintain. Consider consolidating these into single definitions.

📝 Suggested consolidation for .location-row

Remove the duplicate at lines 1245-1251 and update the original definition at lines 693-699:

 .location-row {
     display: flex;
     align-items: center;
-    gap: 4px;
+    gap: 6px;
     font-size: 0.75rem;
     color: var(--tp-text-light);
+    margin-top: 4px;
+    flex-wrap: wrap;
 }
cms/server/admin/handlers/archive.py (1)

1124-1124: Remove redundant import.

json is already imported at the module level (line 24), so this local import is unnecessary.

♻️ Proposed fix
     def post(self, training_program_id: str, attendance_id: str):
         """Update an attendance record's justified status, comment, and/or recorded."""
-        import json
-
         training_program = self.safe_get_item(TrainingProgram, training_program_id)
cms/server/admin/templates/training_program_attendance.html (1)

429-435: Consider adding Escape key support for modal closure.

The modal closes when clicking outside, but keyboard users expect Escape to close modals as well.

⌨️ Add Escape key handler
 // Close modal when clicking outside
 document.addEventListener('click', function(e) {
     var modal = document.getElementById('attendance-modal');
     if (e.target === modal) {
         closeAttendanceModal();
     }
 });
+
+// Close modal with Escape key
+document.addEventListener('keydown', function(e) {
+    if (e.key === 'Escape') {
+        var modal = document.getElementById('attendance-modal');
+        if (modal && modal.style.display === 'flex') {
+            closeAttendanceModal();
+        }
+    }
+});

@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from 5aad17f to f6faf23 Compare January 29, 2026 13:29
@devin-ai-integration devin-ai-integration bot changed the title Add justified absences, comments, and recorded status to attendance Add justified absences, comments, recorded status, and Excel export to attendance Jan 29, 2026
… and filter-based filename

Co-Authored-By: Ron Ryvchin <[email protected]>
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 14 additional flags in Devin Review.

Open in Devin Review

@ronryv ronryv force-pushed the devin/1769650405-attendance-features branch from f30ffc0 to ef5f503 Compare January 29, 2026 14:15
@ronryv
Copy link
Collaborator Author

ronryv commented Jan 29, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View issue and 17 additional flags in Devin Review.

Open in Devin Review

@ronryv
Copy link
Collaborator Author

ronryv commented Jan 29, 2026

@coderabbitai full review

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@cms/server/admin/handlers/archive.py`:
- Around line 136-155: User-controlled cell values can begin with =, +, -, or @
and be interpreted as Excel formulas; add a small sanitizer and apply it
everywhere inputs are written to sheets. Create a helper (e.g.,
sanitize_excel_cell(value) that returns value prefixed with a single quote or
otherwise escapes values starting with one of '=+-@' and use it when writing
student_name and tags_str in excel_write_student_row, when writing delay_reasons
and comment in ExportAttendanceHandler, and when writing task header strings in
ExportCombinedRankingHandler so all exported cells are sanitized before
ws.cell(..., value=...).

In `@cms/server/admin/static/aws_tp_styles.css`:
- Around line 1089-1091: The .justified-badge CSS rule uses var(--tp-success)
(green) but should use the danger color to match the red cross icon; update the
.justified-badge declaration to use var(--tp-danger) (or the project’s danger
token) so justified absences render with the intended red styling.

In `@cms/server/admin/templates/training_program_combined_ranking.html`:
- Around line 238-242: The URL for the Export link is building
training_day_types with join(', ') which inserts spaces and isn't URL-encoded;
update the template expression that emits training_day_types (the href in the
anchor in training_program_combined_ranking.html) to use join(',') and apply
urlencode (e.g., replace training_day_types | join(', ') with training_day_types
| join(',') | urlencode) so the parameter is properly encoded and matches the
approach used in the attendance template.
🧹 Nitpick comments (1)
cms/server/admin/templates/training_program_attendance.html (1)

405-431: Consider handling non-JSON error responses.

If the server returns a non-2xx status with an HTML error page (e.g., 500), response.json() will throw a parsing error with a confusing message. Consider checking response.ok first.

🔧 Proposed fix
     fetch('{{ url("training_program", training_program.id, "attendance") }}/' + currentAttendanceId, {
         method: 'POST',
         headers: {
             'Content-Type': 'application/json',
             'X-XSRFToken': get_cookie("_xsrf")
         },
         body: JSON.stringify(data)
     })
-    .then(function(response) { return response.json(); })
+    .then(function(response) {
+        if (!response.ok) {
+            throw new Error('Server error: ' + response.status);
+        }
+        return response.json();
+    })
     .then(function(result) {

@ronryv ronryv merged commit e099933 into training_program Jan 29, 2026
5 checks passed
ronryv added a commit that referenced this pull request Jan 29, 2026
…o attendance (#109)

* Add justified absences, comments, and proctored status to attendance

- Add justified, comment, and proctored fields to ArchivedAttendance model
- Add UpdateAttendanceHandler for updating attendance records via AJAX
- Update attendance page with modal for editing justified/proctored/comment
- Show justified status with green checkmark, proctored with camera icon
- Add comment indicator icon when comments exist
- Update combined ranking page to show justified and proctored badges
- Add CSS styles for new badges

Co-Authored-By: Ron Ryvchin <[email protected]>

* Add default=False to justified and recorded columns

Without default=False, archiving training days fails with IntegrityError
because the ORM doesn't have default values for these NOT NULL columns.

Co-Authored-By: Ron Ryvchin <[email protected]>

* Fix client side updates

* Add Excel export feature for attendance page

Co-Authored-By: Ron Ryvchin <[email protected]>

* Improve Excel export: add training types, zebra colors, student tags, and filter-based filename

Co-Authored-By: Ron Ryvchin <[email protected]>

* Change student tags to separate column instead of split row

Co-Authored-By: Ron Ryvchin <[email protected]>

* Change student tags to separate column instead of split row

Co-Authored-By: Ron Ryvchin <[email protected]>

* Small comment display change

* Add Excel export for combined ranking page with shared utilities

Co-Authored-By: Ron Ryvchin <[email protected]>

* review comment

* review comments

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants