Compare commits

...
58 Commits
Author SHA1 Message Date
Cris Charles 74e708661a Update README.md 2026-10-06 01:03:32 +08:00
Cris Charles 8799d58280 Update README with features section
Added features section to README with details on document management and student records.
2026-10-06 00:57:11 +08:00
Cris Charles 87c4aff693 Fix markdown formatting in README links 2026-10-06 00:56:45 +08:00
Cris Charles 848b3b133b Update README.md 2026-10-06 00:54:00 +08:00
Cris Charles 939ff3e0fb Update README.md 2026-10-06 00:52:25 +08:00
Cris Charles e947e5ccb0 Format license section in README.md
Updated license section formatting and added license details.
2026-10-06 00:47:04 +08:00
Cris Charles 0c4c7e4972 Create LICENSE 2026-10-06 00:41:40 +08:00
ssbg04 be10ed43de fix(updater): resolve android package install permission issue 2026-10-06 00:18:37 +08:00
ssbg04 b5402f34b5 feat: in-app updater and responsive csv enrollment UI 2026-10-05 15:16:29 +08:00
ssbg04 d126ecaef1 rework the entire docs 2026-10-05 09:45:38 +08:00
ssbg04 a8473c23b7 feat(students): add single-student multi-enrollment CSV import modal
- Add StudentCsvEnrollmentModal for bulk-adding multiple enrollment
  records across school years for one specific student via CSV input
- CSV format: School Year, Grade Level, Section, Track/Strand
- Features: editable monospace text area, Fill Example, Copy Template,
  file upload (.csv/.txt), and live backend progression verification
- Show row-by-row verification results with valid/error status chips
  and actionable error messages (section suggestions, available years)
- Admin-only: hidden from teacher role via _isTeacher guard
- Wire Bulk Enroll (CSV) button in edit_student_modal.dart to
  StudentCsvEnrollmentModal passing studentId, studentName, studentLrn
- Add Bulk CSV shortcut button in edit_enrollment_modal.dart Current
  Enrollments header for admins
- Backend: POST /api/students/:id/verify-enrollments for live per-row
  progression and section existence validation
- Backend: POST /api/students/:id/bulk-enrollments for transactional
  batch enrollment creation with activity logging and socket events
- Add verifyStudentEnrollments and bulkAddStudentEnrollments repository
  methods in student_repository.dart
2026-10-04 13:48:53 +08:00
ssbg04 0058decaef feat: enhance teacher permissions, folder filtering, and bulk enrollment verification 2026-10-04 11:24:07 +08:00
ssbg04 3e71448add fix minor words 2026-10-02 21:48:46 +08:00
ssbg04 d4d80bf9e6 docs: update user documentation with consumer-friendly language and revised feature scopes 2026-10-02 21:37:29 +08:00
ssbg04 35f56f925a fix(reports): eliminate students per year overflow and align compliance card heights to 480px
- Prevent RenderFlex bottom overflow in yearly comparison chart with dynamic height clamping
- Harmonize Missing Documents, Grade Compliance, Status Donut, and Yearly Comparison cards to 480px on desktop
- Optimize logo assets to webp across login, splash, and layout navbars
- Enable resource shrinking and minification in Android release build
- Remove obsolete frontend Excel templates from assets and pubspec
2026-10-02 21:20:21 +08:00
ssbg04 e6e8188312 feat(reports): refine filters and export layout, add backend Excel export, optimize logo to webp 2026-10-02 20:30:15 +08:00
ssbg04 3a3e0dc7b8 feat: enhance deped reports pdf/excel, dashboard rotation, user creation, and history search shortcut
- Reports: migrate compliance export to xlsx, fix android chart tooltip headroom, add deped collapsible section descriptions, and complete 3-page DepEd PDF export

- Dashboard: continuous document breakdown rotation on mobile, persistent setup banner

- Users: automated background password generation, standardized action button dimensions

- History: add Ctrl+F search shortcut with autofocus cursor in search dialog
2026-10-02 17:02:04 +08:00
ssbg04 a74e0dc7fe feat(docs): redesign documentation to clean minimalist standard format and update system features 2026-10-02 10:34:04 +08:00
ssbg04 79f4504694 fix(audit-trail): prevent timestamp and badge overflow on printed history cards 2026-10-02 10:24:35 +08:00
ssbg04 b04981d7fb feat(system): implement socket.io real-time sync, admin passwords, ocr adjustments, and shortcut fixes 2026-10-02 10:17:44 +08:00
ssbg04 f30faeecff feat(students): adapt filter dialog for enrolled vs all tabs
- Hide school year and status filters in student filter dialog on Enrolled tab\n- Remove 'Enrolled' status filter option on All Students tab\n- Sync studentViewTabProvider and studentActiveFilterCountProvider across desktop and mobile layouts
2026-10-01 23:49:48 +08:00
ssbg04 5952de21b2 fix(android): resolve permission denied on PDF export and make settings responsive
- Fix Android PDF and report export permission denied by adding adaptive storage permission handling (MANAGE_EXTERNAL_STORAGE on Android 11+ and legacy storage on Android 10) and multi-tier fallback to app-specific external storage in DownloadService\n- Update DepEd Transparency Board PDF export and reports export to use DownloadService.saveBytes()\n- Make Settings screen About, Support, and System Information responsive across screen widths\n- Update Student and User screen top headers to segmented tabs design and tighten mobile drawer edge touch area
2026-10-01 23:18:51 +08:00
ssbg04 bcfa1a0919 perf(frontend): optimize build size and update students view tab design
- Purge heavy and unused packages (syncfusion_flutter_pdfviewer, excel, sheetifye, cupertino_icons, image_picker, lottie, flutter_holo_date_picker, flutter_speed_dial)
- Delegate PDF and spreadsheet preview to system default viewers via open_filex
- Replace excel report generation with pure Dart UTF-8 BOM CSV generator
- Configure NDK debug symbol stripping in build.gradle.kts
- Align Students screen Enrolled/All Students tab switcher with Archives segmented tab design
2026-10-01 22:01:07 +08:00
ssbg04 3e2a52770c fix(students): ensure accurate active-year student loading and navigate to all tab on search 2026-09-29 05:24:01 +08:00
ssbg04 4f20a67fda fix: resolve multi-account session conflict, remove developer account, add student view tabs 2026-09-29 05:19:45 +08:00
ssbg04 c085154b8a feat(demo): update Android mobile demo to match actual app screenshots with drawer navigation and full screens 2026-09-28 22:38:55 +08:00
ssbg04 e533b60769 feat(demo): build authentic web-based static app matching screenshots 2026-09-28 22:31:29 +08:00
ssbg04 fca3a1f9d7 feat(demo): update prototype to showcase authentic Windows and Android app screens 2026-09-28 22:23:12 +08:00
ssbg04 18abb9a6d8 feat(demo): add interactive static prototype with Windows and Android viewports 2026-09-28 22:19:35 +08:00
ssbg04 d75d441441 fix(students): hide bulk import button on windows app 2026-09-28 21:52:41 +08:00
ssbg04 6f95fd265b feat(update): detect Android architecture and launch architecture-matched APK download URL 2026-09-28 21:45:11 +08:00
ssbg04 09c0e600e9 feat(settings): add about section with browser update check and remove splash popup 2026-09-28 21:29:05 +08:00
ssbg04 c6d5607811 perf(android): enable R8 shrinking, ProGuard rules, and downscale image assets 2026-09-28 20:58:22 +08:00
ssbg04 8fcd875f38 feat(mobile): enable sliding drawer from hamburger icon and wide left-edge gesture 2026-09-28 20:48:07 +08:00
ssbg04 3a1f872f54 feat(capstone): redesign members screen with photo cards and updated institutional branding 2026-09-28 20:41:15 +08:00
ssbg04 30e260a357 refactor(login): remove localhost and vps server config options 2026-09-28 19:32:14 +08:00
ssbg04 58bf10b4ec fix(reports): make transparency board section titles responsive on android, refine bulk ocr, and update downloads directory 2026-09-27 20:13:45 +08:00
ssbg04 bd2a8acbfe feat(settings): clean academic setup buttons and rename screen to Academic & Class Management
- Update screen, settings card, and dashboard titles to 'Academic & Class Management'

- Clean and balance Academic Structure toolbar for mobile and desktop

- Remove redundant buttons on section cards in favor of interactive adviser chip

- Modernize teacher action button to OutlinedButton.icon 'Sections'

- Add responsive bulk academic structure import support across backend and frontend
2026-09-27 02:05:47 +08:00
ssbg04 a9c2ab0da5 fix(setup): make bulk academic import responsive and fix runtime string indexing error 2026-09-27 01:30:57 +08:00
ssbg04 13bbdf9e26 feat(bulk-import): add live CSV input, format guides, and validation preview for students and academic structure 2026-09-27 01:05:59 +08:00
ssbg04 32cff2431a feat(settings): add teacher due date reminders card and automated notification service 2026-09-27 00:43:22 +08:00
ssbg04 ae5246f271 feat(settings): match scroll fade overlays with dashboard screen 2026-09-26 23:55:04 +08:00
ssbg04 42d9a60805 fix font color auto grad 2026-09-26 18:47:17 +08:00
ssbg04 ecfb98ac86 commented tasks done 2026-09-26 18:46:42 +08:00
ssbg04 7faade70a9 feat(reports,settings,docs): server-side PDF export, cutoff date picker, and UI refinements
- Reports: Migrate DepEd Transparency Board PDF export to backend Puppeteer service with enlarged layout and per-section downloads

- Settings: Replace dynamic grace period days input with cutoff date picker and calculated days preview; separate schedule date pickers

- Documents: Default requirements section to collapsed/closed state in upload OCR modal with pulsing border container

- Fix contrast on Auto-Graduation trigger button and ensure desktop responsiveness across report buttons
2026-09-26 18:32:44 +08:00
ssbg04 2376e99217 feat(auth,capstone): validate email on password reset, convert images to webp and add team carousel
- Add email deliverability verification (MEV) to forgot password flow (lookupResetOptions & sendEmailOtp)
- Validate recipient email format and verify deliverability before sending reset password link in user management
- Convert all backdrops, group photos, and solo avatars to WebP format, drastically reducing asset footprint from >60MB to ~4.6MB
- Add auto-advancing slide preview carousel with 2s cooldown, navigation buttons, and wrapping to Capstone Members screen
- Update Capstone member emails and add individual solo avatar photos
2026-09-25 21:18:02 +08:00
ssbg04 99be1e8d79 feat(build): split Android APKs, refine settings, fix student loading and PDF layout
- Split Android release into TIS_RMS_32bit, TIS_RMS_64, and TIS_RMS_Universal APKs in GitHub Actions and INSTALL.md
- Prioritize universal APK in AppUpdateService and update fallback labels
- Refactor settings academic collapse to 'Academic & Graduation' with automatic active school year card and relocated grace period
- Fix persistent loading indicators on student edit/add modals and list background
- Fix DepEd Transparency Board PDF report subheader layout to prevent title and period collision
- Update AGENTS.md backlog with document scanner and PDF processing items
2026-09-25 20:07:14 +08:00
ssbg04 71526fe61e fix(reports): resolve Windows layout overflows and refine UI
- Fix 1px bottom overflow on Overall Compliance KPI card by adjusting height and padding
- Fix text truncation for Pending Docs KPI card
- Fix 49px bottom overflow on Students Per Year card by adjusting container and chart heights
- Fix LayoutBuilder inside IntrinsicHeight issue on desktop report charts
- Refine security settings, audit trail platform tracking, and document handling
- Clean up unused conversion providers and LibreOffice service
2026-09-25 19:04:23 +08:00
ssbg04 13abad6563 feat(security,audit): fix platform tracking for history and settings, collapse logged-in devices in security tab 2026-09-25 09:34:14 +08:00
ssbg04 689dbc48e7 fix: self-deactivation, UI overflow, Open With, archive nav, schedule modal
Settings - Delete Account -> Self Deactivate:
- backend: add POST /api/auth/self-deactivate (immediate, no email link)
  Guards against last-admin and hidden super-admin. Revokes FCM tokens,
  sends notification email, logs activity
- auth.js route: add /self-deactivate route
- auth_repository.dart: add requestSelfDeactivation()
- auth_provider.dart: add requestSelfDeactivation() (calls logout() after)
- settings_screen.dart: 2-step _handleDeactivateAccount: Step 1 confirm,
  Step 2 type DEACTIVATE. Danger zone card updated to deactivation theme

Report screen - compliance filter button label overflow:
- Wrap label Text in Flexible to ellipsize instead of clip

Settings - Edit Schedule Dates modal button overflow:
- Column layout on narrow screens so buttons are never squished

Document screen - Open With not working on Windows:
- Use openwith.exe (Windows 8+) with fallback to rundll32 OpenAs_RunDLL

Windows sidebar - Archive tab:
- Move Archives from ACCOUNT group to OVERVIEW group
2026-09-21 17:51:33 +08:00
ssbg04 1a20994749 feat: document versioning, templates, ZIP backup/restore, and Open/Edit workflow
Backend:
- schema.js: add document_templates, template_versions, document_versions tables;
  migrate documents with template_id/template_version_id columns;
  auto-seed Version 1 for all 190 existing documents
- templateController.js (NEW): full CRUD + file upload/download for templates
  and template versions
- templates.js route (NEW): /api/templates endpoints
- documentController.js: add getDocumentVersions, uploadDocumentVersion,
  downloadDocumentVersion; uploadDocument now seeds Version 1 on creation
- documents.js: wire GET /:id/versions, POST /:id/upload-version
- backupController.js: upgrade backup to ZIP archive (DB + student files +
  template files + SHA-256 manifest); restore handles ZIP and legacy .db;
  safety backup taken before every restore
- package.json: add archiver + unzipper dependencies

Frontend:
- template_model.dart (NEW): DocumentTemplateModel + TemplateVersionModel entities
- document_version_model.dart (NEW): DocumentVersionModel entity
- template_repository.dart (NEW): Dio-based template API client
- document_repository.dart: add getDocumentVersions, uploadDocumentVersion,
  downloadDocumentVersion methods
- document_preview_modal.dart: add Open / Open With (Windows rundll32
  OpenAs_RunDLL / Android chooser) / Edit (download-temp -> open -> upload
  prompt) / Upload New Version (FilePicker) / Version History actions
- document_version_history_sheet.dart (NEW): draggable bottom sheet listing
  all document versions with per-version Preview + Download
- file_folder_card.dart: add Open With, Edit, Upload New Version, Version
  History items to context menu
- documents_screen.dart: wire open_with, edit, upload_version, version_history,
  restore action cases; import version history sheet

Bugfix:
- student_profile_modal.dart: remove enrollment.status access that caused
  NoSuchMethodError (enrollments table has no status column)
2026-09-21 12:55:58 +08:00
ssbg04 e2f10f6260 feat(ui,audit): redesign folder cards and polish student actions, fix audit trail crash and count queries
- Fix audit trail AppPagination infinite width layout crash on Windows and Android
- Fix Android audit trail header 41px horizontal overflow with responsive layout
- Add missing user and history table joins to dashboard and user activity count queries
- Implement StyledFolderIcon matching modern tabbed folder design with internal JHS/SHS chips
- Align bulk operations bar margin to match list and grid widget containers
- Make student screen Add Student button elevated with emerald gradient and shadow
- Move edit student modal close button from leading to actions bar
- Polish Edit Details and Edit Enrollment actions in student profile modal
2026-09-19 22:56:42 +08:00
ssbg04 69b4879ac2 feat(dashboard,ui): overhaul dashboard KPIs, audit trail tab, responsive controls, and settings
- Remove recent activities and user history from dashboard; relocate to dedicated History tab
- Fix Document Breakdown overflow and rebalance Bento KPI grid layout
- Fix Android overflow on student profile Edit Details button
- Simplify grid view cards in documents and archives screens with minimal folder styling
- Fix transparency board filter contrast and export button styling in reports
- Default auto-update enrollment to off, add grace period input, and fix settings danger zone
2026-09-19 02:26:49 +08:00
ssbg04 2dc8ee4242 feat(ui,api): update action buttons, password modal button, and dashboard user count
- Exclude hidden super administrator from dashboard active users count

- Rename UPDATE PASSWORD button to UPDATE in change password modal

- Use single-word labels and uniform dimensions for user profile action buttons
2026-09-18 21:43:39 +08:00
ssbg04 59e94192aa feat(frontend): enhance student profile actions and report transparency board
- Add separate Edit Details and Edit Enrollment buttons in student profile modal

- Change delete button to Inactive in student profile modal

- Remove Inactive status from edit student modal dropdown

- Remove Set Inactive action from student multi-select bar and context menu

- Require long-press to activate multi-select on mobile student avatar

- Add clean tab switcher and collapsible info boxes to DepEd Transparency Board
2026-09-18 19:37:27 +08:00
ssbg04 48fcf181af refactor(reports): use standalone section cards and clean minimal header for Transparency Board 2026-09-18 19:05:13 +08:00
ssbg04 793bd340f1 fix(windows): auto-discover and include Visual Studio ATL/MFC paths in CMake to resolve atlstr.h 2026-09-18 18:41:36 +08:00
ssbg04 029629c349 feat(reports,splash): separate transparency board data/graphs and add release update popup 2026-09-18 18:22:04 +08:00
205 changed files with 42436 additions and 7765 deletions

No files matched your search

+61 -17
View File
@@ -422,9 +422,9 @@ jobs:
}
# ------------------------------------------------------------
# Android Build
# Android Build (Split 32-bit, 64-bit & Universal APKs)
# ------------------------------------------------------------
- name: Build Android Universal APK
- name: Build Android APKs (Split 32-bit, 64-bit & Universal)
working-directory: frontend
shell: pwsh
run: |
@@ -435,33 +435,73 @@ jobs:
& "$env:JAVA_HOME\bin\java.exe" -version
$tag = "${{ steps.release_info.outputs.tag }}"
$verName = "${{ steps.release_info.outputs.version_name }}"
$buildNum = "${{ steps.release_info.outputs.build_number }}"
# 1. Build Split-per-ABI APKs (32-bit armeabi-v7a & 64-bit arm64-v8a)
Write-Host ""
Write-Host "Building Android APK..."
Write-Host "Building Android Split-per-ABI APKs (32-bit & 64-bit)..."
flutter build apk `
--release `
--split-per-abi `
--obfuscate `
--split-debug-info=build/app/outputs/symbols `
--build-name="$verName" `
--build-number="$buildNum"
if ($LASTEXITCODE -ne 0) {
throw "Flutter Android split-per-abi build failed."
}
$apk32 = "build\app\outputs\flutter-apk\app-armeabi-v7a-release.apk"
$apk64 = "build\app\outputs\flutter-apk\app-arm64-v8a-release.apk"
$output32 = "..\installers\TIS_RMS_32bit_$tag.apk"
$output64 = "..\installers\TIS_RMS_64_$tag.apk"
if (-not (Test-Path $apk32)) {
throw "Split build completed without producing $apk32"
}
if (-not (Test-Path $apk64)) {
throw "Split build completed without producing $apk64"
}
Copy-Item $apk32 $output32 -Force
Copy-Item $apk64 $output64 -Force
Copy-Item $apk32 "..\installers\TIS_RMS_32bit.apk" -Force
Copy-Item $apk64 "..\installers\TIS_RMS_64.apk" -Force
Write-Host "32-bit APK copied to $output32 and ..\installers\TIS_RMS_32bit.apk"
Write-Host "64-bit APK copied to $output64 and ..\installers\TIS_RMS_64.apk"
# 2. Build Universal APK (All ABIs bundled)
Write-Host ""
Write-Host "Building Android Universal APK..."
flutter build apk `
--release `
--obfuscate `
--split-debug-info=build/app/outputs/symbols `
--build-name="${{ steps.release_info.outputs.version_name }}" `
--build-number="${{ steps.release_info.outputs.build_number }}"
--build-name="$verName" `
--build-number="$buildNum"
if ($LASTEXITCODE -ne 0) {
throw "Flutter Android APK build failed."
throw "Flutter Android universal APK build failed."
}
$apkPath = "build\app\outputs\flutter-apk\app-release.apk"
$outputPath = "..\installers\TIS_RMS_Android_Universal_${{ steps.release_info.outputs.tag }}.apk"
$apkUniversal = "build\app\outputs\flutter-apk\app-release.apk"
$outputUniversal = "..\installers\TIS_RMS_Universal_$tag.apk"
if (-not (Test-Path $apkPath)) {
throw "Android build completed without producing $apkPath"
if (-not (Test-Path $apkUniversal)) {
throw "Universal build completed without producing $apkUniversal"
}
Copy-Item `
$apkPath `
$outputPath `
-Force
Copy-Item $apkUniversal $outputUniversal -Force
Copy-Item $apkUniversal "..\installers\TIS_RMS_Universal.apk" -Force
Write-Host "Android APK copied to $outputPath"
Write-Host "Universal APK copied to $outputUniversal and ..\installers\TIS_RMS_Universal.apk"
# ------------------------------------------------------------
# Verify Artifacts
@@ -474,7 +514,9 @@ jobs:
$tag = "${{ steps.release_info.outputs.tag }}"
$files = @(
"installers\TIS_RMS_Client_Setup_$tag.exe",
"installers\TIS_RMS_Android_Universal_$tag.apk"
"installers\TIS_RMS_32bit_$tag.apk",
"installers\TIS_RMS_64_$tag.apk",
"installers\TIS_RMS_Universal_$tag.apk"
)
foreach ($file in $files) {
@@ -503,7 +545,9 @@ jobs:
name: Release ${{ steps.release_info.outputs.tag }}
files: |
installers/TIS_RMS_Client_Setup_${{ steps.release_info.outputs.tag }}.exe
installers/TIS_RMS_Android_Universal_${{ steps.release_info.outputs.tag }}.apk
installers/TIS_RMS_32bit_${{ steps.release_info.outputs.tag }}.apk
installers/TIS_RMS_64_${{ steps.release_info.outputs.tag }}.apk
installers/TIS_RMS_Universal_${{ steps.release_info.outputs.tag }}.apk
generate_release_notes: true
draft: false
prerelease: false
+1
View File
@@ -27,6 +27,7 @@ frontend/android/app/google-services.json
**/google-services.json
backend/brain
frontend/brain
documentation/TODO.md
# Database and runtime data folder (never commit)
backend/*.db
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+85 -40
View File
@@ -1,85 +1,130 @@
<div align="center">
<div align="center">TIS RMS
# TIS RMS
Records Management System
### Records Management System
A records management system for managing student records, documents, and school forms.
**A full-stack, offline-capable platform for managing student records, documents, and OCR-powered data extraction in Philippine educational institutions.**
<br/>
![Flutter](https://img.shields.io/badge/Flutter-3.x-02569B?style=for-the-badge&logo=flutter&logoColor=white)
![Node.js](https://img.shields.io/badge/Node.js-18+-339933?style=for-the-badge&logo=node.js&logoColor=white)
![SQLite](https://img.shields.io/badge/SQLite-003B57?style=for-the-badge&logo=sqlite&logoColor=white)
![Firebase](https://img.shields.io/badge/Firebase-FFCA28?style=for-the-badge&logo=firebase&logoColor=black)
![Windows](https://img.shields.io/badge/Windows-Desktop-0078D6?style=for-the-badge&logo=windows&logoColor=white)
![Android](https://img.shields.io/badge/Android-Mobile-3DDC84?style=for-the-badge&logo=android&logoColor=white)
![Version](https://img.shields.io/github/v/tag/ssbg04/TIS_RMS?style=for-the-badge&label=version&color=4CAF50)
[Landing Page](https://web.tis-rms.cc.cd) · [Documentation](https://docs.tis-rms.cc.cd)
</div>
---
## ✨ Features
Features
- 📂 **Document Management** — Upload, preview, and organize student documents (PDFs & images)
- 🎓 **Student Records** — Track enrollment status, LRN, grade level, strand, and document completion
- 🔍 **OCR Extraction** — Extract data from scanned documents via bundled Tesseract OCR (fully offline)
- 📊 **Reports** — Generate DepEd School Form 10 (SF10) for JHS & SHS using bundled Excel templates
- 🔔 **Push Notifications** — Real-time alerts via Firebase Cloud Messaging
- 🔐 **Authentication** — JWT-based sessions with bcrypt password hashing
- 🛠️ **Windows Service** — Backend can run as a persistent Windows Service via NSSM
- Document Management — Upload, preview, and organize student documents.
- Student Records — Manage enrollment information, LRN, grade level, strand, and document status.
- OCR Extraction — Extract information from scanned documents using Tesseract OCR.
- Reports — Generate DepEd School Form 10 (SF10) for JHS and SHS.
- Push Notifications — Deliver notifications through Firebase Cloud Messaging.
- Authentication — JWT-based authentication with bcrypt password hashing.
- Windows Service — Run the backend as a persistent Windows service.
---
## 🏗️ Architecture
Architecture
Two components communicating over a local network:
- **Frontend** — Flutter multi-platform client supporting **Windows** (desktop) and **Android** (mobile), built with Riverpod, Dio, Syncfusion PDF Viewer, and Socket.IO
- **Backend** — Node.js + Express REST API with a local SQLite database, Tesseract OCR, and Ghostscript bundled for offline document processing
- Frontend — Flutter multi-platform client supporting Windows (desktop) and Android (mobile), built with Riverpod, Dio, Syncfusion PDF Viewer, and Socket.IO.
- Backend — Node.js + Express REST API with a local SQLite database, Tesseract OCR, and Ghostscript bundled for offline document processing.
---
## 🚀 Getting Started
Getting Started
**Prerequisites:** Node.js v18+, Flutter SDK (stable), Visual Studio 2022 with C++ workload.
> Tesseract OCR and Ghostscript are bundled — no extra installation needed.
> Tesseract OCR and Ghostscript are bundled — no extra installation needed.»
Backend
### Backend
```bash
cd backend
npm install
copy .example.env .env # configure port, JWT secret, etc.
npm run dev # or: npm start
copy .example.env .env
npm run dev
```
### Frontend
Configure the required environment variables in ".env" before starting the backend.
For production:
```bash
npm start
```
Frontend
```bash
cd frontend
flutter pub get
```
# Windows
Run on Windows:
```bash
flutter run -d windows
flutter build windows
```
# Android
Build for Windows:
```bash
flutter build windows
```
Run on Android:
```bash
flutter run -d android
```
Build an Android APK:
```bash
flutter build apk
```
To build a Windows installer, compile `frontend/TIS_RMS_Client.iss` with **Inno Setup**, or run:
Windows Installer
To build the Windows installer, compile:
```text
frontend/TIS_RMS_Client.iss
```
using Inno Setup.
Alternatively:
```powershell
.\build_release.ps1
```
---
## 📜 License
Documentation
Developed by **BSIT3DSB** — PLSP.
For installation guides, system documentation, configuration, and usage instructions, visit the official documentation:
<div align="center">
<sub>Built with ❤️ using Flutter &amp; Node.js · TIS Records Management System</sub>
</div>
[docs.tis-rms.cc.cd](https://docs.tis-rms.cc.cd)
---
Website
Visit the TIS RMS landing page:
[web.tis-rms.cc.cd](https://web.tis-rms.cc.cd)
---
License
TIS RMS is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
See the [LICENSE](LICENSE) file for the full license text.
Project
Developed by BSIT3DSB — PLSP.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+28 -1
View File
@@ -1,8 +1,10 @@
process.env.TZ = 'Asia/Manila';
const express = require('express');
const http = require('http');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const { initSocket } = require('./src/services/socketService');
require('dotenv').config({ path: path.join(__dirname, '.env'), override: true });
require('dotenv').config({ path: path.join(__dirname, '..', '.env'), override: true });
@@ -29,10 +31,15 @@ const notificationRoutes = require('./src/routes/notifications');
const backupRoutes = require('./src/routes/backup');
const serverRoutes = require('./src/routes/server.routes');
const settingsRoutes = require('./src/routes/settings');
const templateRoutes = require('./src/routes/templates');
const app = express();
const server = http.createServer(app);
const PORT = process.env.PORT || 18484;
// Initialize Socket.IO Real-Time Server
initSocket(server);
// Initialize Database
initSchema();
@@ -59,13 +66,14 @@ app.use('/api/notifications', notificationRoutes);
app.use('/api/backup', backupRoutes);
app.use('/api/server', serverRoutes);
app.use('/api/settings', settingsRoutes);
app.use('/api/templates', templateRoutes);
app.get(['/', '/api'], (req, res) => {
res.set('X-TIS-RMS', 'true');
res.json({ message: 'TIS RMS API is running' });
});
app.listen(PORT, '0.0.0.0', () => {
server.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT} (0.0.0.0)`);
// Run auto-graduation check on startup and daily
try {
@@ -76,6 +84,25 @@ app.listen(PORT, '0.0.0.0', () => {
console.error('Failed to start auto-graduation schedule:', err.message);
}
// Run auto-archiving check on startup, set precision timer, and check every 30 seconds
try {
const { checkAndRunAutoArchive, scheduleAutoArchiveTimer } = require('./src/services/autoArchiveService');
checkAndRunAutoArchive(1);
scheduleAutoArchiveTimer();
setInterval(() => checkAndRunAutoArchive(1), 30 * 1000);
} catch (err) {
console.error('Failed to start auto-archiving schedule:', err.message);
}
// Run teacher document requirement due date reminders check on startup and hourly
try {
const { checkAndRunTeacherDueDateReminders } = require('./src/services/teacherReminderService');
checkAndRunTeacherDueDateReminders();
setInterval(() => checkAndRunTeacherDueDateReminders(), 60 * 60 * 1000);
} catch (err) {
console.error('Failed to start teacher reminder service:', err.message);
}
// Initialize Auto Cloudflare Tunnel activation when internet is connected
try {
const { startAutoTunnel } = require('./src/services/tunnelService');
+1439 -131
View File
File diff suppressed because it is too large. Load diff
+13 -1
View File
@@ -19,6 +19,7 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"archiver": "^8.0.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.9.0",
"cors": "^2.8.6",
@@ -30,9 +31,20 @@
"morgan": "^1.10.1",
"multer": "^2.1.1",
"nodemailer": "^9.0.6",
"sharp": "^0.35.4"
"pdf-lib": "^1.17.1",
"pdfkit": "^0.20.2",
"quickemailverification": "^1.0.4",
"sharp": "^0.35.4",
"socket.io": "^4.8.4",
"unzipper": "^0.12.5"
},
"devDependencies": {
"nodemon": "^3.1.14"
},
"allowScripts": {
"@firebase/util@1.15.2": true,
"bcrypt@6.0.0": true,
"better-sqlite3@12.9.0": true,
"protobufjs@7.6.5": true
}
}
+11 -4
View File
@@ -216,13 +216,20 @@ exports.getArchivedDocuments = (req, res) => {
if (types.length > 0) {
const typeConditions = [];
for (const t of types) {
if (t === 'All JHS') {
if (t === 'All JHS' || t === 'All JHS Requirements') {
typeConditions.push(`dr.category = 'JHS'`);
} else if (t === 'All SHS') {
} else if (t === 'All SHS' || t === 'All SHS Requirements') {
typeConditions.push(`dr.category = 'SHS'`);
} else {
typeConditions.push(`(d.document_type = ? OR dr.name = ?)`);
params.push(t, t);
const cleanT = t.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
typeConditions.push(`(
d.document_type = ?
OR dr.name = ?
OR d.document_type = ?
OR d.document_type = ?
OR dr.name = ?
)`);
params.push(t, t, `JHS - ${cleanT}`, `SHS - ${cleanT}`, cleanT);
}
}
conditions.push(`(${typeConditions.join(' OR ')})`);
+392 -13
View File
@@ -7,6 +7,127 @@ const { createNotification } = require('./notificationController');
const { sendPasswordResetOtp, sendAccountDeletionEmail } = require('../services/emailService');
const { getBestServerBaseUrl } = require('./userController');
// ── MyEmailVerifier (MEV) Email Validator ─────────────────────────────────────
// Returns { valid: bool, reason: string, details?: object } — never throws.
const validateEmailMEV = async (email) => {
if (!email || !email.trim()) return { valid: false, reason: 'Email is empty' };
const apiKey = process.env.MEV_API_KEY;
if (!apiKey) {
console.warn('[MEV] MEV_API_KEY not configured in .env – skipping verification');
return { valid: true, reason: 'MEV key not configured – skipping' };
}
try {
const url = `https://api.myemailverifier.com/api/validate_single.php?apikey=${encodeURIComponent(apiKey)}&email=${encodeURIComponent(email.trim())}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
console.warn(`[MEV] HTTP ${response.status} from MyEmailVerifier`);
return { valid: true, reason: 'Validation service temporarily unavailable' };
}
const data = await response.json();
const status = (data.Status || '').trim().toLowerCase();
const diagnosis = data.Diagnosis || '';
const isDisposable = data.Disposable_Domain === 1 ||
data.Disposable_Domain === '1' ||
data.Disposable_Domain === 'true' ||
data.Disposable_Domain === true;
if (isDisposable) {
return { valid: false, reason: 'Disposable email addresses are not allowed', details: data };
}
if (status === 'invalid') {
return { valid: false, reason: diagnosis || 'Email address does not exist or is invalid', details: data };
}
const isValid = status === 'valid';
return {
valid: isValid,
reason: isValid ? 'valid' : (diagnosis || 'Email verification failed'),
details: data,
};
} catch (err) {
console.warn('[MEV] Email validation error (skipping):', err.message);
return { valid: true, reason: 'MEV unreachable – skipping' };
}
};
exports.validateEmailMEV = validateEmailMEV;
exports.validateEmailQEV = validateEmailMEV; // Backward compatibility
// ── Session Helpers ───────────────────────────────────────────────────────────
const hashToken = (token) => crypto.createHash('sha256').update(token).digest('hex');
// Helper to detect platform from request headers, body, or User-Agent
const detectPlatform = (req) => {
const headerPlatform = req.headers['x-platform'] || req.headers['x-client-platform'];
if (headerPlatform && typeof headerPlatform === 'string' && headerPlatform.trim().length > 0) {
return headerPlatform.trim().toLowerCase();
}
if (req.body && req.body.platform && typeof req.body.platform === 'string' && req.body.platform.trim().length > 0) {
return req.body.platform.trim().toLowerCase();
}
const ua = (req.headers['user-agent'] || '').toLowerCase();
if (ua.includes('android')) return 'android';
if (ua.includes('windows') || ua.includes('win32') || ua.includes('win64')) return 'windows';
if (ua.includes('iphone') || ua.includes('ipad') || ua.includes('ios')) return 'ios';
if (ua.includes('macintosh') || ua.includes('mac os')) return 'macos';
if (ua.includes('linux')) return 'linux';
return 'windows';
};
// Helper to detect device name from request headers, body, or platform
const detectDeviceName = (req, platform) => {
const headerDevice = req.headers['x-device-name'];
if (headerDevice && typeof headerDevice === 'string' && headerDevice.trim().length > 0) {
return headerDevice.trim();
}
if (req.body && req.body.device_name && typeof req.body.device_name === 'string' && req.body.device_name.trim().length > 0) {
return req.body.device_name.trim();
}
const platLower = (platform || '').toLowerCase();
if (platLower.includes('win')) return 'Windows PC';
if (platLower.includes('android')) return 'Android Device';
if (platLower.includes('ios') || platLower.includes('iphone')) return 'iPhone';
if (platLower.includes('mac')) return 'Mac';
if (platLower.includes('web')) return 'Web Browser';
return 'Windows PC';
};
const upsertSession = (userId, token, platform, ipAddress, deviceName) => {
try {
const tokenHash = hashToken(token);
const now = strftime_now();
const effectivePlatform = platform || 'windows';
const effectiveDevice = deviceName || (effectivePlatform === 'windows' ? 'Windows PC' : 'Device');
// If same token re-used (rare), update last_seen
const existing = db.prepare('SELECT id FROM user_sessions WHERE token_hash = ?').get(tokenHash);
if (existing) {
db.prepare(`
UPDATE user_sessions
SET last_seen_at = ?,
platform = COALESCE(?, platform),
ip_address = COALESCE(?, ip_address),
device_name = COALESCE(?, device_name)
WHERE token_hash = ?
`).run(now, effectivePlatform, ipAddress || null, effectiveDevice, tokenHash);
} else {
db.prepare(`
INSERT INTO user_sessions (user_id, token_hash, platform, ip_address, device_name)
VALUES (?, ?, ?, ?, ?)
`).run(userId, tokenHash, effectivePlatform, ipAddress || null, effectiveDevice);
}
} catch (err) { console.warn('[Sessions] upsertSession error:', err.message); }
};
const strftime_now = () => new Date().toISOString().replace('T', 'T').slice(0, 19) + 'Z';
// Helper to mask email (e.g. j***e@gmail.com)
const maskEmail = (email) => {
if (!email || !email.includes('@')) return null;
@@ -37,6 +158,34 @@ exports.login = (req, res) => {
{ expiresIn: '75d' } // Extended for Remember Me support
);
// Record session (device tracking)
const clientPlatform = detectPlatform(req);
const clientDeviceName = detectDeviceName(req, clientPlatform);
const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket?.remoteAddress || null;
upsertSession(user.id, token, clientPlatform, clientIp, clientDeviceName);
// Append login log entry
try {
const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ');
db.prepare(`
INSERT INTO user_login_logs (user_id, username, full_name, role, platform, ip_address)
VALUES (?, ?, ?, ?, ?, ?)
`).run(user.id, user.username, fullName, user.role, clientPlatform, clientIp);
} catch (_) {}
// Close any open sessions from the same IP belonging to a different user.
// This prevents the History screen from showing stale "ACTIVE NOW" entries
// when a different account logs in from the same device / IP address.
if (clientIp) {
try {
db.prepare(`
UPDATE user_login_logs
SET logout_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
WHERE ip_address = ? AND user_id != ? AND logout_at IS NULL
`).run(clientIp, user.id);
} catch (_) {}
}
res.json({
token,
user: {
@@ -118,7 +267,7 @@ exports.changePassword = (req, res) => {
// ============================================================================
// POST /api/auth/lookup-reset-options — Checks username & returns masked email
exports.lookupResetOptions = (req, res) => {
exports.lookupResetOptions = async (req, res) => {
const { username } = req.body;
if (!username || !username.trim()) {
return res.status(400).json({ message: 'Username is required.' });
@@ -141,6 +290,15 @@ exports.lookupResetOptions = (req, res) => {
});
}
// Validate registered email with MEV before allowing reset flow
const mevCheck = await validateEmailMEV(user.email.trim());
if (!mevCheck.valid) {
return res.status(422).json({
message: `Cannot reset password: the registered email address for this account appears to be invalid or undeliverable (${mevCheck.reason}). Please contact an administrator to update your email address.`,
invalidEmail: true
});
}
res.json({
success: true,
username: user.username,
@@ -171,6 +329,14 @@ exports.sendEmailOtp = async (req, res) => {
return res.status(400).json({ message: 'No email address registered for this account.' });
}
// Validate email with MEV before generating OTP and sending email
const mevCheck = await validateEmailMEV(user.email.trim());
if (!mevCheck.valid) {
return res.status(422).json({
message: `Cannot send verification code: the registered email address appears to be invalid or undeliverable (${mevCheck.reason}). Please contact an administrator to update your email address.`
});
}
// Invalidate previous unexpired OTPs for this user's email
db.prepare("UPDATE password_reset_otps SET is_used = 1 WHERE user_id = ? AND delivery_method = 'email' AND is_used = 0")
.run(user.id);
@@ -833,6 +999,72 @@ exports.completePasswordReset = (req, res) => {
}
};
// POST /api/auth/self-deactivate
// Lets the authenticated user deactivate their own account immediately.
// No email link is required — just 2-step client-side confirmation.
exports.selfDeactivateAccount = async (req, res) => {
const userId = req.user?.id;
if (!userId) return res.status(401).json({ message: 'Authentication required' });
try {
const user = db.prepare('SELECT id, username, first_name, last_name, email, role, is_active, is_hidden FROM users WHERE id = ?').get(userId);
if (!user) return res.status(404).json({ message: 'User account not found' });
if (user.role === 'admin') {
const otherActiveAdmins = db.prepare(
"SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND is_active = 1 AND (is_hidden = 0 OR is_hidden IS NULL) AND id != ?"
).get(user.id);
if (!otherActiveAdmins || otherActiveAdmins.count < 1) {
return res.status(403).json({ message: 'Cannot deactivate the only active administrator account. Please promote another admin first.' });
}
}
// Deactivate the account
db.prepare("UPDATE users SET is_active = 0, updated_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) WHERE id = ?").run(userId);
// Revoke all FCM push tokens
db.prepare('DELETE FROM fcm_tokens WHERE user_id = ?').run(userId);
// Revoke all sessions and stamp logout on open login logs
db.prepare('DELETE FROM user_sessions WHERE user_id = ?').run(userId);
try {
db.prepare(`
UPDATE user_login_logs SET logout_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
WHERE user_id = ? AND logout_at IS NULL
`).run(userId);
} catch (_) {}
// Log activity
try {
db.prepare('INSERT INTO activity_log (user_id, action, entity_type, entity_id, description) VALUES (?, ?, ?, ?, ?)')
.run(userId, 'UPDATE', 'user', userId, `Self-deactivated account @${user.username}`);
} catch (_) {}
// Send email notification if available (async, fire-and-forget)
const { sendAccountStatusEmail } = require('../services/emailService');
if (user.email && user.email.trim()) {
(async () => {
const qevResult = await validateEmailQEV(user.email.trim());
if (!qevResult.valid) return;
sendAccountStatusEmail({
to: user.email.trim(),
username: user.username,
fullName: [user.first_name, user.last_name].filter(Boolean).join(' '),
isActivated: false,
}).catch(() => {});
})();
}
res.json({
success: true,
message: 'Your account has been deactivated. You will be signed out.',
});
} catch (error) {
console.error('selfDeactivateAccount error:', error);
res.status(500).json({ message: 'Failed to deactivate account', error: error.message });
}
};
// POST /api/auth/request-delete-account
exports.requestAccountDeletion = async (req, res) => {
const userId = req.user?.id;
@@ -842,9 +1074,6 @@ exports.requestAccountDeletion = async (req, res) => {
const user = db.prepare('SELECT id, username, first_name, last_name, email, role, is_hidden FROM users WHERE id = ?').get(userId);
if (!user) return res.status(404).json({ message: 'User account not found' });
if (user.is_hidden === 1) {
return res.status(403).json({ message: 'Developer super administrator account cannot be deleted.' });
}
if (user.role === 'admin') {
const otherAdmins = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND (is_hidden = 0 OR is_hidden IS NULL) AND id != ?").get(user.id);
@@ -857,6 +1086,13 @@ exports.requestAccountDeletion = async (req, res) => {
return res.status(400).json({ message: 'An email address is required to verify account deletion. Please add an email address to your profile first.' });
}
const qevResult = await validateEmailQEV(user.email.trim());
if (!qevResult.valid) {
return res.status(422).json({
message: `Cannot send deletion email: the registered address "${user.email}" appears to be invalid or undeliverable (${qevResult.reason}).`
});
}
// Generate secure random token
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString();
@@ -1096,15 +1332,6 @@ exports.confirmDeleteAccount = (req, res) => {
return res.status(410).json({ message: msg });
}
if (record.is_hidden === 1) {
const msg = 'Developer super administrator account cannot be deleted.';
if (isHtml) return res.status(403).send(renderDeletionPage({
title: 'Action Prohibited',
isError: true,
contentHtml: `<div class="danger-badge"><strong>Action Prohibited:</strong> ${msg}</div>`,
}));
return res.status(403).json({ message: msg });
}
const fullName = [record.first_name, record.middle_name, record.last_name].filter(Boolean).join(' ');
@@ -1193,3 +1420,155 @@ exports.confirmDeleteAccount = (req, res) => {
}
};
// ── GET /api/auth/sessions — list active sessions for current user ─────────────
exports.getSessions = (req, res) => {
try {
const authHeader = req.headers.authorization || '';
const currentToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
const currentHash = currentToken ? hashToken(currentToken) : null;
const reqPlatform = detectPlatform(req);
const reqDevice = detectDeviceName(req, reqPlatform);
const reqIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket?.remoteAddress || null;
// Auto backfill current active session if platform/device_name is missing
if (currentHash && reqPlatform) {
try {
db.prepare(`
UPDATE user_sessions
SET platform = COALESCE(NULLIF(platform, ''), NULLIF(platform, 'unknown'), ?),
device_name = COALESCE(NULLIF(device_name, ''), NULLIF(device_name, 'Unknown Device'), ?),
ip_address = COALESCE(ip_address, ?),
last_seen_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
WHERE token_hash = ?
`).run(reqPlatform, reqDevice, reqIp, currentHash);
} catch (_) {}
}
const sessions = db.prepare(`
SELECT id, device_name, platform, ip_address, created_at, last_seen_at, token_hash
FROM user_sessions
WHERE user_id = ?
ORDER BY last_seen_at DESC
`).all(req.user.id);
const mapped = sessions.map(s => {
const rawPlat = (s.platform || '').trim().toLowerCase();
const plat = (rawPlat && rawPlat !== 'unknown') ? rawPlat : 'windows';
const devName = (s.device_name && s.device_name !== 'Unknown Device' && s.device_name !== 'Device')
? s.device_name
: (plat === 'windows' ? 'Windows PC' : plat === 'android' ? 'Android Device' : `${plat.charAt(0).toUpperCase() + plat.slice(1)} Device`);
return {
id: s.id,
platform: plat,
device_name: devName,
ip_address: s.ip_address,
created_at: s.created_at,
last_seen_at: s.last_seen_at,
is_current: currentHash ? s.token_hash === currentHash : false
};
});
res.json(mapped);
} catch (error) {
res.status(500).json({ message: 'Failed to fetch sessions', error: error.message });
}
};
// ── DELETE /api/auth/sessions/:id — revoke a specific session ─────────────────
exports.revokeSession = (req, res) => {
try {
const session = db.prepare('SELECT id, user_id FROM user_sessions WHERE id = ?').get(req.params.id);
if (!session) return res.status(404).json({ message: 'Session not found' });
if (session.user_id !== req.user.id) return res.status(403).json({ message: 'Not your session' });
db.prepare('DELETE FROM user_sessions WHERE id = ?').run(req.params.id);
res.json({ message: 'Session revoked' });
} catch (error) {
res.status(500).json({ message: 'Failed to revoke session', error: error.message });
}
};
// ── DELETE /api/auth/sessions — revoke all OTHER sessions (keep current) ──────
exports.revokeAllOtherSessions = (req, res) => {
try {
const authHeader = req.headers.authorization || '';
const currentToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
const currentHash = currentToken ? hashToken(currentToken) : null;
if (currentHash) {
db.prepare('DELETE FROM user_sessions WHERE user_id = ? AND token_hash != ?').run(req.user.id, currentHash);
} else {
db.prepare('DELETE FROM user_sessions WHERE user_id = ?').run(req.user.id);
}
res.json({ message: 'All other sessions revoked' });
} catch (error) {
res.status(500).json({ message: 'Failed to revoke sessions', error: error.message });
}
};
// ── POST /api/auth/logout — stamp logout_at + remove current session ──────────
exports.logout = (req, res) => {
try {
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
if (token) {
const tokenHash = hashToken(token);
db.prepare('DELETE FROM user_sessions WHERE token_hash = ?').run(tokenHash);
}
// Stamp logout_at on the most recent open login log for this user
try {
db.prepare(`
UPDATE user_login_logs SET logout_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
WHERE id = (
SELECT id FROM user_login_logs
WHERE user_id = ? AND logout_at IS NULL
ORDER BY login_at DESC LIMIT 1
)
`).run(req.user.id);
} catch (_) {}
res.json({ message: 'Logged out successfully' });
} catch (error) {
res.status(500).json({ message: 'Logout failed', error: error.message });
}
};
// ── GET /api/auth/login-logs — paginated login/logout log (admin: all users; self: own) ──
exports.getLoginLogs = (req, res) => {
try {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
const offset = (page - 1) * limit;
const userId = req.query.user_id ? parseInt(req.query.user_id) : null;
const search = req.query.search ? `%${req.query.search}%` : null;
const dateFrom = req.query.date_from || null;
const dateTo = req.query.date_to || null;
let where = 'WHERE 1=1';
const params = [];
if (userId) { where += ' AND l.user_id = ?'; params.push(userId); }
if (search) { where += ' AND (l.username LIKE ? OR l.full_name LIKE ?)'; params.push(search, search); }
if (dateFrom) { where += ' AND l.login_at >= ?'; params.push(dateFrom); }
if (dateTo) { where += ' AND l.login_at <= ?'; params.push(dateTo + 'T23:59:59Z'); }
const total = db.prepare(`SELECT COUNT(*) as count FROM user_login_logs l ${where}`).get(...params).count;
const rows = db.prepare(`
SELECT l.id, l.user_id, l.username, l.full_name, l.role,
COALESCE(NULLIF(l.platform, ''), NULLIF(l.platform, 'unknown'), 'windows') as platform,
l.ip_address, l.login_at, l.logout_at
FROM user_login_logs l
${where}
ORDER BY l.login_at DESC
LIMIT ? OFFSET ?
`).all(...params, limit, offset);
res.json({ total, page, limit, logs: rows });
} catch (error) {
res.status(500).json({ message: 'Failed to fetch login logs', error: error.message });
}
};
// Export helpers for use in middleware / other controllers
exports.hashToken = hashToken;
exports.upsertSession = upsertSession;
exports.detectPlatform = detectPlatform;
exports.detectDeviceName = detectDeviceName;
+223 -112
View File
@@ -1,94 +1,51 @@
const fs = require('fs');
const path = require('path');
const db = require('../config/db'); // Needed to close the database on restore
const crypto = require('crypto');
const archiver = require('archiver');
const unzipper = require('unzipper');
const db = require('../config/db');
const INFO_FILE = path.resolve('./data/backup_info.json');
const LOGS_FILE = path.resolve('./data/backup_logs.txt');
const SCHEMA_VERSION = '2'; // bump when backup format changes
// ── Logging helpers ──────────────────────────────────────────────────────────
function logActivity(action) {
const timestamp = new Date().toISOString();
// Update JSON info
let info = {};
if (fs.existsSync(INFO_FILE)) {
try { info = JSON.parse(fs.readFileSync(INFO_FILE, 'utf8')); } catch(e) {}
try { info = JSON.parse(fs.readFileSync(INFO_FILE, 'utf8')); } catch (e) {}
}
if (action === 'backup') info.lastBackup = timestamp;
if (action === 'restore') info.lastRestore = timestamp;
fs.writeFileSync(INFO_FILE, JSON.stringify(info, null, 2));
// Append to log text
const logLine = `[${timestamp}] ${action.toUpperCase()} completed successfully.\n`;
fs.appendFileSync(LOGS_FILE, logLine);
fs.appendFileSync(LOGS_FILE, `[${timestamp}] ${action.toUpperCase()} completed successfully.\n`);
}
// Safely remove a directory on Windows (retries on EBUSY/EPERM, catches if still locked by Defender/indexing)
function safeRemoveDirSync(dirPath) {
if (!dirPath || !fs.existsSync(dirPath)) return;
try {
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
} catch (e) {
console.warn(`[Backup] Notice: Could not remove directory ${dirPath} immediately (${e.code || e.message}). It will be cleaned up later.`);
}
try { fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); }
catch (e) { console.warn(`[Backup] Could not remove ${dirPath}: ${e.message}`); }
}
// Safely remove a file on Windows (retries or catches if locked)
function safeRemoveFileSync(filePath) {
if (!filePath || !fs.existsSync(filePath)) return;
try {
fs.unlinkSync(filePath);
} catch (e) {
console.warn(`[Backup] Notice: Could not remove file ${filePath} immediately (${e.code || e.message}).`);
}
try { fs.unlinkSync(filePath); }
catch (e) { console.warn(`[Backup] Could not remove ${filePath}: ${e.message}`); }
}
exports.getBackupInfo = (req, res) => {
let info = { lastBackup: null, lastRestore: null };
if (fs.existsSync(INFO_FILE)) {
try { info = JSON.parse(fs.readFileSync(INFO_FILE, 'utf8')); } catch(e) {}
}
res.json(info);
};
exports.downloadBackup = async (req, res) => {
let tempDbBackupPath = null;
try {
const dataDir = path.resolve('./data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const dateStr = new Date().toISOString().split('T')[0];
const filename = `tis_rms_${dateStr}.db`;
// Create a safe online backup of the SQLite database using better-sqlite3 db.backup()
tempDbBackupPath = path.join(dataDir, `temp_backup_${Date.now()}_tis_rms.db`);
console.log('[Backup] Running db.backup() online SQLite backup...');
await db.backup(tempDbBackupPath);
// Download directly without zipping
res.download(tempDbBackupPath, filename, (err) => {
safeRemoveFileSync(tempDbBackupPath);
if (err && !res.headersSent) {
console.error('[Backup] Download send error:', err);
res.status(500).json({ message: 'Failed to send backup file', error: err.message });
}
});
// Log the backup activity
logActivity('backup');
} catch (error) {
if (tempDbBackupPath) {
safeRemoveFileSync(tempDbBackupPath);
}
console.error('[Backup] Download error:', error);
if (!res.headersSent) {
res.status(500).json({ message: 'Failed to generate backup', error: error.message });
}
}
};
// ── SHA-256 checksum ──────────────────────────────────────────────────────────
function sha256File(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
// ── Validate SQLite magic bytes ───────────────────────────────────────────────
function isSqliteFile(filePath) {
try {
const fd = fs.openSync(filePath, 'r');
@@ -96,77 +53,231 @@ function isSqliteFile(filePath) {
fs.readSync(fd, buffer, 0, 16, 0);
fs.closeSync(fd);
return buffer.toString('utf8', 0, 15) === 'SQLite format 3';
} catch (e) {
return false;
}
} catch (e) { return false; }
}
exports.restoreBackup = async (req, res) => {
// ── GET /api/backup/info ──────────────────────────────────────────────────────
exports.getBackupInfo = (req, res) => {
let info = { lastBackup: null, lastRestore: null };
if (fs.existsSync(INFO_FILE)) {
try { info = JSON.parse(fs.readFileSync(INFO_FILE, 'utf8')); } catch (e) {}
}
res.json(info);
};
// ── GET /api/backup/download ──────────────────────────────────────────────────
exports.downloadBackup = async (req, res) => {
const tempDbPath = path.resolve('./data', `temp_backup_${Date.now()}_tis_rms.db`);
try {
if (!req.file) {
return res.status(400).json({ message: 'No backup file provided.' });
const dataDir = path.resolve('./data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const dateStr = new Date().toISOString().split('T')[0];
const zipFileName = `tis_rms_backup_${dateStr}.zip`;
// 1. Online SQLite backup
console.log('[Backup] Running online SQLite backup…');
await db.backup(tempDbPath);
// 2. Build manifest with checksums
const dbChecksum = await sha256File(tempDbPath);
const manifest = {
schema_version: SCHEMA_VERSION,
created_at: new Date().toISOString(),
db_checksum_sha256: dbChecksum,
includes: ['tis_rms.db', 'students/', 'templates/'],
};
// 3. Stream ZIP to response
res.setHeader('Content-Disposition', `attachment; filename="${zipFileName}"`);
res.setHeader('Content-Type', 'application/zip');
const archive = archiver('zip', { zlib: { level: 6 } });
archive.on('warning', (err) => {
if (err.code !== 'ENOENT') throw err;
console.warn('[Backup] archiver warning:', err.message);
});
archive.on('error', (err) => {
console.error('[Backup] archiver error:', err);
if (!res.headersSent) res.status(500).json({ message: 'Backup archive failed', error: err.message });
});
archive.pipe(res);
// DB file
archive.file(tempDbPath, { name: 'tis_rms.db' });
// Students directory
const studentsDir = process.env.STUDENT_DIR_ROOT
? path.resolve(process.env.STUDENT_DIR_ROOT)
: path.resolve('./data/students');
if (fs.existsSync(studentsDir)) {
archive.directory(studentsDir, 'students');
}
console.log(`[Backup] Received restore file: ${req.file.originalname}`);
// Templates directory
const templatesDir = process.env.TEMPLATES_DIR
? path.resolve(process.env.TEMPLATES_DIR)
: path.resolve('./data/templates');
if (fs.existsSync(templatesDir)) {
archive.directory(templatesDir, 'templates');
}
// Manifest
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
await archive.finalize();
// Cleanup temp DB after stream finishes
res.on('finish', () => {
safeRemoveFileSync(tempDbPath);
logActivity('backup');
});
} catch (error) {
safeRemoveFileSync(tempDbPath);
console.error('[Backup] Download error:', error);
if (!res.headersSent) {
res.status(500).json({ message: 'Failed to generate backup', error: error.message });
}
}
};
// ── POST /api/backup/restore ──────────────────────────────────────────────────
exports.restoreBackup = async (req, res) => {
let tempExtractDir = null;
try {
if (!req.file) return res.status(400).json({ message: 'No backup file provided.' });
const filePath = req.file.path;
const originalName = (req.file.originalname || '').toLowerCase();
const isZip = originalName.endsWith('.zip');
const isDb = originalName.endsWith('.db') || originalName.endsWith('.sqlite') || originalName.endsWith('.sqlite3');
if (!isSqliteFile(filePath) && !originalName.endsWith('.db') && !originalName.endsWith('.sqlite') && !originalName.endsWith('.sqlite3')) {
if (!isZip && !isDb) {
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Invalid backup file. Please provide a valid SQLite .db backup file without zipping.' });
return res.status(400).json({ message: 'Invalid backup file. Provide a .zip backup (recommended) or a legacy .db file.' });
}
console.log('[Backup] Closing current database connection...');
// Close DB before overwriting
try {
// ── Legacy DB-only restore ────────────────────────────────────────────
if (isDb) {
if (!isSqliteFile(filePath)) {
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Invalid SQLite file.' });
}
console.log('[Backup] Legacy DB-only restore…');
const safetyPath = path.resolve('./data', `pre_restore_${Date.now()}.db`);
await db.backup(safetyPath);
db.close();
} catch (e) {
console.warn('[Backup] db.close warning:', e.message);
}
const currentDataDir = path.resolve('./data');
if (!fs.existsSync(currentDataDir)) {
fs.mkdirSync(currentDataDir, { recursive: true });
}
const currentDbPath = path.join(currentDataDir, 'tis_rms.db');
const currentDbWalPath = path.join(currentDataDir, 'tis_rms.db-wal');
const currentDbShmPath = path.join(currentDataDir, 'tis_rms.db-shm');
console.log('[Backup] Overwriting database...');
fs.copyFileSync(filePath, currentDbPath);
console.log('[Backup] Cleaning up any existing WAL and SHM files...');
if (fs.existsSync(currentDbWalPath)) {
safeRemoveFileSync(currentDbWalPath);
}
if (fs.existsSync(currentDbShmPath)) {
safeRemoveFileSync(currentDbShmPath);
}
// Cleanup uploaded file
if (fs.existsSync(filePath)) {
fs.copyFileSync(filePath, path.resolve('./data/tis_rms.db'));
['.db-wal', '.db-shm'].forEach(ext => {
const p = path.resolve('./data/tis_rms' + ext);
if (fs.existsSync(p)) safeRemoveFileSync(p);
});
safeRemoveFileSync(filePath);
logActivity('restore');
res.json({ success: true, message: 'Database restored (legacy mode — no file restore). Server will restart.', warning: 'Student and template files were not included in this backup.' });
setTimeout(() => process.exit(0), 2000);
return;
}
console.log('[Backup] Restore completed successfully. Shutting down server...');
// ── ZIP restore ───────────────────────────────────────────────────────
console.log('[Backup] ZIP restore started…');
tempExtractDir = path.resolve('./data', `temp_restore_${Date.now()}`);
fs.mkdirSync(tempExtractDir, { recursive: true });
// 1. Extract
await fs.createReadStream(filePath)
.pipe(unzipper.Extract({ path: tempExtractDir }))
.promise();
// 2. Read + validate manifest
const manifestPath = path.join(tempExtractDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
safeRemoveDirSync(tempExtractDir);
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Invalid backup: missing manifest.json.' });
}
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
console.log(`[Backup] Manifest: schema_version=${manifest.schema_version}, created_at=${manifest.created_at}`);
// 3. Verify DB checksum
const extractedDbPath = path.join(tempExtractDir, 'tis_rms.db');
if (!fs.existsSync(extractedDbPath)) {
safeRemoveDirSync(tempExtractDir);
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Invalid backup: tis_rms.db not found in archive.' });
}
if (!isSqliteFile(extractedDbPath)) {
safeRemoveDirSync(tempExtractDir);
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Backup database file is not a valid SQLite file.' });
}
if (manifest.db_checksum_sha256) {
const actualChecksum = await sha256File(extractedDbPath);
if (actualChecksum !== manifest.db_checksum_sha256) {
safeRemoveDirSync(tempExtractDir);
safeRemoveFileSync(filePath);
return res.status(400).json({ message: 'Backup integrity check failed: database checksum mismatch.' });
}
console.log('[Backup] DB checksum verified ✓');
}
// 4. Safety backup of current installation
const safetyPath = path.resolve('./data', `pre_restore_${Date.now()}.db`);
console.log(`[Backup] Creating safety backup → ${safetyPath}`);
await db.backup(safetyPath);
// 5. Close DB + overwrite
console.log('[Backup] Closing DB and restoring…');
db.close();
fs.copyFileSync(extractedDbPath, path.resolve('./data/tis_rms.db'));
['.db-wal', '.db-shm'].forEach(ext => {
const p = path.resolve('./data/tis_rms' + ext);
if (fs.existsSync(p)) safeRemoveFileSync(p);
});
// 6. Restore student files (merge)
const extractedStudentsDir = path.join(tempExtractDir, 'students');
if (fs.existsSync(extractedStudentsDir)) {
const studentsDir = process.env.STUDENT_DIR_ROOT
? path.resolve(process.env.STUDENT_DIR_ROOT)
: path.resolve('./data/students');
console.log(`[Backup] Restoring student files → ${studentsDir}`);
fs.cpSync(extractedStudentsDir, studentsDir, { recursive: true, force: true });
}
// 7. Restore template files (merge)
const extractedTemplatesDir = path.join(tempExtractDir, 'templates');
if (fs.existsSync(extractedTemplatesDir)) {
const templatesDir = process.env.TEMPLATES_DIR
? path.resolve(process.env.TEMPLATES_DIR)
: path.resolve('./data/templates');
console.log(`[Backup] Restoring template files → ${templatesDir}`);
fs.cpSync(extractedTemplatesDir, templatesDir, { recursive: true, force: true });
}
// 8. Cleanup
safeRemoveDirSync(tempExtractDir);
safeRemoveFileSync(filePath);
// Log the restore activity
logActivity('restore');
// Send response first, then exit after a short delay
res.json({ success: true, message: 'Database restored successfully. Server will shut down now.' });
res.json({ success: true, message: 'Backup restored successfully (DB + files). Server will restart.' });
setTimeout(() => {
console.log('Exiting process for restart...');
console.log('[Backup] Restarting after restore…');
process.exit(0);
}, 2000);
} catch (error) {
console.error('[Backup] Restore error:', error);
if (req.file && fs.existsSync(req.file.path)) {
safeRemoveFileSync(req.file.path);
if (tempExtractDir) safeRemoveDirSync(tempExtractDir);
if (req.file?.path && fs.existsSync(req.file.path)) safeRemoveFileSync(req.file.path);
if (!res.headersSent) {
res.status(500).json({ message: 'Failed to restore backup', error: error.message });
}
res.status(500).json({ message: 'Failed to restore backup', error: error.message });
}
};
@@ -36,10 +36,12 @@ exports.heartbeat = (req, res) => {
const now = new Date().toISOString();
const existing = connectedUsers.get(username);
const effectivePlatform = platform || req.headers['x-platform'] || req.headers['x-client-platform'] || existing?.platform || 'windows';
connectedUsers.set(username, {
username,
role: role || existing?.role || 'unknown',
platform: platform || existing?.platform || 'unknown',
platform: effectivePlatform,
ip: ip || existing?.ip || null,
loginTime: existing?.loginTime || now,
lastSeen: now,
+33 -4
View File
@@ -12,7 +12,7 @@ exports.getStats = (req, res) => {
let totalStudents;
let completedDocuments;
let missingDocuments;
const activeUsers = db.prepare('SELECT COUNT(*) as count FROM users WHERE is_active = 1').get().count;
const activeUsers = db.prepare('SELECT COUNT(*) as count FROM users WHERE is_active = 1 AND COALESCE(is_hidden, 0) = 0').get().count;
// Resolve Active Academic Year
const activeAy = db.prepare("SELECT * FROM academic_years WHERE status = 'active' LIMIT 1").get()
@@ -146,7 +146,8 @@ exports.getRecentActivities = (req, res) => {
const offset = (page - 1) * limit;
const dateFrom = req.query.date_from || '';
const dateTo = req.query.date_to || '';
const action = req.query.action || '';
const action = (req.query.action || '').trim();
const search = (req.query.search || '').trim();
// Comma-separated entity type filter, e.g. "student,document" (teacher view)
const entityTypesRaw = req.query.entity_types || '';
@@ -155,7 +156,29 @@ exports.getRecentActivities = (req, res) => {
if (dateFrom) { conditions.push("DATE(a.created_at) >= DATE(?)"); params.push(dateFrom); }
if (dateTo) { conditions.push("DATE(a.created_at) <= DATE(?)"); params.push(dateTo); }
if (action) { conditions.push("a.action = ?"); params.push(action); }
if (action) {
const actUpper = action.toUpperCase();
if (actUpper === 'ALL' || actUpper === 'ALL ACTIONS') {
// no action filter
} else if (actUpper === 'ADD' || actUpper === 'CREATE') {
conditions.push("(UPPER(a.action) IN ('CREATE', 'ADD'))");
} else if (actUpper === 'UPDATE') {
conditions.push("UPPER(a.action) = 'UPDATE'");
} else if (actUpper === 'DELETE') {
conditions.push("UPPER(a.action) = 'DELETE'");
} else if (actUpper === 'ARCHIVE') {
conditions.push("(UPPER(a.action) = 'ARCHIVE' OR a.description LIKE '%archive%' OR a.description LIKE '%Inactive%' OR a.description LIKE '%Graduated%')");
} else {
conditions.push("UPPER(a.action) = ?");
params.push(actUpper);
}
}
if (search) {
conditions.push("(a.description LIKE ? OR COALESCE(u.username, dh.username, 'System') LIKE ? OR COALESCE(u.first_name || ' ' || u.last_name, dh.full_name, 'System') LIKE ?)");
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
}
if (entityTypesRaw) {
const types = entityTypesRaw.split(',').map(t => t.trim()).filter(Boolean);
@@ -200,7 +223,13 @@ exports.getRecentActivities = (req, res) => {
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const total = db.prepare(`SELECT COUNT(*) as count FROM activity_log a ${where}`).get(params).count;
const total = db.prepare(`
SELECT COUNT(*) as count
FROM activity_log a
LEFT JOIN users u ON a.user_id = u.id
LEFT JOIN deleted_users_history dh ON a.user_id = dh.deleted_user_id
${where}
`).get(params).count;
const rows = db.prepare(`
SELECT
+267 -95
View File
@@ -6,9 +6,9 @@ const { execFile } = require('child_process');
const util = require('util');
const execFileAsync = util.promisify(execFile);
const { createNotification } = require('./notificationController');
const LibreOfficeService = require('../services/libreOfficeService');
const sharp = require('sharp');
const emailService = require('../services/emailService');
const pdfService = require('../services/pdfService');
// ── Helper: insert one row into activity_log ─────────────────────────────────
const logActivity = (userId, action, entityType, entityId, description) => {
@@ -216,17 +216,40 @@ exports.uploadDocument = (req, res) => {
const fileSize = file.size || (file.path && fs.existsSync(file.path) ? fs.statSync(file.path).size : null);
const result = db.prepare(`
INSERT INTO documents (student_id, requirement_id, file_name, file_path, document_type, status, uploaded_by, file_size)
VALUES (?, ?, ?, ?, ?, 'Completed', ?, ?)
`).run(studentId, reqId, file.originalname, file.path, documentType, req.user.id, fileSize);
INSERT INTO documents (student_id, requirement_id, file_name, file_path, document_type, status, uploaded_by, file_size, template_id, template_version_id)
VALUES (?, ?, ?, ?, ?, 'Completed', ?, ?, ?, ?)
`).run(
studentId, reqId, file.originalname, file.path, documentType,
req.user.id, fileSize,
req.body.templateId ?? null,
req.body.templateVersionId ?? null
);
const docId = result.lastInsertRowid;
// Auto-seed Version 1
try {
db.prepare(`
INSERT INTO document_versions
(document_id, version_number, file_name, file_path, file_size, template_id, template_version_id, uploaded_by)
VALUES (?, 1, ?, ?, ?, ?, ?, ?)
`).run(
docId, file.originalname, file.path, fileSize,
req.body.templateId ?? null,
req.body.templateVersionId ?? null,
req.user.id
);
} catch (verErr) {
console.error('[uploadDocument] Failed to seed document_versions:', verErr.message);
}
// Log activity
const student = db.prepare('SELECT first_name, last_name FROM students WHERE id = ?').get(studentId);
const studentName = student ? `${student.first_name} ${student.last_name}` : `Student #${studentId}`;
logActivity(req.user.id, 'CREATE', 'document', result.lastInsertRowid,
logActivity(req.user.id, 'CREATE', 'document', docId,
`Uploaded "${file.originalname}" (${documentType}) for ${studentName}`);
createNotification(null, 'Document Uploaded', `Document "${file.originalname}" (${documentType}) has been uploaded for ${studentName}.`, 'document', 'document', result.lastInsertRowid);
createNotification(null, 'Document Uploaded', `Document "${file.originalname}" (${documentType}) has been uploaded for ${studentName}.`, 'document', 'document', docId);
// Check auto-enrollment update if document is SF10 or SF9
try {
@@ -242,7 +265,7 @@ exports.uploadDocument = (req, res) => {
console.error('[uploadDocument] autoEnrollFromSF trigger error:', sfErr.message);
}
res.status(201).json({ id: result.lastInsertRowid, message: 'Document uploaded successfully' });
res.status(201).json({ id: docId, message: 'Document uploaded successfully' });
} catch (error) {
console.error('Upload Error:', error);
res.status(500).json({ message: 'Failed to upload document', error: error.message });
@@ -427,27 +450,45 @@ exports.getAllDocuments = (req, res) => {
if (types.length > 0) {
const typeConditions = [];
for (const t of types) {
if (t === 'All JHS') {
if (t === 'All JHS' || t === 'All JHS Requirements') {
typeConditions.push(`dr.category = 'JHS'`);
} else if (t === 'All SHS') {
} else if (t === 'All SHS' || t === 'All SHS Requirements') {
typeConditions.push(`dr.category = 'SHS'`);
} else {
typeConditions.push(`(d.document_type = ? OR dr.name = ?)`);
params.push(t, t);
const cleanT = t.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
typeConditions.push(`(
d.document_type = ?
OR dr.name = ?
OR d.document_type = ?
OR d.document_type = ?
OR dr.name = ?
)`);
params.push(t, t, `JHS - ${cleanT}`, `SHS - ${cleanT}`, cleanT);
}
}
conditions.push(`(${typeConditions.join(' OR ')})`);
}
}
if (gradeLevel.trim()) {
conditions.push(`e.grade_level = ?`);
params.push(gradeLevel.trim());
if (gradeLevel.trim()) {
const gradeMatch = gradeLevel.trim().match(/\d+/);
if (gradeMatch) {
conditions.push(`e.grade_level = ?`);
params.push(parseInt(gradeMatch[0]));
}
}
if (schoolYear.trim()) {
conditions.push(`ay.year_range = ?`);
params.push(schoolYear.trim());
} else {
conditions.push(`LOWER(ay.status) = ?`);
params.push('active');
}
if (false) {
conditions.push(`ay.year_range = ?`);
params.push(schoolYear.trim());
}
if (studentId.trim()) {
@@ -575,6 +616,60 @@ exports.getStatuses = (req, res) => {
}
};
exports.getDocumentTypes = (req, res) => {
try {
const reqRows = db.prepare(`
SELECT DISTINCT name, category
FROM document_requirements
WHERE is_enabled = 1
ORDER BY name ASC
`).all();
const docRows = db.prepare(`
SELECT DISTINCT document_type
FROM documents
WHERE document_type IS NOT NULL AND document_type != ''
ORDER BY document_type ASC
`).all();
const jhsSet = new Set();
const shsSet = new Set();
const generalSet = new Set();
for (const r of reqRows) {
const cleanName = r.name.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
if (r.category === 'JHS') {
jhsSet.add(cleanName);
} else if (r.category === 'SHS') {
shsSet.add(cleanName);
} else {
generalSet.add(cleanName);
}
}
for (const d of docRows) {
const dt = d.document_type.trim();
if (/^JHS\s*-\s*/i.test(dt)) {
jhsSet.add(dt.replace(/^JHS\s*-\s*/i, '').trim());
} else if (/^SHS\s*-\s*/i.test(dt)) {
shsSet.add(dt.replace(/^SHS\s*-\s*/i, '').trim());
} else if (dt) {
if (!jhsSet.has(dt) && !shsSet.has(dt)) {
generalSet.add(dt);
}
}
}
res.json({
jhs: Array.from(jhsSet).sort(),
shs: Array.from(shsSet).sort(),
general: Array.from(generalSet).sort(),
});
} catch (error) {
res.status(500).json({ message: 'Failed to fetch document types', error: error.message });
}
};
// ============================================================
// GET /api/documents/student/:studentId — get single student's documents
// ============================================================
@@ -878,87 +973,6 @@ exports.copyDocument = (req, res) => {
}
};
// ============================================================
// POST /api/documents/:id/convert-to-pdf — convert Excel to PDF
// ============================================================
exports.convertToPdf = async (req, res) => {
const { id } = req.params;
try {
const doc = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
if (!doc) return res.status(404).json({ message: 'Document not found' });
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
if (isTeacher) {
const hasAccess = db.prepare(`
SELECT 1 FROM enrollments e
JOIN teacher_sections ts ON e.section_id = ts.section_id
WHERE e.student_id = ? AND ts.teacher_id = ?
`).get(doc.student_id, req.user.id);
if (!hasAccess) {
return res.status(403).json({ message: 'Access denied to convert this document.' });
}
}
const originalPath = doc.file_path;
if (!fs.existsSync(originalPath)) {
return res.status(404).json({ message: 'Physical file not found on disk' });
}
const ext = path.extname(doc.file_name).toLowerCase();
const validExcelExts = ['.xlsx', '.xls', '.csv'];
if (!validExcelExts.includes(ext)) {
return res.status(400).json({ message: 'Only Excel files (.xlsx, .xls, .csv) can be converted to PDF.' });
}
const dir = path.dirname(originalPath);
const baseName = path.basename(doc.file_name, ext);
// Perform conversion with headless LibreOffice
const { pdfPath: tempPdfPath } = await LibreOfficeService.convertToPdf(originalPath, dir);
// Determine non-colliding new filename with .pdf extension
const existingNames = db.prepare(
'SELECT file_name FROM documents WHERE student_id = ? AND deleted_at IS NULL'
).all(doc.student_id).map((r) => r.file_name);
let newFileName = `${baseName}.pdf`;
let counter = 1;
while (existingNames.includes(newFileName)) {
newFileName = `${baseName} (${counter}).pdf`;
counter++;
}
const finalPdfPath = path.join(dir, `${Date.now()}-${newFileName.replace(/[^a-zA-Z0-9.-]/g, '_')}`);
fs.renameSync(tempPdfPath, finalPdfPath);
// Insert new PDF document row into database
const convertedFileSize = fs.existsSync(finalPdfPath) ? fs.statSync(finalPdfPath).size : null;
const result = db.prepare(`
INSERT INTO documents (student_id, requirement_id, file_name, file_path, document_type, status, uploaded_by, file_size)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(doc.student_id, doc.requirement_id, newFileName, finalPdfPath, doc.document_type, doc.status, req.user.id, convertedFileSize);
const newDoc = db.prepare('SELECT * FROM documents WHERE id = ?').get(result.lastInsertRowid);
// Log activity
const student = db.prepare('SELECT first_name, last_name FROM students WHERE id = ?').get(doc.student_id);
const studentName = student ? `${student.first_name} ${student.last_name}` : `Student #${doc.student_id}`;
logActivity(req.user.id, 'CREATE', 'document', result.lastInsertRowid,
`Converted Excel document "${doc.file_name}" to PDF "${newFileName}" for ${studentName}`);
createNotification(null, 'Document Converted', `Document "${doc.file_name}" converted to PDF for ${studentName}.`, 'document', 'document', result.lastInsertRowid);
res.status(201).json({
id: result.lastInsertRowid,
fileName: newFileName,
document: newDoc,
message: 'Document converted to PDF successfully',
});
} catch (error) {
console.error('ConvertToPdf Error:', error);
res.status(500).json({ message: error.message || 'Failed to convert document to PDF' });
}
};
// ============================================================
// POST /api/documents/bulk-delete — bulk delete documents
// ============================================================
@@ -1414,6 +1428,22 @@ exports.executePrintQueue = (req, res) => {
}
};
// POST /api/documents/print-queue/merge-pdf
exports.mergePrintQueuePdf = async (req, res) => {
try {
const userId = req.user.id;
const { documentIds } = req.body || {};
const pdfBuffer = await pdfService.mergeDocumentsToPdf({ documentIds, userId });
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'inline; filename="merged_documents.pdf"');
res.send(pdfBuffer);
} catch (error) {
console.error('mergePrintQueuePdf error:', error);
res.status(500).json({ message: 'Failed to merge documents into PDF', error: error.message });
}
};
// ============================================================
// Expired Soft Delete Cleanup (30 days)
// ============================================================
@@ -1486,3 +1516,145 @@ exports.sendPrintPickupNotification = async (req, res) => {
}
};
// ============================================================
// GET /api/documents/:id/versions — list all versions
// ============================================================
exports.getDocumentVersions = (req, res) => {
try {
const doc = db.prepare('SELECT id, student_id FROM documents WHERE id = ? AND deleted_at IS NULL').get(req.params.id);
if (!doc) return res.status(404).json({ message: 'Document not found' });
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
if (isTeacher) {
const hasAccess = db.prepare(`
SELECT 1 FROM enrollments e
JOIN teacher_sections ts ON e.section_id = ts.section_id
WHERE e.student_id = ? AND ts.teacher_id = ?
`).get(doc.student_id, req.user.id);
if (!hasAccess) return res.status(403).json({ message: 'Access denied.' });
}
const versions = db.prepare(`
SELECT dv.id, dv.version_number, dv.file_name, dv.file_size, dv.notes,
dv.uploaded_by, dv.created_at,
u.first_name || ' ' || u.last_name AS uploaded_by_name,
dv.template_id, dv.template_version_id,
tv.version_number AS template_version_number,
dt.name AS template_name
FROM document_versions dv
LEFT JOIN users u ON dv.uploaded_by = u.id
LEFT JOIN template_versions tv ON dv.template_version_id = tv.id
LEFT JOIN document_templates dt ON dv.template_id = dt.id
WHERE dv.document_id = ?
ORDER BY dv.version_number DESC
`).all(req.params.id);
res.json(versions);
} catch (error) {
console.error('getDocumentVersions error:', error);
res.status(500).json({ message: 'Failed to fetch document versions', error: error.message });
}
};
// ============================================================
// POST /api/documents/:id/upload-version — upload a new version
// ============================================================
exports.uploadDocumentVersion = (req, res) => {
const file = req.file;
if (!file) return res.status(400).json({ message: 'No file uploaded' });
try {
const doc = db.prepare('SELECT id, student_id, document_type, requirement_id FROM documents WHERE id = ? AND deleted_at IS NULL').get(req.params.id);
if (!doc) {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
return res.status(404).json({ message: 'Document not found' });
}
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
if (isTeacher) {
const hasAccess = db.prepare(`
SELECT 1 FROM enrollments e
JOIN teacher_sections ts ON e.section_id = ts.section_id
WHERE e.student_id = ? AND ts.teacher_id = ?
`).get(doc.student_id, req.user.id);
if (!hasAccess) {
if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
return res.status(403).json({ message: 'Access denied.' });
}
}
const lastVersion = db.prepare(
'SELECT MAX(version_number) AS max_v FROM document_versions WHERE document_id = ?'
).get(req.params.id);
const nextVersion = (lastVersion?.max_v ?? 0) + 1;
const fileSize = file.size || (fs.existsSync(file.path) ? fs.statSync(file.path).size : null);
const notes = req.body.notes ?? null;
db.prepare(`
INSERT INTO document_versions
(document_id, version_number, file_name, file_path, file_size, uploaded_by, notes, template_id, template_version_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
req.params.id, nextVersion, file.originalname, file.path, fileSize,
req.user.id, notes,
req.body.templateId ?? null,
req.body.templateVersionId ?? null
);
// Update document header: file_name + file_path always reflect latest version
db.prepare(`
UPDATE documents SET file_name = ?, file_path = ?, file_size = ?,
template_id = COALESCE(?, template_id),
template_version_id = COALESCE(?, template_version_id)
WHERE id = ?
`).run(
file.originalname, file.path, fileSize,
req.body.templateId ?? null,
req.body.templateVersionId ?? null,
req.params.id
);
logActivity(req.user.id, 'UPDATE', 'document', req.params.id,
`Uploaded version ${nextVersion} for document id ${req.params.id}`);
res.status(201).json({ message: `Version ${nextVersion} uploaded successfully`, version_number: nextVersion });
} catch (error) {
console.error('uploadDocumentVersion error:', error);
if (file && fs.existsSync(file.path)) fs.unlinkSync(file.path);
res.status(500).json({ message: 'Failed to upload document version', error: error.message });
}
};
// ============================================================
// GET /api/documents/:id/download — download latest version file
// ============================================================
exports.downloadDocumentVersion = (req, res) => {
try {
const { versionId } = req.query;
let filePath, fileName;
if (versionId) {
const ver = db.prepare(`
SELECT dv.file_path, dv.file_name, d.student_id
FROM document_versions dv
JOIN documents d ON dv.document_id = d.id
WHERE dv.id = ? AND dv.document_id = ?
`).get(versionId, req.params.id);
if (!ver) return res.status(404).json({ message: 'Version not found' });
filePath = ver.file_path;
fileName = ver.file_name;
} else {
const doc = db.prepare('SELECT file_path, file_name FROM documents WHERE id = ? AND deleted_at IS NULL').get(req.params.id);
if (!doc) return res.status(404).json({ message: 'Document not found' });
filePath = doc.file_path;
fileName = doc.file_name;
}
if (!fs.existsSync(filePath)) return res.status(404).json({ message: 'File not found on disk' });
res.download(path.resolve(filePath), fileName);
} catch (error) {
console.error('downloadDocumentVersion error:', error);
res.status(500).json({ message: 'Failed to download document version', error: error.message });
}
};
+56 -7
View File
@@ -14,7 +14,7 @@ const sanitizeFolderName = (str) =>
// GET /api/folders — list folders (manual + auto-created)
// ============================================================
exports.getFolders = (req, res) => {
const { studentId, parentId, search = '' } = req.query;
const { studentId, parentId, search = '', gradeLevel = '', section = '', academicYear = '' } = req.query;
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
const teacherId = req.user?.id;
@@ -30,11 +30,13 @@ exports.getFolders = (req, res) => {
`).run();
const conditions = [];
const params = [];
const whereParams = [];
const enrollmentParams = [];
const teacherParams = [];
if (studentId) {
conditions.push('f.student_id = ?');
params.push(studentId);
whereParams.push(studentId);
}
if (parentId !== undefined) {
@@ -42,18 +44,63 @@ exports.getFolders = (req, res) => {
conditions.push('f.parent_id IS NULL');
} else {
conditions.push('f.parent_id = ?');
params.push(parseInt(parentId));
whereParams.push(parseInt(parentId));
}
} else if (!studentId) {
conditions.push('f.parent_id IS NULL');
}
if (search.trim()) {
conditions.push('f.name LIKE ?');
params.push(`%${search.trim().split('').join('%' )}%`);
whereParams.push(`%${search.trim().split('').join('%')}%`);
}
// Only show folders for Enrolled students (or manual subfolders)
conditions.push("(f.student_id IS NULL OR s.status = 'Enrolled')");
// Resolve target academic year: explicitly requested or active academic year
let targetYearRange = academicYear.trim();
if (!targetYearRange || targetYearRange === 'All Years') {
const activeAy = db.prepare("SELECT year_range FROM academic_years WHERE LOWER(status) = 'active' LIMIT 1").get();
if (activeAy) {
targetYearRange = activeAy.year_range;
}
}
let enrollmentJoin = '';
if (gradeLevel.trim() || section.trim() || targetYearRange) {
enrollmentJoin = `JOIN (
SELECT e1.student_id, e1.grade_level, sec.name as section_name
FROM enrollments e1
JOIN academic_years ay1 ON e1.academic_year_id = ay1.id
LEFT JOIN sections sec ON e1.section_id = sec.id
WHERE e1.id = (
SELECT e2.id
FROM enrollments e2
JOIN academic_years ay2 ON e2.academic_year_id = ay2.id
WHERE e2.student_id = e1.student_id
${targetYearRange ? 'AND ay2.year_range = ?' : "AND LOWER(ay2.status) = 'active'"}
ORDER BY e2.id DESC
LIMIT 1
)
) latest_enr ON latest_enr.student_id = f.student_id`;
if (targetYearRange) {
enrollmentParams.push(targetYearRange);
}
if (gradeLevel.trim()) {
const gradeMatch = gradeLevel.match(/\d+/);
if (gradeMatch) {
conditions.push('latest_enr.grade_level = ?');
whereParams.push(parseInt(gradeMatch[0]));
}
}
if (section.trim()) {
conditions.push('latest_enr.section_name = ?');
whereParams.push(section.trim());
}
}
const whereClause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
// When the caller is a teacher, restrict to folders for students in
@@ -65,7 +112,7 @@ exports.getFolders = (req, res) => {
JOIN teacher_sections ts_sub ON e_sub.section_id = ts_sub.section_id
WHERE ts_sub.teacher_id = ?
) ts_scope ON ts_scope.student_id = f.student_id`;
params.unshift(teacherId);
teacherParams.push(teacherId);
}
const sql = `
@@ -101,6 +148,7 @@ exports.getFolders = (req, res) => {
) as shs_completed
FROM document_folders f
${teacherJoinSql}
${enrollmentJoin}
LEFT JOIN students s ON f.student_id = s.id
LEFT JOIN users u ON f.created_by = u.id
LEFT JOIN deleted_users_history dh ON f.created_by = dh.deleted_user_id
@@ -108,7 +156,8 @@ exports.getFolders = (req, res) => {
ORDER BY f.name ASC
`;
const folders = db.prepare(sql).all(params);
const finalParams = [...teacherParams, ...enrollmentParams, ...whereParams];
const folders = db.prepare(sql).all(...finalParams);
res.json(folders);
} catch (error) {
console.error('getFolders error:', error);
@@ -49,6 +49,21 @@ exports.createNotification = (userId, title, message, category = 'system', entit
entityId,
notificationId: notifId
}).catch(() => {});
// Real-time WebSocket notification emission to active clients
try {
const { emitNotificationCreated } = require('../services/socketService');
emitNotificationCreated({
id: notifId,
userId: userId || null,
title,
message,
category,
entityType,
entityId,
createdAt: new Date().toISOString(),
});
} catch (_) {}
};
// POST /api/notifications/fcm-token
+239 -8
View File
@@ -1,4 +1,6 @@
const db = require('../config/db');
const pdfService = require('../services/pdfService');
const excelService = require('../services/excelService');
// GET /api/reports/academic-years
exports.getAcademicYears = (req, res) => {
@@ -94,7 +96,7 @@ exports.getStats = (req, res) => {
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND d.status = 'Completed'
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
GROUP BY r.id, r.category, r.name
@@ -126,7 +128,7 @@ exports.getStats = (req, res) => {
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND d.status = 'Completed'
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_count,
@@ -143,7 +145,7 @@ exports.getStats = (req, res) => {
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND d.status = 'Completed'
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_requirements
@@ -368,9 +370,8 @@ exports.getStorageUsed = async (req, res) => {
};
// GET /api/reports/transparency-board
exports.getTransparencyBoardData = (req, res) => {
try {
const { academicYearId, yearIds } = req.query;
function fetchTransparencyBoardPayload(query = {}) {
const { academicYearId, yearIds } = query;
let academicYears = [];
if (yearIds) {
@@ -600,12 +601,242 @@ exports.getTransparencyBoardData = (req, res) => {
overallPercentage: 0
};
res.json({
return {
years: yearsData,
equity4Ps
});
};
}
// GET /api/reports/transparency-board
exports.getTransparencyBoardData = (req, res) => {
try {
const data = fetchTransparencyBoardPayload(req.query);
res.json(data);
} catch (error) {
res.status(500).json({ message: 'Failed to fetch transparency board data', error: error.message });
}
};
// GET /api/reports/transparency-board/pdf
exports.generateTransparencyBoardPdf = async (req, res) => {
try {
const data = fetchTransparencyBoardPayload(req.query);
const { schoolName, divisionName, regionName, category } = req.query;
const normalizedCategory = (category || 'all').toLowerCase();
const pdfBuffer = await pdfService.generateTransparencyBoardPdf(data, {
schoolName,
divisionName,
regionName,
category: normalizedCategory,
});
const latestYear = data.years && data.years.length > 0 ? data.years[data.years.length - 1] : null;
const yearLabel = latestYear ? latestYear.yearRange.replace(/-/g, '_') : 'All';
let filename = `DepEd_Transparency_Board_SY_${yearLabel}.pdf`;
if (normalizedCategory === 'enrollment') {
filename = `DepEd_Enrollment_Report_SY_${yearLabel}.pdf`;
} else if (normalizedCategory === 'dropouts_transferees' || normalizedCategory === 'dropouts') {
filename = `DepEd_Dropouts_Transferees_SY_${yearLabel}.pdf`;
} else if (normalizedCategory === '4ps' || normalizedCategory === 'four_ps' || normalizedCategory === 'equity4ps') {
filename = `DepEd_4Ps_Beneficiaries_SY_${yearLabel}.pdf`;
}
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
res.send(pdfBuffer);
} catch (error) {
console.error('generateTransparencyBoardPdf error:', error);
res.status(500).json({ message: 'Failed to generate transparency board PDF', error: error.message });
}
};
// Helper to fetch compliance stats data for Excel export
function fetchComplianceStatsPayload(query = {}, user = null) {
const { academicYearId, gradeLevel, sectionId, status } = query;
let params = [];
let enrollParams = [];
let whereClauses = [];
let enrollWhereClauses = [];
if (user?.role === 'teacher') {
whereClauses.push('e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?)');
enrollWhereClauses.push('e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?)');
params.push(user.id);
enrollParams.push(user.id);
}
if (academicYearId) {
whereClauses.push('e.academic_year_id = ?');
enrollWhereClauses.push('e.academic_year_id = ?');
params.push(academicYearId);
enrollParams.push(academicYearId);
}
if (gradeLevel) {
whereClauses.push('e.grade_level = ?');
enrollWhereClauses.push('e.grade_level = ?');
params.push(gradeLevel);
enrollParams.push(gradeLevel);
}
if (sectionId) {
whereClauses.push('e.section_id = ?');
enrollWhereClauses.push('e.section_id = ?');
params.push(sectionId);
enrollParams.push(sectionId);
}
const whereSql = whereClauses.length > 0 ? 'AND ' + whereClauses.join(' AND ') : '';
const enrollWhereSql = enrollWhereClauses.length > 0 ? 'AND ' + enrollWhereClauses.join(' AND ') : '';
const countsQuery = `
SELECT
COUNT(DISTINCT CASE WHEN s.status = 'Enrolled' THEN s.id END) as active,
COUNT(DISTINCT CASE WHEN s.status = 'Inactive' THEN s.id END) as inactive,
COUNT(DISTINCT CASE WHEN s.status = 'Dropped' THEN s.id END) as dropped,
COUNT(DISTINCT CASE WHEN s.status = 'Transferred' THEN s.id END) as transferee,
COUNT(DISTINCT CASE WHEN s.status = 'Graduated' THEN s.id END) as graduated,
COUNT(DISTINCT CASE WHEN s.is_4ps = 1 THEN s.id END) as fourPs
FROM students s
JOIN enrollments e ON s.id = e.student_id
AND e.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
WHERE 1=1 ${enrollWhereSql}
`;
const studentCounts = db.prepare(countsQuery).get(enrollParams) || {};
const missingQuery = `
SELECT r.id as requirementId, r.category || ' - ' || r.name as name, COUNT(DISTINCT s.id) as count
FROM document_requirements r
CROSS JOIN students s
JOIN enrollments e ON s.id = e.student_id
AND e.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
${whereSql}
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
GROUP BY r.id, r.category, r.name
ORDER BY count DESC
`;
const missingDocsBreakdown = db.prepare(missingQuery).all(params);
let studentFilterSql = whereSql;
let studentParams = [...params];
if (status) {
studentFilterSql += " AND s.status = ?";
studentParams.push(status);
}
const studentsQuery = `
SELECT s.id, s.lrn, s.first_name, s.last_name, s.sex, s.status,
e_latest.grade_level, sec.name as section_name,
(
SELECT COUNT(*)
FROM document_requirements r
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_count,
(
SELECT group_concat('[' || r.category || '] ' || r.name, ', ')
FROM document_requirements r
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_requirements
FROM students s
JOIN enrollments e_latest ON s.id = e_latest.student_id
AND e_latest.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
LEFT JOIN sections sec ON e_latest.section_id = sec.id
WHERE 1=1 ${studentFilterSql.replace(/\be\./g, 'e_latest.')}
ORDER BY s.last_name ASC, s.first_name ASC
LIMIT 1000
`;
const studentsList = db.prepare(studentsQuery).all(studentParams);
let yearLabel = 'All Years';
if (academicYearId) {
const ay = db.prepare('SELECT year_range FROM academic_years WHERE id = ?').get(academicYearId);
if (ay && ay.year_range) {
yearLabel = ay.year_range;
}
}
return {
studentCounts,
missingDocsBreakdown,
students: studentsList,
yearLabel
};
}
// GET or POST /api/reports/compliance/excel
exports.generateComplianceReportExcel = async (req, res) => {
try {
let payload;
if (req.body && req.body.students && req.body.studentCounts) {
payload = req.body;
} else {
payload = fetchComplianceStatsPayload(req.query, req.user);
}
const excelBuffer = await excelService.generateComplianceExcel(payload);
const yearSlug = (payload.yearLabel || 'All_Years').replace(/[^a-zA-Z0-9]/g, '_');
const filename = `TIS_RMS_Report_${yearSlug}_${Date.now()}.xlsx`;
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(excelBuffer);
} catch (error) {
console.error('generateComplianceReportExcel error:', error);
res.status(500).json({ message: 'Failed to generate compliance report Excel', error: error.message });
}
};
+15 -1
View File
@@ -1,10 +1,15 @@
const db = require('../config/db');
const autoArchiveService = require('../services/autoArchiveService');
const { checkAndRunTeacherDueDateReminders } = require('../services/teacherReminderService');
exports.getSettings = (req, res) => {
try {
const rows = db.prepare('SELECT key, value FROM system_settings').all();
const settings = {
auto_update_enrollment_from_sf: 'true'
auto_update_enrollment_from_sf: 'false',
enrollment_grace_period_days: '30',
remind_teachers_due_date_enabled: 'false',
remind_teachers_due_date_days: '3'
};
for (const row of rows) {
settings[row.key] = row.value;
@@ -49,6 +54,15 @@ exports.updateSettings = (req, res) => {
insertOrUpdate(updates);
if (updates.enrollment_grace_period_days !== undefined || updates.auto_archive_datetime !== undefined || updates.auto_archive_enabled !== undefined) {
autoArchiveService.checkAndRunAutoArchive(req.user?.id);
autoArchiveService.scheduleAutoArchiveTimer();
}
if (updates.remind_teachers_due_date_enabled !== undefined || updates.remind_teachers_due_date_days !== undefined) {
checkAndRunTeacherDueDateReminders();
}
const rows = db.prepare('SELECT key, value FROM system_settings').all();
const updatedSettings = {};
for (const row of rows) {
+207 -10
View File
@@ -1,5 +1,6 @@
const db = require('../config/db');
const autoGraduationService = require('../services/autoGraduationService');
const autoArchiveService = require('../services/autoArchiveService');
// ============================================================
// ACADEMIC YEAR AUTOMATION
@@ -85,10 +86,11 @@ exports.createAcademicYear = (req, res) => {
const finalStatus = status || 'inactive';
const result = db.prepare('INSERT INTO academic_years (year_range, status, start_date, end_date) VALUES (?, ?, ?, ?)')
.run(normalizedRange, finalStatus, sDate, eDate);
// If the new year is active, deactivate all others and check graduation
// If the new year is active, deactivate all others and check graduation & archiving
if (finalStatus === 'active') {
deactivateOtherYears(result.lastInsertRowid);
autoGraduationService.checkAndRunAutoGraduation(req.user?.id);
autoArchiveService.checkAndRunAutoArchive(req.user?.id);
}
res.status(201).json({ id: result.lastInsertRowid, message: 'Academic year created successfully' });
} catch (error) {
@@ -142,6 +144,7 @@ exports.updateAcademicYear = (req, res) => {
if (finalStatus === 'active') {
deactivateOtherYears(parseInt(id));
autoGraduationService.checkAndRunAutoGraduation(req.user?.id);
autoArchiveService.checkAndRunAutoArchive(req.user?.id);
}
res.json({ message: 'Academic year updated successfully' });
} catch (error) {
@@ -161,6 +164,15 @@ exports.checkAutoGraduation = (req, res) => {
}
};
exports.checkAutoArchive = (req, res) => {
try {
const result = autoArchiveService.checkAndRunAutoArchive(req.user?.id);
res.json(result);
} catch (error) {
res.status(500).json({ message: 'Failed to check auto archive', error: error.message });
}
};
exports.deleteAcademicYear = (req, res) => {
const { id } = req.params;
try {
@@ -181,9 +193,14 @@ exports.deleteAcademicYear = (req, res) => {
exports.getAllSections = (req, res) => {
try {
const sections = db.prepare(`
SELECT s.*, ay.year_range as academic_year_range
SELECT s.*, ay.year_range as academic_year_range,
ts.teacher_id,
u.first_name as teacher_first_name,
u.last_name as teacher_last_name
FROM sections s
LEFT JOIN academic_years ay ON s.academic_year_id = ay.id
LEFT JOIN teacher_sections ts ON s.id = ts.section_id
LEFT JOIN users u ON ts.teacher_id = u.id
ORDER BY ay.year_range DESC, s.grade_level ASC, s.name ASC
`).all();
res.json(sections);
@@ -194,7 +211,18 @@ exports.getAllSections = (req, res) => {
exports.getSectionsByYear = (req, res) => {
try {
const sections = db.prepare('SELECT * FROM sections WHERE academic_year_id = ? ORDER BY grade_level ASC, name ASC').all(req.params.yearId);
const sections = db.prepare(`
SELECT s.*, ay.year_range as academic_year_range,
ts.teacher_id,
u.first_name as teacher_first_name,
u.last_name as teacher_last_name
FROM sections s
LEFT JOIN academic_years ay ON s.academic_year_id = ay.id
LEFT JOIN teacher_sections ts ON s.id = ts.section_id
LEFT JOIN users u ON ts.teacher_id = u.id
WHERE s.academic_year_id = ?
ORDER BY s.grade_level ASC, s.name ASC
`).all(req.params.yearId);
res.json(sections);
} catch (error) {
res.status(500).json({ message: 'Failed to fetch sections', error: error.message });
@@ -202,14 +230,21 @@ exports.getSectionsByYear = (req, res) => {
};
exports.createSection = (req, res) => {
const { name, gradeLevel, academicYearId } = req.body;
const { name, gradeLevel, academicYearId, teacherId } = req.body;
if (!name || !name.trim() || !gradeLevel || !academicYearId) {
return res.status(400).json({ message: 'name, gradeLevel, and academicYearId are required' });
}
try {
const result = db.prepare('INSERT INTO sections (name, grade_level, academic_year_id) VALUES (?, ?, ?)')
.run(name.trim(), gradeLevel, academicYearId);
res.status(201).json({ id: result.lastInsertRowid, message: 'Section created successfully' });
let sectionId;
db.transaction(() => {
const result = db.prepare('INSERT INTO sections (name, grade_level, academic_year_id) VALUES (?, ?, ?)')
.run(name.trim(), gradeLevel, academicYearId);
sectionId = result.lastInsertRowid;
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, sectionId);
}
})();
res.status(201).json({ id: sectionId, message: 'Section created successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to create section', error: error.message });
}
@@ -217,7 +252,7 @@ exports.createSection = (req, res) => {
exports.updateSection = (req, res) => {
const { id } = req.params;
const { name, gradeLevel, academicYearId } = req.body;
const { name, gradeLevel, academicYearId, teacherId } = req.body;
if (!name || !name.trim() || !gradeLevel || !academicYearId) {
return res.status(400).json({ message: 'name, gradeLevel, and academicYearId are required' });
@@ -227,14 +262,41 @@ exports.updateSection = (req, res) => {
const section = db.prepare('SELECT id FROM sections WHERE id = ?').get(id);
if (!section) return res.status(404).json({ message: 'Section not found' });
db.prepare('UPDATE sections SET name = ?, grade_level = ?, academic_year_id = ? WHERE id = ?')
.run(name.trim(), gradeLevel, academicYearId, id);
db.transaction(() => {
db.prepare('UPDATE sections SET name = ?, grade_level = ?, academic_year_id = ? WHERE id = ?')
.run(name.trim(), gradeLevel, academicYearId, id);
if (teacherId !== undefined) {
db.prepare('DELETE FROM teacher_sections WHERE section_id = ?').run(id);
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, id);
}
}
})();
res.json({ message: 'Section updated successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to update section', error: error.message });
}
};
exports.setSectionAdviser = (req, res) => {
const { id } = req.params;
const { teacherId } = req.body;
try {
const section = db.prepare('SELECT id FROM sections WHERE id = ?').get(id);
if (!section) return res.status(404).json({ message: 'Section not found' });
db.transaction(() => {
db.prepare('DELETE FROM teacher_sections WHERE section_id = ?').run(id);
if (teacherId) {
db.prepare('INSERT INTO teacher_sections (teacher_id, section_id) VALUES (?, ?)').run(teacherId, id);
}
})();
res.json({ message: 'Section adviser updated successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to update section adviser', error: error.message });
}
};
exports.deleteSection = (req, res) => {
const { id } = req.params;
try {
@@ -315,3 +377,138 @@ exports.deleteGradeLevel = (req, res) => {
res.status(500).json({ message: 'Failed to delete grade level', error: error.message });
}
};
// POST /api/setup/bulk-academic-structure (admin only)
exports.bulkCreateAcademicStructure = (req, res) => {
const { rows } = req.body;
if (!rows || !Array.isArray(rows) || rows.length === 0) {
return res.status(400).json({ message: 'rows array is required and must not be empty' });
}
const currentYear = new Date().getFullYear();
const regex = /^(\d{4})\s*-\s*(\d{4})$/;
const results = {
totalRows: rows.length,
createdYears: [],
createdSections: [],
existingSections: [],
errors: []
};
const processTransaction = db.transaction(() => {
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const rawYear = (row.academicYear || row.yearRange || '').toString().trim();
const rawGrade = parseInt(row.gradeLevel, 10);
const rawSection = (row.sectionName || row.section || '').toString().trim();
if (!rawYear || isNaN(rawGrade) || !rawSection) {
results.errors.push({
row: i + 1,
data: row,
error: 'Academic Year, Grade Level (numeric), and Section Name are required'
});
continue;
}
// Validate year format YYYY-YYYY
const match = rawYear.match(regex);
if (!match) {
results.errors.push({
row: i + 1,
data: row,
error: `Invalid academic year format "${rawYear}". Must be YYYY-YYYY (e.g. 2024-2025)`
});
continue;
}
const startYear = parseInt(match[1], 10);
const endYear = parseInt(match[2], 10);
if (endYear !== startYear + 1) {
results.errors.push({
row: i + 1,
data: row,
error: `Invalid academic year range "${rawYear}". End year must be start year + 1`
});
continue;
}
if (startYear > currentYear) {
results.errors.push({
row: i + 1,
data: row,
error: `Cannot add future academic year "${rawYear}" beyond ${currentYear}-${currentYear + 1}`
});
continue;
}
if (rawGrade < 7 || rawGrade > 12) {
results.errors.push({
row: i + 1,
data: row,
error: `Grade level ${rawGrade} must be between 7 and 12`
});
continue;
}
const normalizedRange = `${startYear}-${endYear}`;
// 1. Find or create academic year
let yearRecord = db.prepare('SELECT id, year_range, status FROM academic_years WHERE year_range = ?').get(normalizedRange);
if (!yearRecord) {
const newYear = db.prepare('INSERT INTO academic_years (year_range, status) VALUES (?, ?)')
.run(normalizedRange, 'inactive');
yearRecord = { id: newYear.lastInsertRowid, year_range: normalizedRange, status: 'inactive' };
if (!results.createdYears.includes(normalizedRange)) {
results.createdYears.push(normalizedRange);
}
}
// 2. Ensure grade level exists in grade_levels table
const gradeExists = db.prepare('SELECT id FROM grade_levels WHERE level = ?').get(rawGrade);
if (!gradeExists) {
db.prepare('INSERT OR IGNORE INTO grade_levels (level, name) VALUES (?, ?)')
.run(rawGrade, `Grade ${rawGrade}`);
}
// 3. Find or create section
const existingSection = db.prepare(
'SELECT id FROM sections WHERE LOWER(name) = LOWER(?) AND grade_level = ? AND academic_year_id = ?'
).get(rawSection, rawGrade, yearRecord.id);
if (existingSection) {
results.existingSections.push({
academicYear: normalizedRange,
gradeLevel: rawGrade,
sectionName: rawSection
});
} else {
const newSec = db.prepare('INSERT INTO sections (name, grade_level, academic_year_id) VALUES (?, ?, ?)')
.run(rawSection, rawGrade, yearRecord.id);
results.createdSections.push({
id: newSec.lastInsertRowid,
academicYear: normalizedRange,
gradeLevel: rawGrade,
sectionName: rawSection
});
}
}
});
try {
processTransaction();
res.json({
success: true,
message: `Processed ${results.totalRows} rows. Created ${results.createdSections.length} section(s) across ${results.createdYears.length} new academic year(s).`,
data: results
});
} catch (error) {
console.error('bulkCreateAcademicStructure error:', error);
res.status(500).json({
success: false,
message: 'Failed to process bulk academic structure import',
error: error.message
});
}
};
+354 -10
View File
@@ -2,6 +2,11 @@ const db = require('../config/db');
const fs = require('fs');
const path = require('path');
const { createNotification } = require('./notificationController');
const {
emitStudentAdded,
emitStudentUpdated,
emitStudentDeleted,
} = require('../services/socketService');
// ============================================================
// HELPER — insert one row into activity_log
@@ -250,19 +255,22 @@ exports.getAllStudents = (req, res) => {
SELECT e.id FROM enrollments e
JOIN academic_years ay_inner ON e.academic_year_id = ay_inner.id
WHERE e.student_id = s.id
${filterYearRange ? 'AND ay_inner.year_range = ?' : ''}
ORDER BY e.grade_level DESC, ay_inner.year_range DESC, e.id DESC LIMIT 1
${filterYearRange ? 'AND ay_inner.year_range = ?' : 'AND LOWER(ay_inner.status) = \'active\''}
ORDER BY e.grade_level DESC, e.id DESC LIMIT 1
)
JOIN sections sec ON sec.id = e_latest.section_id
LEFT JOIN sections sec ON sec.id = e_latest.section_id
JOIN academic_years ay ON ay.id = e_latest.academic_year_id`;
if (filterYearRange) {
enrollJoinParams.push(filterYearRange);
}
}
if (gradeLevel.trim()) {
conditions.push(`e_latest.grade_level = ?`);
whereParams.push(parseInt(gradeLevel));
if (gradeLevel.trim()) {
const gradeMatch = gradeLevel.trim().match(/\d+/);
if (gradeMatch) {
conditions.push(`e_latest.grade_level = ?`);
whereParams.push(parseInt(gradeMatch[0]));
}
}
if (section.trim()) {
conditions.push(`sec.name = ?`);
@@ -534,6 +542,7 @@ exports.createStudent = (req, res) => {
logActivity(req.user?.id, 'CREATE', 'student', newId, `Created student ${firstName} ${lastName}`);
createNotification(null, 'Student Created', `New student ${firstName} ${lastName} (LRN: ${lrn.trim()}) has been enrolled.`, 'student', 'student', newId);
emitStudentAdded({ id: newId, lrn: lrn.trim(), firstName, lastName });
res.status(201).json({
id: newId,
@@ -605,6 +614,7 @@ exports.bulkGraduate = (req, res) => {
`${count} student(s) status updated to Graduated.`,
'student'
);
emitStudentUpdated({ action: 'bulkGraduate', count });
}
res.json({ message: `${count} student(s) successfully graduated.` });
@@ -622,6 +632,31 @@ exports.updateStudent = (req, res) => {
let { lrn, firstName, middleName, lastName, extension, sex, birthDate, status, academicYearId, gradeLevel, sectionId, trackStrand, is4ps } = req.body;
birthDate = normalizeBirthDate(birthDate);
// ---- Existence check ----
const existing = db.prepare('SELECT * FROM students WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ message: 'Student not found.' });
// Teacher restrictions: verify section access and limit editable fields
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
if (isTeacher) {
const hasAccess = db.prepare(`
SELECT 1
FROM enrollments e
JOIN teacher_sections ts ON e.section_id = ts.section_id
WHERE e.student_id = ? AND ts.teacher_id = ?
`).get(id, req.user.id);
if (!hasAccess) {
return res.status(403).json({ message: "Access denied to update this student." });
}
// Teachers can ONLY edit: Name, Date of Birth, Sex, 4Ps Beneficiary
// Lock non-editable fields to existing student record values
status = existing.status;
lrn = existing.lrn;
academicYearId = null;
gradeLevel = null;
sectionId = null;
}
// ---- Server-side validation ----
const errors = [];
if (!lrn || !isValidLRN(lrn)) errors.push('LRN must be exactly 12 digits.');
@@ -655,10 +690,6 @@ exports.updateStudent = (req, res) => {
if (errors.length) return res.status(400).json({ message: errors[0], errors });
// ---- Existence check ----
const existing = db.prepare('SELECT id FROM students WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ message: 'Student not found.' });
// ---- Duplicate LRN check (excluding self) ----
const duplicate = db.prepare('SELECT id FROM students WHERE lrn = ? AND id != ?').get(lrn.trim(), id);
if (duplicate) return res.status(409).json({ message: `Another student already has LRN ${lrn}.` });
@@ -751,6 +782,7 @@ exports.updateStudent = (req, res) => {
logActivity(req.user?.id, 'UPDATE', 'student', id, `Updated student ${maskLrn(lrn)}`);
createNotification(null, 'Student Updated', `Student ${firstName} ${lastName} (LRN: ${lrn.trim()}) record was updated.`, 'student', 'student', id);
emitStudentUpdated({ id, lrn: lrn.trim() });
res.json({ message: 'Student updated successfully.' });
} catch (error) {
console.error('updateStudent error:', error);
@@ -780,6 +812,7 @@ exports.deleteStudent = (req, res) => {
logActivity(req.user?.id, 'DELETE', 'student', id, `Deleted student: ${student.first_name} ${student.last_name} (LRN: ${maskLrn(student.lrn)})`);
createNotification(null, 'Student Deleted', `Student ${student.first_name} ${student.last_name} was removed.`, 'student', 'student', id);
emitStudentDeleted({ id });
res.json({ message: 'Student deleted successfully.' });
} catch (error) {
console.error('deleteStudent error:', error);
@@ -787,6 +820,56 @@ exports.deleteStudent = (req, res) => {
}
};
// ============================================================
// POST /api/students/verify-lrns
// ============================================================
exports.verifyLrns = (req, res) => {
const { lrns } = req.body;
if (!Array.isArray(lrns)) {
return res.status(400).json({ message: 'lrns must be an array' });
}
try {
const uniqueLrns = [...new Set(lrns.map(l => String(l).trim()).filter(l => l.length > 0))];
if (uniqueLrns.length === 0) {
return res.json({ verified: [] });
}
const placeholders = uniqueLrns.map(() => '?').join(',');
const foundStudents = db.prepare(`
SELECT id, lrn, first_name, last_name, status
FROM students
WHERE lrn IN (${placeholders})
`).all(...uniqueLrns);
const foundMap = new Map(foundStudents.map(s => [s.lrn, s]));
const verified = uniqueLrns.map(lrn => {
const st = foundMap.get(lrn);
if (st) {
return {
lrn,
studentId: st.id,
firstName: st.first_name,
lastName: st.last_name,
status: st.status,
found: true
};
} else {
return {
lrn,
found: false
};
}
});
res.json({ verified });
} catch (error) {
console.error('verifyLrns error:', error);
res.status(500).json({ message: 'Failed to verify LRNs', error: error.message });
}
};
// ============================================================
// POST /api/students/bulk-enroll
// ============================================================
@@ -897,6 +980,7 @@ exports.bulkStatusStudents = (req, res) => {
? `Updated ${studentRows.length} students\n${studentListText}`
: (studentRows.length === 1 ? `Updated 1 student ${studentRows[0].last_name || ''} - ${maskLrn(studentRows[0].lrn)}` : `Bulk updated status to "${status}" for ${studentIds.length} students`);
logActivity(req.user?.id, 'UPDATE', 'student', null, bulkDesc);
emitStudentUpdated({ action: 'bulkStatus', studentIds, status });
res.json({ message: `Successfully updated status to "${status}" for ${studentIds.length} students.` });
} catch (error) {
console.error('bulkStatusStudents error:', error);
@@ -918,6 +1002,16 @@ exports.addEnrollment = (req, res) => {
const student = db.prepare('SELECT id FROM students WHERE id = ?').get(studentId);
if (!student) return res.status(404).json({ message: 'Student not found.' });
if (req.user && req.user.role === 'teacher') {
const isAssigned = db.prepare(`
SELECT 1 FROM teacher_sections
WHERE teacher_id = ? AND section_id = ?
`).get(req.user.id, sectionId);
if (!isAssigned) {
return res.status(403).json({ message: "You can only enroll students into sections assigned to you." });
}
}
const progressionCheck = validateEnrollmentProgression(studentId, academicYearId, gradeLevel);
if (!progressionCheck.valid) {
return res.status(400).json({ message: progressionCheck.message });
@@ -933,6 +1027,7 @@ exports.addEnrollment = (req, res) => {
const lrn = studentRow?.lrn || studentId;
logActivity(req.user?.id, 'CREATE', 'enrollment', info.lastInsertRowid, getEnrollmentLogDesc(academicYearId, gradeLevel, sectionId, lrn));
createNotification(null, 'Student Enrolled', `Student (LRN: ${lrn}) was enrolled in a new section.`, 'student', 'student', studentId);
emitStudentUpdated({ id: studentId });
res.status(201).json({ message: 'Enrollment added successfully', id: info.lastInsertRowid });
} catch (error) {
console.error('addEnrollment error:', error);
@@ -940,6 +1035,252 @@ exports.addEnrollment = (req, res) => {
}
};
// ============================================================
// POST /api/students/:id/verify-enrollments
// ============================================================
exports.verifyStudentEnrollments = (req, res) => {
const { id: studentId } = req.params;
const { enrollments } = req.body;
if (!Array.isArray(enrollments) || enrollments.length === 0) {
return res.status(400).json({ message: 'enrollments must be a non-empty array' });
}
const student = db.prepare('SELECT id, lrn, first_name, last_name FROM students WHERE id = ?').get(studentId);
if (!student) return res.status(404).json({ message: 'Student not found.' });
const allAcademicYears = db.prepare('SELECT id, year_range, status FROM academic_years ORDER BY year_range ASC').all();
const allSections = db.prepare('SELECT id, name, grade_level, academic_year_id FROM sections').all();
// Query existing enrollments for this student
const existingEnrollments = db.prepare(`
SELECT e.id, e.grade_level, e.academic_year_id, ay.year_range, sec.name as section_name
FROM enrollments e
JOIN academic_years ay ON e.academic_year_id = ay.id
LEFT JOIN sections sec ON e.section_id = sec.id
WHERE e.student_id = ?
ORDER BY ay.year_range ASC
`).all(studentId);
const verifiedRecords = [];
const seenYearsInBatch = new Set();
const cleanStr = (s) => (s != null ? String(s).trim() : '');
const normalizeYear = (s) => cleanStr(s).replace(/\s+/g, '');
for (let i = 0; i < enrollments.length; i++) {
const item = enrollments[i];
const rawSchoolYear = cleanStr(item.schoolYear || item.yearRange);
const rawGradeLevel = cleanStr(item.gradeLevel);
const rawSection = cleanStr(item.section || item.sectionName);
const rawTrack = cleanStr(item.trackStrand || item.track);
const rec = {
index: i,
schoolYear: rawSchoolYear,
gradeLevel: rawGradeLevel,
sectionName: rawSection,
trackStrand: rawTrack || null,
academicYearId: null,
sectionId: null,
parsedGradeLevel: null,
isValid: false,
error: null,
};
if (!rawSchoolYear || !rawGradeLevel || !rawSection) {
rec.error = 'School Year, Grade Level, and Section are all required.';
verifiedRecords.push(rec);
continue;
}
// Match School Year
const normTargetYear = normalizeYear(rawSchoolYear);
const matchedAy = allAcademicYears.find(ay => normalizeYear(ay.year_range) === normTargetYear);
if (!matchedAy) {
const available = allAcademicYears.map(a => a.year_range).slice(-5).join(', ');
rec.error = `School Year "${rawSchoolYear}" does not exist in the system.${available ? ` (Available: ${available})` : ''}`;
verifiedRecords.push(rec);
continue;
}
rec.academicYearId = matchedAy.id;
rec.schoolYear = matchedAy.year_range;
// Match Grade Level (7 - 12)
const gradeDigits = rawGradeLevel.match(/\d+/);
const gLevel = gradeDigits ? parseInt(gradeDigits[0], 10) : NaN;
if (isNaN(gLevel) || gLevel < 7 || gLevel > 12) {
rec.error = `Invalid Grade Level "${rawGradeLevel}". Must be between 7 and 12.`;
verifiedRecords.push(rec);
continue;
}
rec.parsedGradeLevel = gLevel;
// Match Section for that Academic Year and Grade Level
const sectionMatch = allSections.find(s =>
s.academic_year_id === matchedAy.id &&
s.grade_level === gLevel &&
s.name.toLowerCase() === rawSection.toLowerCase()
);
if (!sectionMatch) {
const availableForGrade = allSections
.filter(s => s.academic_year_id === matchedAy.id && s.grade_level === gLevel)
.map(s => s.name);
if (availableForGrade.length > 0) {
rec.error = `Section "${rawSection}" not found for Grade ${gLevel} in S.Y. ${matchedAy.year_range}. Available sections: ${availableForGrade.join(', ')}.`;
} else {
rec.error = `No sections configured for Grade ${gLevel} in S.Y. ${matchedAy.year_range}.`;
}
verifiedRecords.push(rec);
continue;
}
rec.sectionId = sectionMatch.id;
rec.sectionName = sectionMatch.name;
// Check if duplicate year in this CSV batch
if (seenYearsInBatch.has(matchedAy.id)) {
rec.error = `Duplicate School Year: "${matchedAy.year_range}" is specified more than once in this batch.`;
verifiedRecords.push(rec);
continue;
}
seenYearsInBatch.add(matchedAy.id);
// Check if student is already enrolled in this school year in DB
const existingInYear = existingEnrollments.find(e => e.academic_year_id === matchedAy.id || normalizeYear(e.year_range) === normTargetYear);
if (existingInYear) {
rec.error = `Already enrolled: Student is already enrolled in Grade ${existingInYear.grade_level}${existingInYear.section_name ? ` - ${existingInYear.section_name}` : ''} for S.Y. ${existingInYear.year_range}.`;
verifiedRecords.push(rec);
continue;
}
// Progression validation against existing enrollments
let progressionError = null;
for (const other of existingEnrollments) {
if (other.year_range < matchedAy.year_range) {
if (gLevel < other.grade_level) {
progressionError = `Progression error: Cannot enroll in Grade ${gLevel} for S.Y. ${matchedAy.year_range}. Student was already enrolled in Grade ${other.grade_level} in earlier S.Y. ${other.year_range} (downgrading restriction).`;
break;
}
} else if (other.year_range > matchedAy.year_range) {
if (gLevel > other.grade_level) {
progressionError = `Progression error: Cannot enroll in Grade ${gLevel} for S.Y. ${matchedAy.year_range}. Student is already enrolled in Grade ${other.grade_level} in subsequent S.Y. ${other.year_range} (downgrading restriction).`;
break;
}
}
}
if (progressionError) {
rec.error = progressionError;
verifiedRecords.push(rec);
continue;
}
rec.isValid = true;
verifiedRecords.push(rec);
}
// Validate progression AMONG valid batch items
const validBatchItems = verifiedRecords.filter(r => r.isValid);
validBatchItems.sort((a, b) => a.schoolYear.localeCompare(b.schoolYear));
for (let j = 0; j < validBatchItems.length - 1; j++) {
const current = validBatchItems[j];
const next = validBatchItems[j + 1];
if (current.schoolYear < next.schoolYear && current.parsedGradeLevel > next.parsedGradeLevel) {
next.isValid = false;
next.error = `Progression error: Grade ${next.parsedGradeLevel} in S.Y. ${next.schoolYear} cannot downgrade from Grade ${current.parsedGradeLevel} in earlier S.Y. ${current.schoolYear}.`;
}
}
const allValid = verifiedRecords.length > 0 && verifiedRecords.every(r => r.isValid);
const validCount = verifiedRecords.filter(r => r.isValid).length;
return res.json({
success: true,
allValid,
validCount,
totalCount: verifiedRecords.length,
results: verifiedRecords,
});
};
// ============================================================
// POST /api/students/:id/bulk-enrollments
// ============================================================
exports.bulkAddStudentEnrollments = (req, res) => {
const { id: studentId } = req.params;
const { enrollments } = req.body;
if (!Array.isArray(enrollments) || enrollments.length === 0) {
return res.status(400).json({ message: 'enrollments must be a non-empty array' });
}
const student = db.prepare('SELECT id, lrn, first_name, last_name FROM students WHERE id = ?').get(studentId);
if (!student) return res.status(404).json({ message: 'Student not found.' });
try {
let insertedCount = 0;
db.transaction(() => {
const insertEnrollment = db.prepare(`
INSERT INTO enrollments (student_id, academic_year_id, section_id, grade_level, track_strand)
VALUES (?, ?, ?, ?, ?)
`);
const updateStudentStatus = db.prepare('UPDATE students SET status = ? WHERE id = ?');
for (const item of enrollments) {
const { academicYearId, sectionId, gradeLevel, trackStrand } = item;
if (!academicYearId || !sectionId || !gradeLevel) {
throw new Error('Each enrollment requires academicYearId, sectionId, and gradeLevel');
}
// Verify progression before inserting
const progressionCheck = validateEnrollmentProgression(studentId, academicYearId, gradeLevel);
if (!progressionCheck.valid) {
throw new Error(progressionCheck.message);
}
const resIns = insertEnrollment.run(studentId, academicYearId, sectionId, gradeLevel, trackStrand || null);
insertedCount++;
const logDesc = getEnrollmentLogDesc(academicYearId, gradeLevel, sectionId, student.lrn, `${student.first_name} ${student.last_name}`);
logActivity(req.user?.id, 'CREATE', 'enrollment', resIns.lastInsertRowid, logDesc);
}
// Ensure student status is updated to Enrolled
updateStudentStatus.run('Enrolled', studentId);
})();
// Overall activity log and notification
logActivity(
req.user?.id,
'CREATE',
'enrollment',
studentId,
`Imported ${insertedCount} enrollment records via CSV for student ${student.first_name} ${student.last_name} (${maskLrn(student.lrn)})`
);
createNotification(
null,
'Enrollments Imported',
`${insertedCount} enrollment(s) imported for ${student.first_name} ${student.last_name}.`,
'student',
'student',
studentId
);
emitStudentUpdated({ id: studentId });
res.status(201).json({
success: true,
message: `Successfully imported ${insertedCount} enrollment(s) for ${student.first_name} ${student.last_name}.`,
count: insertedCount,
});
} catch (error) {
console.error('bulkAddStudentEnrollments error:', error);
res.status(400).json({ message: error.message || 'Failed to bulk import enrollments.' });
}
};
// ============================================================
// PUT /api/students/enrollments/:enrollmentId
// ============================================================
@@ -969,6 +1310,7 @@ exports.updateEnrollment = (req, res) => {
const studentRow = db.prepare('SELECT lrn FROM students WHERE id = ?').get(existing.student_id);
const lrn = studentRow?.lrn || existing.student_id;
logActivity(req.user?.id, 'UPDATE', 'enrollment', enrollmentId, getEnrollmentLogDesc(academicYearId, gradeLevel, sectionId, lrn));
emitStudentUpdated({ id: existing.student_id });
res.json({ message: 'Enrollment updated successfully' });
} catch (error) {
console.error('updateEnrollment error:', error);
@@ -990,6 +1332,7 @@ exports.deleteEnrollment = (req, res) => {
const studentRow = db.prepare('SELECT lrn FROM students WHERE id = ?').get(existing.student_id);
const lrn = studentRow?.lrn || existing.student_id;
logActivity(req.user?.id, 'DELETE', 'enrollment', enrollmentId, getEnrollmentLogDesc(existing.academic_year_id, existing.grade_level, existing.section_id, lrn));
emitStudentUpdated({ id: existing.student_id });
res.json({ message: 'Enrollment deleted successfully' });
} catch (error) {
console.error('deleteEnrollment error:', error);
@@ -1209,6 +1552,7 @@ exports.bulkCreateStudents = (req, res) => {
`${created} student(s) imported via OCR Bulk Import.`,
'student'
);
emitStudentAdded({ action: 'bulkCreate', count: created });
}
res.status(207).json({ created, skipped, failed, results });
@@ -0,0 +1,322 @@
const db = require('../config/db');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// ── Storage root for template files ──────────────────────────────────────────
const TEMPLATES_DIR = process.env.TEMPLATES_DIR
? path.resolve(process.env.TEMPLATES_DIR)
: path.resolve(__dirname, '../../../data/templates');
if (!fs.existsSync(TEMPLATES_DIR)) {
fs.mkdirSync(TEMPLATES_DIR, { recursive: true });
}
// ── Multer config ─────────────────────────────────────────────────────────────
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const templateId = req.params.id || req.body.templateId || 'new';
const dir = path.join(TEMPLATES_DIR, String(templateId));
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
req._templateUploadDir = dir;
cb(null, dir);
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const base = path.basename(file.originalname, ext).replace(/[<>:"/\\|?*\x00-\x1F]/g, '').trim();
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
cb(null, `${ts}_${base}${ext}`);
},
});
const upload = multer({ storage });
exports.uploadMiddleware = upload.single('file');
// ── Helper: log activity ──────────────────────────────────────────────────────
const logActivity = (userId, action, entityType, entityId, description) => {
try {
db.prepare(
'INSERT INTO activity_log (user_id, action, entity_type, entity_id, description) VALUES (?, ?, ?, ?, ?)'
).run(userId ?? null, action, entityType, entityId ?? null, description);
} catch (_) {}
};
// ── GET /api/templates ────────────────────────────────────────────────────────
exports.getTemplates = (req, res) => {
try {
const { requirementId, isActive } = req.query;
const conditions = [];
const params = [];
if (requirementId) {
conditions.push('t.requirement_id = ?');
params.push(requirementId);
}
if (isActive !== undefined && isActive !== '') {
conditions.push('t.is_active = ?');
params.push(isActive === 'true' ? 1 : 0);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const templates = db.prepare(`
SELECT
t.id, t.requirement_id, t.name, t.description, t.is_active,
t.created_by, t.created_at, t.updated_at,
r.name AS requirement_name,
r.category AS requirement_category,
(SELECT COUNT(*) FROM template_versions tv WHERE tv.template_id = t.id) AS version_count,
(SELECT tv2.id FROM template_versions tv2 WHERE tv2.template_id = t.id AND tv2.is_current = 1 LIMIT 1) AS current_version_id,
(SELECT tv2.version_number FROM template_versions tv2 WHERE tv2.template_id = t.id AND tv2.is_current = 1 LIMIT 1) AS current_version_number,
(SELECT tv2.file_name FROM template_versions tv2 WHERE tv2.template_id = t.id AND tv2.is_current = 1 LIMIT 1) AS current_file_name
FROM document_templates t
LEFT JOIN document_requirements r ON t.requirement_id = r.id
${where}
ORDER BY t.name ASC
`).all(params);
res.json(templates);
} catch (err) {
console.error('getTemplates error:', err);
res.status(500).json({ message: 'Failed to fetch templates', error: err.message });
}
};
// ── GET /api/templates/:id ────────────────────────────────────────────────────
exports.getTemplateById = (req, res) => {
try {
const template = db.prepare(`
SELECT
t.id, t.requirement_id, t.name, t.description, t.is_active,
t.created_by, t.created_at, t.updated_at,
r.name AS requirement_name, r.category AS requirement_category
FROM document_templates t
LEFT JOIN document_requirements r ON t.requirement_id = r.id
WHERE t.id = ?
`).get(req.params.id);
if (!template) return res.status(404).json({ message: 'Template not found' });
const versions = db.prepare(`
SELECT tv.id, tv.version_number, tv.file_name, tv.file_size, tv.is_current,
tv.uploaded_by, tv.created_at,
u.first_name || ' ' || u.last_name AS uploaded_by_name
FROM template_versions tv
LEFT JOIN users u ON tv.uploaded_by = u.id
WHERE tv.template_id = ?
ORDER BY tv.version_number DESC
`).all(req.params.id);
res.json({ ...template, versions });
} catch (err) {
console.error('getTemplateById error:', err);
res.status(500).json({ message: 'Failed to fetch template', error: err.message });
}
};
// ── GET /api/templates/:id/versions ──────────────────────────────────────────
exports.getTemplateVersions = (req, res) => {
try {
const versions = db.prepare(`
SELECT tv.id, tv.version_number, tv.file_name, tv.file_size, tv.is_current,
tv.uploaded_by, tv.created_at,
u.first_name || ' ' || u.last_name AS uploaded_by_name
FROM template_versions tv
LEFT JOIN users u ON tv.uploaded_by = u.id
WHERE tv.template_id = ?
ORDER BY tv.version_number DESC
`).all(req.params.id);
res.json(versions);
} catch (err) {
console.error('getTemplateVersions error:', err);
res.status(500).json({ message: 'Failed to fetch versions', error: err.message });
}
};
// ── POST /api/templates ───────────────────────────────────────────────────────
exports.createTemplate = (req, res) => {
const { name, description, requirementId, isActive = true } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({ message: 'Template name is required' });
}
try {
const result = db.prepare(`
INSERT INTO document_templates (requirement_id, name, description, is_active, created_by)
VALUES (?, ?, ?, ?, ?)
`).run(
requirementId ?? null,
name.trim(),
description?.trim() ?? null,
isActive ? 1 : 0,
req.user?.id ?? null
);
const templateId = result.lastInsertRowid;
// If a file was uploaded, create Version 1
if (req.file) {
// Move file from 'new' dir to real template dir
const newDir = path.join(TEMPLATES_DIR, String(templateId));
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir, { recursive: true });
const destPath = path.join(newDir, req.file.filename);
fs.renameSync(req.file.path, destPath);
const fileSize = fs.existsSync(destPath) ? fs.statSync(destPath).size : null;
db.prepare(`
INSERT INTO template_versions (template_id, version_number, file_name, file_path, file_size, uploaded_by, is_current)
VALUES (?, 1, ?, ?, ?, ?, 1)
`).run(templateId, req.file.originalname, destPath, fileSize, req.user?.id ?? null);
}
logActivity(req.user?.id, 'CREATE', 'template', templateId, `Created template: ${name}`);
res.status(201).json({ id: templateId, message: 'Template created successfully' });
} catch (err) {
console.error('createTemplate error:', err);
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
res.status(500).json({ message: 'Failed to create template', error: err.message });
}
};
// ── PATCH /api/templates/:id ──────────────────────────────────────────────────
exports.updateTemplate = (req, res) => {
const { name, description, requirementId, isActive } = req.body;
try {
const existing = db.prepare('SELECT id FROM document_templates WHERE id = ?').get(req.params.id);
if (!existing) return res.status(404).json({ message: 'Template not found' });
const fields = [];
const params = [];
if (name !== undefined) { fields.push('name = ?'); params.push(name.trim()); }
if (description !== undefined) { fields.push('description = ?'); params.push(description?.trim() ?? null); }
if (requirementId !== undefined) { fields.push('requirement_id = ?'); params.push(requirementId ?? null); }
if (isActive !== undefined) { fields.push('is_active = ?'); params.push(isActive ? 1 : 0); }
if (fields.length === 0) return res.status(400).json({ message: 'No fields to update' });
fields.push("updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')");
params.push(req.params.id);
db.prepare(`UPDATE document_templates SET ${fields.join(', ')} WHERE id = ?`).run(...params);
logActivity(req.user?.id, 'UPDATE', 'template', req.params.id, `Updated template id ${req.params.id}`);
res.json({ message: 'Template updated successfully' });
} catch (err) {
console.error('updateTemplate error:', err);
res.status(500).json({ message: 'Failed to update template', error: err.message });
}
};
// ── POST /api/templates/:id/upload-version ────────────────────────────────────
exports.uploadTemplateVersion = (req, res) => {
if (!req.file) return res.status(400).json({ message: 'No file uploaded' });
try {
const templateId = parseInt(req.params.id, 10);
const template = db.prepare('SELECT id FROM document_templates WHERE id = ?').get(templateId);
if (!template) {
fs.unlinkSync(req.file.path);
return res.status(404).json({ message: 'Template not found' });
}
// Get next version number
const lastVersion = db.prepare(
'SELECT MAX(version_number) AS max_v FROM template_versions WHERE template_id = ?'
).get(templateId);
const nextVersion = (lastVersion?.max_v ?? 0) + 1;
// Move file to correct location
const destDir = path.join(TEMPLATES_DIR, String(templateId));
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
const destPath = path.join(destDir, req.file.filename);
if (req.file.path !== destPath) fs.renameSync(req.file.path, destPath);
const fileSize = fs.existsSync(destPath) ? fs.statSync(destPath).size : null;
db.transaction(() => {
// Unset current flag on previous versions
db.prepare('UPDATE template_versions SET is_current = 0 WHERE template_id = ?').run(templateId);
// Insert new version
db.prepare(`
INSERT INTO template_versions (template_id, version_number, file_name, file_path, file_size, uploaded_by, is_current)
VALUES (?, ?, ?, ?, ?, ?, 1)
`).run(templateId, nextVersion, req.file.originalname, destPath, fileSize, req.user?.id ?? null);
// Update template updated_at
db.prepare("UPDATE document_templates SET updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?").run(templateId);
})();
logActivity(req.user?.id, 'CREATE', 'template_version', templateId,
`Uploaded version ${nextVersion} for template id ${templateId}`);
res.status(201).json({ message: `Template version ${nextVersion} uploaded successfully`, version_number: nextVersion });
} catch (err) {
console.error('uploadTemplateVersion error:', err);
if (req.file && fs.existsSync(req.file.path)) fs.unlinkSync(req.file.path);
res.status(500).json({ message: 'Failed to upload template version', error: err.message });
}
};
// ── GET /api/templates/:id/download ──────────────────────────────────────────
// Downloads the current (latest) version of the template file
exports.downloadTemplate = (req, res) => {
try {
const version = db.prepare(`
SELECT tv.file_path, tv.file_name
FROM template_versions tv
WHERE tv.template_id = ? AND tv.is_current = 1
LIMIT 1
`).get(req.params.id);
if (!version) return res.status(404).json({ message: 'No current template version found' });
if (!fs.existsSync(version.file_path)) {
return res.status(404).json({ message: 'Template file not found on disk' });
}
res.download(version.file_path, version.file_name);
} catch (err) {
console.error('downloadTemplate error:', err);
res.status(500).json({ message: 'Failed to download template', error: err.message });
}
};
// ── GET /api/template-versions/:versionId/download ───────────────────────────
exports.downloadTemplateVersion = (req, res) => {
try {
const version = db.prepare(
'SELECT file_path, file_name FROM template_versions WHERE id = ?'
).get(req.params.versionId);
if (!version) return res.status(404).json({ message: 'Template version not found' });
if (!fs.existsSync(version.file_path)) {
return res.status(404).json({ message: 'File not found on disk' });
}
res.download(version.file_path, version.file_name);
} catch (err) {
console.error('downloadTemplateVersion error:', err);
res.status(500).json({ message: 'Failed to download template version', error: err.message });
}
};
// ── DELETE /api/templates/:id ─────────────────────────────────────────────────
exports.deleteTemplate = (req, res) => {
try {
const template = db.prepare('SELECT id, name FROM document_templates WHERE id = ?').get(req.params.id);
if (!template) return res.status(404).json({ message: 'Template not found' });
// Remove all version files from disk
const versions = db.prepare('SELECT file_path FROM template_versions WHERE template_id = ?').all(req.params.id);
for (const v of versions) {
if (v.file_path && fs.existsSync(v.file_path)) {
try { fs.unlinkSync(v.file_path); } catch (_) {}
}
}
db.prepare('DELETE FROM document_templates WHERE id = ?').run(req.params.id);
logActivity(req.user?.id, 'DELETE', 'template', req.params.id, `Deleted template: ${template.name}`);
res.json({ message: 'Template deleted' });
} catch (err) {
console.error('deleteTemplate error:', err);
res.status(500).json({ message: 'Failed to delete template', error: err.message });
}
};
+148 -26
View File
@@ -10,6 +10,10 @@ const {
sendAccountStatusEmail,
} = require('../services/emailService');
const { getDetectedTunnelUrl } = require('../services/tunnelService');
// validateEmailMEV is lazy-required to avoid circular dependency (authController → userController → authController)
const getMEV = () => require('./authController').validateEmailMEV;
const getQEV = getMEV;
// ── Base URL Helper (Auto-detects Tunnel vs LAN IP vs Domain) ────────────────
const getBestServerBaseUrl = (req) => {
@@ -120,7 +124,7 @@ exports.getUsers = (req, res) => {
};
// POST /api/users - Create a new user
exports.createUser = (req, res) => {
exports.createUser = async (req, res) => {
const { username, firstName, middleName, lastName, extension, role, email, phone } = req.body;
// Password is optional — auto-generated if not provided
let providedPassword = req.body.password;
@@ -134,8 +138,42 @@ exports.createUser = (req, res) => {
return res.status(400).json({ message: 'Invalid role. Must be admin or teacher.' });
}
// Temporary password = username + 123 (e.g. ccharles123)
const temporaryPassword = `${username}123`;
// Validate email with MyEmailVerifier before creating user
if (email && email.trim()) {
try {
const mevResult = await getMEV()(email.trim());
if (!mevResult.valid) {
return res.status(400).json({
message: 'Invalid or undeliverable email address.',
reason: mevResult.reason,
});
}
} catch (mevErr) {
console.warn('[UserController] MEV pre-check skipped due to error:', mevErr.message);
}
}
// Generate secure randomized temporary password for all users if not explicitly provided
let temporaryPassword;
if (providedPassword && providedPassword.trim()) {
temporaryPassword = providedPassword.trim();
} else {
const uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const lowercase = 'abcdefghjkmnpqrstuvwxyz';
const digits = '23456789';
const symbols = '!@#$%&*';
const allChars = uppercase + lowercase + digits + symbols;
const pwdParts = [
uppercase[crypto.randomInt(0, uppercase.length)],
lowercase[crypto.randomInt(0, lowercase.length)],
digits[crypto.randomInt(0, digits.length)],
symbols[crypto.randomInt(0, symbols.length)],
];
for (let i = 4; i < 10; i++) {
pwdParts.push(allChars[crypto.randomInt(0, allChars.length)]);
}
temporaryPassword = pwdParts.sort(() => crypto.randomInt(-1, 2)).join('');
}
try {
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
@@ -159,15 +197,25 @@ exports.createUser = (req, res) => {
// Asynchronously send welcome email with credentials if email is provided
if (email && email.trim()) {
sendAccountCreatedEmail({
to: email.trim(),
username,
fullName,
role,
temporaryPassword,
}).catch(emailErr => {
console.error(`[UserController] Failed to send account created email to ${email}:`, emailErr.message);
});
// QEV: validate email before sending (fire-and-forget with validation)
(async () => {
try {
const qevResult = await getQEV()(email.trim());
if (!qevResult.valid) {
console.warn(`[UserController] Skipping welcome email to ${email}: QEV says ${qevResult.reason}`);
return;
}
await sendAccountCreatedEmail({
to: email.trim(),
username,
fullName,
role,
temporaryPassword,
});
} catch (emailErr) {
console.error(`[UserController] Failed to send account created email to ${email}:`, emailErr.message);
}
})();
}
res.status(201).json({
@@ -182,7 +230,7 @@ exports.createUser = (req, res) => {
// PUT /api/users/:id - Update a user
exports.updateUser = (req, res) => {
exports.updateUser = async (req, res) => {
const { id } = req.params;
const { firstName, middleName, lastName, extension, role, email, phone } = req.body;
@@ -194,8 +242,20 @@ exports.updateUser = (req, res) => {
try {
const user = db.prepare('SELECT id, username, role, first_name, middle_name, last_name, extension, email, phone, is_hidden FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (user.is_hidden === 1 && req.user.id !== user.id) {
return res.status(404).json({ message: 'User not found.' });
// Validate email with MyEmailVerifier if changed and not empty
if (email !== undefined && email !== null && email.trim() !== '' && email.trim().toLowerCase() !== (user.email || '').trim().toLowerCase()) {
try {
const mevResult = await getMEV()(email.trim());
if (!mevResult.valid) {
return res.status(400).json({
message: 'Invalid or undeliverable email address.',
reason: mevResult.reason,
});
}
} catch (mevErr) {
console.warn('[UserController] MEV pre-check skipped due to error:', mevErr.message);
}
}
// Prevent self-demotion (locked role for self)
@@ -252,9 +312,6 @@ exports.resetPassword = async (req, res) => {
const user = db.prepare('SELECT id, username, first_name, last_name, role, email, is_hidden FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (user.is_hidden === 1 && req.user.id !== user.id) {
return res.status(404).json({ message: 'User not found.' });
}
if (user.id === req.user.id) {
return res.status(403).json({ message: 'Cannot reset your own password via this route. Use the Change Password profile setting.' });
@@ -266,6 +323,14 @@ exports.resetPassword = async (req, res) => {
});
}
// QEV: validate recipient email before sending reset link
const qevCheck = await getQEV()(user.email.trim());
if (!qevCheck.valid) {
return res.status(422).json({
message: `Cannot send password reset email: the registered address "${user.email}" appears to be invalid or undeliverable (${qevCheck.reason}). Please update the user's email address first.`
});
}
// Invalidate any previous pending reset links for this user
db.prepare(`
UPDATE password_reset_links
@@ -362,7 +427,6 @@ exports.deleteUser = (req, res) => {
const user = db.prepare('SELECT id, username, first_name, middle_name, last_name, role, is_hidden FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (user.is_hidden === 1) return res.status(403).json({ message: 'Cannot delete developer super administrator account.' });
if (user.id === adminId) return res.status(403).json({ message: 'Cannot delete your own account.' });
if (user.role === 'admin') {
@@ -404,9 +468,6 @@ exports.toggleUserStatus = (req, res) => {
try {
const user = db.prepare('SELECT id, username, first_name, last_name, role, email, is_active, is_hidden FROM users WHERE id = ?').get(id);
if (!user) return res.status(404).json({ message: 'User not found.' });
if (user.is_hidden === 1) {
return res.status(403).json({ message: 'Cannot modify developer super administrator account.' });
}
const newStatus = user.is_active === 1 ? 0 : 1;
db.prepare("UPDATE users SET is_active = ?, updated_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) WHERE id = ?")
@@ -497,20 +558,53 @@ exports.getUserHistory = (req, res) => {
const offset = (page - 1) * limit;
const dateFrom = req.query.date_from || '';
const dateTo = req.query.date_to || '';
const action = req.query.action || '';
const action = (req.query.action || '').trim();
const role = req.query.role || '';
const search = (req.query.search || '').trim();
const conditions = [];
const params = [];
if (dateFrom) { conditions.push("DATE(h.created_at) >= DATE(?)"); params.push(dateFrom); }
if (dateTo) { conditions.push("DATE(h.created_at) <= DATE(?)"); params.push(dateTo); }
if (action) { conditions.push("h.action = ?"); params.push(action); }
if (role) { conditions.push("h.role = ?"); params.push(role); }
if (action) {
const actLower = action.toLowerCase();
if (actLower === 'all' || actLower === 'all actions') {
// no action filter
} else if (actLower === 'add' || actLower === 'create' || actLower === 'created') {
conditions.push("LOWER(h.action) IN ('created', 'create', 'add')");
} else if (actLower === 'update' || actLower === 'updated') {
conditions.push("LOWER(h.action) IN ('updated', 'update')");
} else if (actLower === 'delete' || actLower === 'deleted') {
conditions.push("LOWER(h.action) IN ('deleted', 'delete')");
} else if (actLower === 'archive' || actLower === 'archived') {
conditions.push("(LOWER(h.action) IN ('archived', 'archive') OR h.action LIKE '%archive%')");
} else {
conditions.push("LOWER(h.action) = ?");
params.push(actLower);
}
}
if (role) {
conditions.push("LOWER(h.role) = LOWER(?)");
params.push(role);
}
if (search) {
conditions.push("(h.username LIKE ? OR h.full_name LIKE ? OR COALESCE(u.username, dh.username, 'System') LIKE ? OR COALESCE(u.first_name || ' ' || u.last_name, dh.full_name, 'System') LIKE ?)");
params.push(`%${search}%`, `%${search}%`, `%${search}%`, `%${search}%`);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const total = db.prepare(`SELECT COUNT(*) as count FROM user_history h ${where}`).get(params).count;
const total = db.prepare(`
SELECT COUNT(*) as count
FROM user_history h
LEFT JOIN users u ON h.performed_by = u.id
LEFT JOIN deleted_users_history dh ON h.performed_by = dh.deleted_user_id
${where}
`).get(params).count;
const rows = db.prepare(`
SELECT
@@ -629,6 +723,20 @@ exports.remindTeachers = async (req, res) => {
if (sectionsWithAttentionStudents.length > 0) {
try {
// QEV: validate teacher email before sending reminder
const qevResult = await getQEV()(teacher.email);
if (!qevResult.valid) {
console.warn(`[RemindTeachers] Skipping ${teacher.email}: QEV says ${qevResult.reason}`);
results.push({
teacherId: teacher.id,
teacherName,
email: teacher.email,
status: 'skipped',
error: `Invalid email: ${qevResult.reason}`
});
skippedCount++;
continue;
}
await sendTeacherAttentionReminder({
to: teacher.email,
teacherName,
@@ -686,3 +794,17 @@ exports.remindTeachers = async (req, res) => {
};
exports.getBestServerBaseUrl = getBestServerBaseUrl;
// POST /api/users/validate-email — validate a single email via QEV (admin only)
exports.validateEmail = async (req, res) => {
const { email } = req.body;
if (!email || !email.trim()) {
return res.status(400).json({ valid: false, reason: 'Email is required' });
}
try {
const result = await getQEV()(email.trim());
res.json(result);
} catch (error) {
res.status(500).json({ valid: false, reason: 'Validation service error', error: error.message });
}
};
+217 -12
View File
@@ -513,6 +513,102 @@ const initSchema = () => {
)
`).run();
// ── New columns on documents for template origin tracking ─────────────
const docColsV2 = db.prepare("PRAGMA table_info(documents)").all();
if (!docColsV2.some(c => c.name === 'template_id')) {
db.prepare("ALTER TABLE documents ADD COLUMN template_id INTEGER DEFAULT NULL").run();
console.log('Migration: added template_id column to documents table');
}
if (!docColsV2.some(c => c.name === 'template_version_id')) {
db.prepare("ALTER TABLE documents ADD COLUMN template_version_id INTEGER DEFAULT NULL").run();
console.log('Migration: added template_version_id column to documents table');
}
// 7b. DocumentTemplates Table
db.prepare(`
CREATE TABLE IF NOT EXISTS document_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
requirement_id INTEGER,
name TEXT NOT NULL,
description TEXT,
is_active INTEGER DEFAULT 1,
created_by INTEGER,
created_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
FOREIGN KEY (requirement_id) REFERENCES document_requirements(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
)
`).run();
// 7c. TemplateVersions Table
db.prepare(`
CREATE TABLE IF NOT EXISTS template_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_id INTEGER NOT NULL,
version_number INTEGER NOT NULL DEFAULT 1,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER DEFAULT NULL,
uploaded_by INTEGER,
is_current INTEGER DEFAULT 0,
created_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
FOREIGN KEY (template_id) REFERENCES document_templates(id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
)
`).run();
// 7d. DocumentVersions Table
db.prepare(`
CREATE TABLE IF NOT EXISTS document_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id INTEGER NOT NULL,
version_number INTEGER NOT NULL DEFAULT 1,
file_name TEXT NOT NULL,
file_path TEXT NOT NULL,
file_size INTEGER DEFAULT NULL,
template_id INTEGER DEFAULT NULL,
template_version_id INTEGER DEFAULT NULL,
uploaded_by INTEGER,
notes TEXT,
created_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE,
FOREIGN KEY (template_id) REFERENCES document_templates(id) ON DELETE SET NULL,
FOREIGN KEY (template_version_id) REFERENCES template_versions(id) ON DELETE SET NULL,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
)
`).run();
// Migration: auto-seed Version 1 for every existing document that has no versions yet
try {
const docsWithoutVersions = db.prepare(`
SELECT d.id, d.file_name, d.file_path, d.file_size, d.uploaded_by, d.created_at,
d.template_id, d.template_version_id
FROM documents d
WHERE d.id NOT IN (SELECT DISTINCT document_id FROM document_versions)
AND d.deleted_at IS NULL
`).all();
if (docsWithoutVersions.length > 0) {
const seedVersionStmt = db.prepare(`
INSERT INTO document_versions
(document_id, version_number, file_name, file_path, file_size, template_id, template_version_id, uploaded_by, created_at)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?)
`);
db.transaction(() => {
for (const doc of docsWithoutVersions) {
seedVersionStmt.run(
doc.id, doc.file_name, doc.file_path, doc.file_size ?? null,
doc.template_id ?? null, doc.template_version_id ?? null,
doc.uploaded_by ?? null, doc.created_at
);
}
})();
console.log(`[Migration] Seeded document Version 1 for ${docsWithoutVersions.length} existing documents.`);
}
} catch (verErr) {
console.error('[Migration Error] Failed to seed document_versions:', verErr.message);
}
// 9. PrintQueue & Notifications
db.prepare(`
CREATE TABLE IF NOT EXISTS print_queue (
@@ -626,6 +722,75 @@ const initSchema = () => {
)
`).run();
// ── User Sessions (active logged-in device tracking) ──────────────────
// Populated on login, cleaned on logout / password-change / deactivation.
// token_hash is SHA-256 of the JWT so we never store the raw token.
db.prepare(`
CREATE TABLE IF NOT EXISTS user_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token_hash TEXT NOT NULL UNIQUE,
device_name TEXT DEFAULT NULL,
platform TEXT DEFAULT NULL,
ip_address TEXT DEFAULT NULL,
created_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
last_seen_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`).run();
// Safe additive column migrations for user_sessions (if table pre-exists without some columns)
const sessionCols = db.prepare('PRAGMA table_info(user_sessions)').all();
if (!sessionCols.some(c => c.name === 'device_name')) {
db.prepare('ALTER TABLE user_sessions ADD COLUMN device_name TEXT DEFAULT NULL').run();
}
if (!sessionCols.some(c => c.name === 'platform')) {
db.prepare('ALTER TABLE user_sessions ADD COLUMN platform TEXT DEFAULT NULL').run();
}
if (!sessionCols.some(c => c.name === 'ip_address')) {
db.prepare('ALTER TABLE user_sessions ADD COLUMN ip_address TEXT DEFAULT NULL').run();
}
if (!sessionCols.some(c => c.name === 'last_seen_at')) {
db.prepare("ALTER TABLE user_sessions ADD COLUMN last_seen_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))").run();
}
// ── User Login Logs (login/logout timestamp history) ──────────────────
// Append-only log: login_at always set, logout_at filled on explicit logout
// or null for sessions terminated by token expiry / deactivation.
db.prepare(`
CREATE TABLE IF NOT EXISTS user_login_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
username TEXT NOT NULL,
full_name TEXT NOT NULL,
role TEXT NOT NULL,
platform TEXT DEFAULT NULL,
ip_address TEXT DEFAULT NULL,
login_at DATETIME DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
logout_at DATETIME DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`).run();
// Safe additive column migrations for user_login_logs
const loginLogCols = db.prepare('PRAGMA table_info(user_login_logs)').all();
if (!loginLogCols.some(c => c.name === 'platform')) {
db.prepare('ALTER TABLE user_login_logs ADD COLUMN platform TEXT DEFAULT NULL').run();
}
if (!loginLogCols.some(c => c.name === 'ip_address')) {
db.prepare('ALTER TABLE user_login_logs ADD COLUMN ip_address TEXT DEFAULT NULL').run();
}
if (!loginLogCols.some(c => c.name === 'logout_at')) {
db.prepare('ALTER TABLE user_login_logs ADD COLUMN logout_at DATETIME DEFAULT NULL').run();
}
// Backfill legacy NULL / empty / unknown platform values to default 'windows'
try {
db.prepare("UPDATE user_sessions SET platform = 'windows' WHERE platform IS NULL OR platform = '' OR platform = 'unknown'").run();
db.prepare("UPDATE user_login_logs SET platform = 'windows' WHERE platform IS NULL OR platform = '' OR platform = 'unknown'").run();
db.prepare("UPDATE user_sessions SET device_name = 'Windows PC' WHERE device_name IS NULL OR device_name = '' OR device_name = 'Unknown Device'").run();
} catch (_) {}
// Migration: add category, entity_type, entity_id column to notifications if missing
const notifCols = db.prepare("PRAGMA table_info(notifications)").all();
if (!notifCols.some(c => c.name === 'category')) {
@@ -768,7 +933,15 @@ const initSchema = () => {
`).run();
db.prepare(`
INSERT OR IGNORE INTO system_settings (key, value)
VALUES ('auto_update_enrollment_from_sf', 'true')
VALUES ('auto_update_enrollment_from_sf', 'false')
`).run();
db.prepare(`
INSERT OR IGNORE INTO system_settings (key, value)
VALUES ('remind_teachers_due_date_enabled', 'false')
`).run();
db.prepare(`
INSERT OR IGNORE INTO system_settings (key, value)
VALUES ('remind_teachers_due_date_days', '3')
`).run();
// Seed Admin if not exists
@@ -782,17 +955,49 @@ const initSchema = () => {
console.log('Default Admin created: admin / admin123');
}
// Seed Hidden Developer Super Admin if not exists
const devAdminUser = db.prepare("SELECT * FROM users WHERE username = 'developer'").get();
if (!devAdminUser) {
const hashedDevPassword = bcrypt.hashSync('Developer@2026!', 10);
db.prepare(`
INSERT INTO users (username, password, first_name, last_name, role, email, is_hidden)
VALUES ('developer', ?, 'Developer', 'Super Admin', 'admin', 'dev@tis.edu.ph', 1)
`).run(hashedDevPassword);
console.log('Hidden Developer Super Admin created: developer / Developer@2026!');
} else if (devAdminUser.is_hidden !== 1) {
db.prepare("UPDATE users SET is_hidden = 1 WHERE username = 'developer'").run();
// Remove legacy hidden developer account if it still exists in the database
const devAdminUser = db.prepare("SELECT id FROM users WHERE username = 'developer'").get();
if (devAdminUser) {
const fallbackAdmin = db.prepare("SELECT id FROM users WHERE role = 'admin' AND username != 'developer' ORDER BY id ASC LIMIT 1").get();
const fallbackAdminId = fallbackAdmin ? fallbackAdmin.id : null;
if (fallbackAdminId) {
try { db.prepare("UPDATE documents SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_versions SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE template_versions SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_templates SET created_by = ? WHERE created_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_folders SET created_by = ? WHERE created_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE activity_log SET user_id = ? WHERE user_id = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE user_history SET performed_by = ? WHERE performed_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE printed_document_history SET user_id = ? WHERE user_id = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE recent_deleted SET deleted_by = ? WHERE deleted_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE deleted_users_history SET deleted_by = ? WHERE deleted_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {}
} else {
try { db.prepare("UPDATE documents SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_versions SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE template_versions SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_templates SET created_by = NULL WHERE created_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE document_folders SET created_by = NULL WHERE created_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE activity_log SET user_id = NULL WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE user_history SET performed_by = NULL WHERE performed_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE printed_document_history SET user_id = NULL WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE recent_deleted SET deleted_by = NULL WHERE deleted_by = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("UPDATE deleted_users_history SET deleted_by = NULL WHERE deleted_by = ?").run(devAdminUser.id); } catch (_) {}
}
try { db.prepare("DELETE FROM user_sessions WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM user_login_logs WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM fcm_tokens WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM notifications WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM password_reset_requests WHERE user_id = ? OR reviewed_by = ?").run(devAdminUser.id, devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM password_reset_otps WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM password_reset_links WHERE user_id = ? OR admin_id = ?").run(devAdminUser.id, devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM account_deletion_requests WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM print_queue WHERE user_id = ?").run(devAdminUser.id); } catch (_) {}
try { db.prepare("DELETE FROM teacher_sections WHERE teacher_id = ?").run(devAdminUser.id); } catch (_) {}
db.prepare("DELETE FROM users WHERE id = ?").run(devAdminUser.id);
console.log('Removed legacy developer account from database.');
}
// Seed default document requirements if none exist
+14
View File
@@ -24,5 +24,19 @@ router.post('/request-delete-account', authenticateToken, authController.request
router.get('/confirm-delete-account-web', authController.confirmDeleteAccountWebPage);
router.post('/confirm-delete-account', authController.confirmDeleteAccount);
// Self-Service Account Deactivation (immediate, 2-step client confirmation)
router.post('/self-deactivate', authenticateToken, authController.selfDeactivateAccount);
// Session management (logged-in devices)
router.get('/sessions', authenticateToken, authController.getSessions);
router.delete('/sessions', authenticateToken, authController.revokeAllOtherSessions);
router.delete('/sessions/:id', authenticateToken, authController.revokeSession);
// Logout (stamps logout_at + removes session)
router.post('/logout', authenticateToken, authController.logout);
// Login/logout history (admin only)
router.get('/login-logs', authenticateToken, authorizeRoles('admin'), authController.getLoginLogs);
module.exports = router;
+5 -1
View File
@@ -6,6 +6,7 @@ const { authenticateToken, authorizeRoles } = require('../middleware/auth');
router.get('/', authenticateToken, documentController.getAllDocuments);
router.get('/requirements', authenticateToken, documentController.getRequirements);
router.get('/statuses', authenticateToken, documentController.getStatuses);
router.get('/types', authenticateToken, documentController.getDocumentTypes);
// Print Queue routes — must be BEFORE /:id routes to avoid conflict
router.get('/print-queue', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.getPrintQueue);
@@ -13,6 +14,7 @@ router.get('/print-history', authenticateToken, authorizeRoles('admin', 'teacher
router.delete('/print-history/clear', authenticateToken, authorizeRoles('admin'), documentController.clearPrintHistory);
router.post('/print-queue', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.addToPrintQueue);
router.post('/print-queue/print', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.executePrintQueue);
router.post('/print-queue/merge-pdf', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.mergePrintQueuePdf);
router.delete('/print-queue/clear', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.clearPrintQueue);
router.delete('/print-queue/:queueId', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.removeFromPrintQueue);
router.post('/print-notify-email', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.sendPrintPickupNotification);
@@ -32,8 +34,10 @@ router.post('/bulk-print', authenticateToken, authorizeRoles('admin', 'teacher')
router.post('/bulk-copy', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkCopy);
router.get('/:id/view', authenticateToken, documentController.viewDocument);
router.get('/:id/versions', authenticateToken, documentController.getDocumentVersions);
router.post('/:id/upload-version', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.uploadMiddleware, documentController.uploadDocumentVersion);
router.get('/:id/download', authenticateToken, documentController.downloadDocumentVersion);
router.get('/:id/thumbnail', authenticateToken, documentController.getThumbnail);
router.post('/:id/convert-to-pdf', authenticateToken, documentController.convertToPdf);
router.post('/:id/copy', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.copyDocument);
router.patch('/:id/status', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.updateStatus);
router.post('/:id/restore', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.restoreDocument);
+4
View File
@@ -11,5 +11,9 @@ router.get('/export-data', authenticateToken, reportsController.getExportData);
router.get('/yearly-comparison', authenticateToken, reportsController.getYearlyComparison);
router.get('/storage', authenticateToken, reportsController.getStorageUsed);
router.get('/transparency-board', authenticateToken, reportsController.getTransparencyBoardData);
router.get('/transparency-board/pdf', authenticateToken, reportsController.generateTransparencyBoardPdf);
router.get('/compliance/excel', authenticateToken, reportsController.generateComplianceReportExcel);
router.post('/compliance/excel', authenticateToken, reportsController.generateComplianceReportExcel);
module.exports = router;
+3
View File
@@ -9,6 +9,7 @@ const { authenticateToken, authorizeRoles } = require('../middleware/auth');
router.get('/academic-years', authenticateToken, setupController.getAllAcademicYears);
router.post('/academic-years', authenticateToken, authorizeRoles('admin'), setupController.createAcademicYear);
router.post('/academic-years/check-auto-graduation', authenticateToken, authorizeRoles('admin'), setupController.checkAutoGraduation);
router.post('/academic-years/check-auto-archive', authenticateToken, authorizeRoles('admin'), setupController.checkAutoArchive);
router.put('/academic-years/:id', authenticateToken, authorizeRoles('admin'), setupController.updateAcademicYear);
router.delete('/academic-years/:id', authenticateToken, authorizeRoles('admin'), setupController.deleteAcademicYear);
@@ -18,7 +19,9 @@ router.delete('/academic-years/:id', authenticateToken, authorizeRoles('admin'),
router.get('/sections', authenticateToken, setupController.getAllSections);
router.get('/academic-years/:yearId/sections', authenticateToken, setupController.getSectionsByYear);
router.post('/sections', authenticateToken, authorizeRoles('admin'), setupController.createSection);
router.post('/bulk-academic-structure', authenticateToken, authorizeRoles('admin'), setupController.bulkCreateAcademicStructure);
router.put('/sections/:id', authenticateToken, authorizeRoles('admin'), setupController.updateSection);
router.put('/sections/:id/adviser', authenticateToken, authorizeRoles('admin'), setupController.setSectionAdviser);
router.delete('/sections/:id', authenticateToken, authorizeRoles('admin'), setupController.deleteSection);
// ==========================================
+4 -1
View File
@@ -9,14 +9,17 @@ router.get('/:id', authenticateToken, studentController.getStudentById);
// Create / Update / Delete restricted to super_admin & admin
router.post('/', authenticateToken, authorizeRoles('admin'), studentController.createStudent);
router.post('/verify-lrns', authenticateToken, authorizeRoles('admin'), studentController.verifyLrns);
router.post('/bulk-enroll', authenticateToken, authorizeRoles('admin'), studentController.bulkEnrollStudents);
router.post('/bulk-ocr-import', authenticateToken, authorizeRoles('admin'), studentController.bulkCreateStudents);
router.put('/bulk-graduate', authenticateToken, authorizeRoles('admin'), studentController.bulkGraduate);
router.post('/bulk-status', authenticateToken, authorizeRoles('admin'), studentController.bulkStatusStudents);
router.put('/:id', authenticateToken, authorizeRoles('admin'), studentController.updateStudent);
router.put('/:id', authenticateToken, authorizeRoles('admin', 'teacher'), studentController.updateStudent);
router.delete('/:id', authenticateToken, authorizeRoles('admin'), studentController.deleteStudent);
router.post('/:id/enrollments', authenticateToken, authorizeRoles('admin'), studentController.addEnrollment);
router.post('/:id/verify-enrollments', authenticateToken, authorizeRoles('admin'), studentController.verifyStudentEnrollments);
router.post('/:id/bulk-enrollments', authenticateToken, authorizeRoles('admin'), studentController.bulkAddStudentEnrollments);
router.post('/:id/ocr-enrollment', authenticateToken, authorizeRoles('admin'), studentController.scanEnrollmentFromSF);
router.put('/enrollments/:enrollmentId', authenticateToken, authorizeRoles('admin'), studentController.updateEnrollment);
router.delete('/enrollments/:enrollmentId', authenticateToken, authorizeRoles('admin'), studentController.deleteEnrollment);
+40
View File
@@ -0,0 +1,40 @@
const express = require('express');
const router = express.Router();
const templateController = require('../controllers/templateController');
const { authenticateToken, authorizeRoles } = require('../middleware/auth');
// ── Read (all authenticated) ──────────────────────────────────────────────────
router.get('/', authenticateToken, templateController.getTemplates);
router.get('/:id', authenticateToken, templateController.getTemplateById);
router.get('/:id/versions', authenticateToken, templateController.getTemplateVersions);
router.get('/:id/download', authenticateToken, templateController.downloadTemplate);
// ── Admin only ────────────────────────────────────────────────────────────────
router.post(
'/',
authenticateToken,
authorizeRoles('admin'),
templateController.uploadMiddleware,
templateController.createTemplate
);
router.patch(
'/:id',
authenticateToken,
authorizeRoles('admin'),
templateController.updateTemplate
);
router.post(
'/:id/upload-version',
authenticateToken,
authorizeRoles('admin'),
templateController.uploadMiddleware,
templateController.uploadTemplateVersion
);
router.delete(
'/:id',
authenticateToken,
authorizeRoles('admin'),
templateController.deleteTemplate
);
module.exports = router;
+3 -1
View File
@@ -19,5 +19,7 @@ router.post('/:teacherId/sections',authenticateToken, authorizeRoles('admin'), u
// User History (admin only — dashboard access)
router.get('/history', authenticateToken, authorizeRoles('admin'), userController.getUserHistory);
module.exports = router;
// Email validation via QEV (admin only)
router.post('/validate-email', authenticateToken, authorizeRoles('admin'), userController.validateEmail);
module.exports = router;
+188
View File
@@ -0,0 +1,188 @@
const db = require('../config/db');
const logActivity = (userId, action, entityType, entityId, description) => {
try {
let uid = userId;
const userCheck = uid ? db.prepare("SELECT id FROM users WHERE id = ?").get(uid) : null;
if (!userCheck) {
const anyUser = db.prepare("SELECT id FROM users LIMIT 1").get();
uid = anyUser ? anyUser.id : null;
}
db.prepare(`
INSERT INTO activity_log (user_id, action, entity_type, entity_id, description)
VALUES (?, ?, ?, ?, ?)
`).run(uid, action, entityType, entityId, description);
} catch (err) {
console.error('[autoArchiveService] logActivity error:', err.message);
}
};
/**
* Checks the active academic year start_date.
* If not set or no active year, does not run auto-archive.
* Grace period (in days) is fetched from system_settings ('enrollment_grace_period_days', default 30).
* If today >= (start_date + grace_period_days):
* ONLY targets students with status = 'Enrolled' (never touches Graduated, Transferred, Dropped, or Inactive).
* Checks which of these 'Enrolled' students do NOT have an enrollment record in the active academic year.
* Updates their status to 'Inactive'.
* Archives their active documents (status = 'Archived').
* Logs the activity to activity_log.
*/
const checkAndRunAutoArchive = (userId = 1) => {
try {
const enabledRow = db.prepare("SELECT value FROM system_settings WHERE key = 'auto_archive_enabled'").get();
if (enabledRow?.value === 'false') {
return { executed: false, reason: 'Automatic archiving is disabled in system settings.' };
}
const activeYear = db.prepare("SELECT * FROM academic_years WHERE status = 'active' LIMIT 1").get();
if (!activeYear) {
return { executed: false, reason: 'No active academic year found.' };
}
let scheduleDesc = '';
// Check if an explicit auto_archive_datetime is set in system_settings
const dtRow = db.prepare("SELECT value FROM system_settings WHERE key = 'auto_archive_datetime'").get();
if (dtRow?.value && dtRow.value.trim().length > 0) {
const scheduledDt = new Date(dtRow.value);
if (!isNaN(scheduledDt.getTime())) {
const now = new Date();
if (now < scheduledDt) {
return {
executed: false,
reason: `Auto-archive scheduled date and time (${dtRow.value}) has not been reached yet.`
};
}
scheduleDesc = `scheduled cutoff (${dtRow.value})`;
}
}
if (!scheduleDesc) {
if (!activeYear.start_date) {
return {
executed: false,
reason: 'Active academic year has no start_date configured.'
};
}
// Fetch grace period days from system_settings (defaults to 30 days)
const graceRow = db.prepare("SELECT value FROM system_settings WHERE key = 'enrollment_grace_period_days'").get();
const graceDays = Math.max(1, parseInt(graceRow?.value || '30', 10));
// Calculate cutoff date: start_date + graceDays
const startDate = new Date(activeYear.start_date + 'T00:00:00');
if (isNaN(startDate.getTime())) {
return { executed: false, reason: 'Invalid start_date format in active academic year.' };
}
const cutoffDate = new Date(startDate);
cutoffDate.setDate(cutoffDate.getDate() + graceDays);
const cutoffDateStr = cutoffDate.toISOString().slice(0, 10);
const todayStr = new Date().toISOString().slice(0, 10);
if (todayStr < cutoffDateStr) {
return {
executed: false,
reason: `Enrollment grace period (${graceDays} days) from academic year start (${activeYear.start_date}) has not been reached yet. Cutoff date is ${cutoffDateStr} (today: ${todayStr}).`
};
}
scheduleDesc = `${graceDays}-day grace period`;
}
// STRICT REQUIREMENT: ONLY TARGET STUDENTS WITH status = 'Enrolled'
// Never alters students who are already Graduated, Transferred, Dropped, or Inactive.
const unEnrolledStudents = db.prepare(`
SELECT DISTINCT s.id, s.lrn, s.first_name, s.last_name
FROM students s
WHERE s.status = 'Enrolled'
AND s.id NOT IN (
SELECT student_id FROM enrollments WHERE academic_year_id = ?
)
ORDER BY s.id ASC
`).all(activeYear.id);
if (unEnrolledStudents.length === 0) {
return {
executed: true,
archivedCount: 0,
message: `All enrolled students are registered in active academic year ${activeYear.year_range}. No students auto-archived.`
};
}
const updateStudentStatus = db.prepare("UPDATE students SET status = 'Inactive' WHERE id = ?");
const archiveStudentDocs = db.prepare(`
UPDATE documents
SET status = 'Archived'
WHERE student_id = ? AND deleted_at IS NULL AND status != 'Archived'
`);
db.transaction(() => {
for (const st of unEnrolledStudents) {
updateStudentStatus.run(st.id);
archiveStudentDocs.run(st.id);
}
})();
const studentSummary = unEnrolledStudents.slice(0, 20).map(s => `${s.lrn || ''} (${s.first_name} ${s.last_name})`.trim()).join(', ');
const extraText = unEnrolledStudents.length > 20 ? ` and ${unEnrolledStudents.length - 20} more` : '';
logActivity(
userId,
'ARCHIVE',
'student',
null,
`Auto-archived ${unEnrolledStudents.length} student(s) to 'Inactive' (no enrollment in AY ${activeYear.year_range} after ${scheduleDesc}): ${studentSummary}${extraText}`
);
return {
executed: true,
archivedCount: unEnrolledStudents.length,
message: `Successfully auto-archived ${unEnrolledStudents.length} student(s) to 'Inactive'.`
};
} catch (err) {
console.error('[autoArchiveService] checkAndRunAutoArchive error:', err);
return { executed: false, error: err.message };
}
};
let archiveTimer = null;
const scheduleAutoArchiveTimer = () => {
try {
if (archiveTimer) {
clearTimeout(archiveTimer);
archiveTimer = null;
}
const enabledRow = db.prepare("SELECT value FROM system_settings WHERE key = 'auto_archive_enabled'").get();
if (enabledRow?.value === 'false') {
console.log('[autoArchiveService] Auto-archiving is disabled in system settings. Timer not scheduled.');
return;
}
const dtRow = db.prepare("SELECT value FROM system_settings WHERE key = 'auto_archive_datetime'").get();
if (dtRow?.value && dtRow.value.trim().length > 0) {
const scheduledDt = new Date(dtRow.value);
if (!isNaN(scheduledDt.getTime())) {
const now = Date.now();
const diff = scheduledDt.getTime() - now;
if (diff > 0 && diff < 2147483647) {
console.log(`[autoArchiveService] Precision timer set: auto-archiving will execute in ${Math.round(diff / 1000)}s (${scheduledDt.toLocaleString()})`);
archiveTimer = setTimeout(() => {
console.log('[autoArchiveService] Scheduled time arrived! Executing auto-archive check...');
checkAndRunAutoArchive(1);
}, diff);
} else if (diff <= 0) {
console.log(`[autoArchiveService] Scheduled cutoff (${scheduledDt.toLocaleString()}) is now or in past, checking auto-archive immediately.`);
checkAndRunAutoArchive(1);
}
}
}
} catch (err) {
console.error('[autoArchiveService] scheduleAutoArchiveTimer error:', err.message);
}
};
module.exports = {
checkAndRunAutoArchive,
scheduleAutoArchiveTimer
};
File diff suppressed because it is too large. Load diff
+227
View File
@@ -0,0 +1,227 @@
'use strict';
const ExcelJS = require('exceljs');
/**
* Generate Compliance Report Excel (.xlsx) buffer matching TIS RMS design and layout.
*
* @param {Object} data
* @param {Object} data.studentCounts - { active, dropped, transferee, graduated, inactive, fourPs }
* @param {Array} data.missingDocsBreakdown - [ { name, count } ]
* @param {Array} data.students - [ { lrn, first_name, last_name, fullName, sex, grade_level, gradeLevel, section_name, sectionName, status, missing_count, missingCount, missing_requirements, missingRequirements } ]
* @param {string} data.yearLabel - e.g. "2025-2026" or "All Years"
* @returns {Promise<Buffer>}
*/
async function generateComplianceExcel({ studentCounts = {}, missingDocsBreakdown = [], students = [], yearLabel = 'All Years' }) {
const workbook = new ExcelJS.Workbook();
workbook.creator = 'TIS RMS';
workbook.lastModifiedBy = 'TIS RMS';
workbook.created = new Date();
workbook.modified = new Date();
const borderThin = {
top: { style: 'thin', color: { argb: 'FFE2E8F0' } },
left: { style: 'thin', color: { argb: 'FFE2E8F0' } },
bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
right: { style: 'thin', color: { argb: 'FFE2E8F0' } }
};
// ── SHEET 1: Summary ────────────────────────────────────────────────────────
const summarySheet = workbook.addWorksheet('Summary', {
views: [{ showGridLines: true }]
});
summarySheet.columns = [
{ width: 44 },
{ width: 18 }
];
// Title & Metadata
const titleRow = summarySheet.addRow(['TALISAY INTEGRATED SCHOOL - TIS RMS']);
titleRow.font = { name: 'Arial', size: 14, bold: true, color: { argb: 'FF166534' } };
const subTitleRow = summarySheet.addRow([`Annual Report Summary: ${yearLabel}`]);
subTitleRow.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF334155' } };
const genDateStr = new Date().toISOString().replace('T', ' ').substring(0, 19);
const dateRow = summarySheet.addRow([`Generated: ${genDateStr}`]);
dateRow.font = { name: 'Arial', size: 9, italic: true, color: { argb: 'FF64748B' } };
summarySheet.addRow([]); // Blank spacer
// Section 1: Student Statistics
const sec1Header = summarySheet.addRow(['STUDENT STATISTICS', '']);
sec1Header.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF166534' } };
sec1Header.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
sec1Header.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
const statHeader = summarySheet.addRow(['Student Status', 'Total Count']);
statHeader.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FF1E293B' } };
statHeader.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
statHeader.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
statHeader.getCell(2).alignment = { horizontal: 'right' };
statHeader.getCell(1).border = borderThin;
statHeader.getCell(2).border = borderThin;
const statsRowsData = [
['Active (Enrolled)', Number(studentCounts.active || 0)],
['Dropouts (Dropped)', Number(studentCounts.dropped || 0)],
['Transferees (Transferred)', Number(studentCounts.transferee || 0)],
['Graduated', Number(studentCounts.graduated || 0)],
];
for (const [label, count] of statsRowsData) {
const r = summarySheet.addRow([label, count]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF334155' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
}
summarySheet.addRow([]); // Blank spacer
// Section 2: Missing Documents Per Requirement Type
const sec2Header = summarySheet.addRow(['MISSING DOCUMENTS PER REQUIREMENT TYPE', '']);
sec2Header.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF166534' } };
sec2Header.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
sec2Header.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
const docHeader = summarySheet.addRow(['Document Type', 'Missing Count']);
docHeader.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FF1E293B' } };
docHeader.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
docHeader.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
docHeader.getCell(2).alignment = { horizontal: 'right' };
docHeader.getCell(1).border = borderThin;
docHeader.getCell(2).border = borderThin;
if (missingDocsBreakdown.length === 0) {
const r = summarySheet.addRow(['All student requirements complete', 0]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF64748B' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
} else {
for (const row of missingDocsBreakdown) {
const r = summarySheet.addRow([row.name, Number(row.count || 0)]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF334155' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
}
}
// ── SHEET 2: Student Compliance List ────────────────────────────────────────
const listSheet = workbook.addWorksheet('Student Compliance List', {
views: [{ state: 'frozen', xSplit: 0, ySplit: 1, showGridLines: true }]
});
listSheet.columns = [
{ header: '#', key: 'index', width: 6 },
{ header: 'LRN', key: 'lrn', width: 18 },
{ header: 'Student Name', key: 'student_name', width: 28 },
{ header: 'Sex', key: 'sex', width: 10 },
{ header: 'Grade Level', key: 'grade_level', width: 14 },
{ header: 'Section', key: 'section', width: 18 },
{ header: 'Status', key: 'status', width: 14 },
{ header: 'Missing Count', key: 'missing_count', width: 15 },
{ header: 'Missing Documents', key: 'missing_docs', width: 50 },
];
// Style the header row
const listHeaderRow = listSheet.getRow(1);
listHeaderRow.height = 26;
listHeaderRow.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FFFFFFFF' } };
listHeaderRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF166534' } // Forest Green
};
listHeaderRow.alignment = { vertical: 'middle', horizontal: 'center' };
listHeaderRow.getCell('student_name').alignment = { vertical: 'middle', horizontal: 'left' };
listHeaderRow.getCell('missing_docs').alignment = { vertical: 'middle', horizontal: 'left' };
for (let c = 1; c <= 9; c++) {
listHeaderRow.getCell(c).border = borderThin;
}
// Populate Student Data Rows
for (let i = 0; i < students.length; i++) {
const s = students[i];
const fullName = s.fullName ||
([s.last_name, s.first_name].filter(Boolean).join(', ') || 'N/A');
const gradeVal = s.grade_level != null ? s.grade_level : s.gradeLevel;
const gradeStr = gradeVal != null ? `Grade ${gradeVal}` : 'N/A';
const sectionStr = s.section_name || s.sectionName || 'N/A';
const missingCount = s.missing_count != null ? Number(s.missing_count) : Number(s.missingCount || 0);
const missingDocsStr = s.missing_requirements || s.missingRequirements ||
(missingCount === 0 ? 'None (Complete)' : 'N/A');
const row = listSheet.addRow({
index: i + 1,
lrn: s.lrn || '',
student_name: fullName,
sex: s.sex || 'N/A',
grade_level: gradeStr,
section: sectionStr,
status: s.status || 'N/A',
missing_count: missingCount,
missing_docs: missingDocsStr,
});
row.height = 20;
row.font = { name: 'Arial', size: 9.5, color: { argb: 'FF1F2937' } };
const isEven = i % 2 === 1;
const rowBg = isEven ? 'FFF8FAFC' : 'FFFFFFFF';
for (let colIdx = 1; colIdx <= 9; colIdx++) {
const cell = row.getCell(colIdx);
cell.border = borderThin;
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowBg } };
cell.alignment = { vertical: 'middle' };
}
// Specific cell alignments
row.getCell('index').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('lrn').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('sex').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('grade_level').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('status').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('missing_count').alignment = { vertical: 'middle', horizontal: 'right' };
row.getCell('student_name').alignment = { vertical: 'middle', horizontal: 'left' };
row.getCell('missing_docs').alignment = { vertical: 'middle', horizontal: 'left' };
// Highlight missing count if > 0
if (missingCount > 0) {
row.getCell('missing_count').font = {
name: 'Arial',
size: 9.5,
bold: true,
color: { argb: 'FFDC2626' }
};
} else {
row.getCell('missing_count').font = {
name: 'Arial',
size: 9.5,
color: { argb: 'FF166534' }
};
}
}
// Auto-filter on student list
if (students.length > 0) {
listSheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: students.length + 1, column: 9 }
};
}
const buffer = await workbook.xlsx.writeBuffer();
return Buffer.from(buffer);
}
module.exports = {
generateComplianceExcel,
};
-263
View File
@@ -1,263 +0,0 @@
const fs = require('fs');
const path = require('path');
const { exec, execSync } = require('child_process');
const util = require('util');
const ExcelJS = require('exceljs');
const execAsync = util.promisify(exec);
/**
* Service for converting documents (Excel, etc.) to PDF using Headless LibreOffice.
*
* Features:
* 1. Headless LibreOffice CLI: Completely isolated, sandboxed execution with zero GUI
* popups, no COM object dependencies, and no risk of desktop/IDE interference.
* 2. Automatic Long Bond Paper (Folio 8.5" x 13") formatting via ExcelJS preprocessing.
* 3. Removes static/shrunk zoom percentages so tables fit 100% of printable page width.
*/
class LibreOfficeService {
/**
* Finds the path to the LibreOffice / soffice executable.
* @returns {string|null}
*/
static getExecutablePath() {
// 1. Explicit environment variable
if (process.env.LIBREOFFICE_PATH && fs.existsSync(process.env.LIBREOFFICE_PATH)) {
return process.env.LIBREOFFICE_PATH;
}
const isWindows = process.platform === 'win32';
if (isWindows) {
const candidatePaths = [
path.join(__dirname, '..', '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.com'),
path.join(__dirname, '..', '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
path.join(__dirname, '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.com'),
path.join(__dirname, '..', '..', 'bin', 'LibreOffice', 'program', 'soffice.exe'),
'C:\\Program Files\\LibreOffice\\program\\soffice.com',
'C:\\Program Files\\LibreOffice\\program\\soffice.exe',
'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.com',
'C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe',
];
for (const candidate of candidatePaths) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
try {
const output = execSync('where.exe soffice.com soffice.exe', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
const firstLine = output.trim().split(/\r?\n/)[0];
if (firstLine && fs.existsSync(firstLine)) {
return firstLine;
}
} catch (_) {}
} else {
const candidatePaths = [
'/usr/bin/soffice',
'/usr/bin/libreoffice',
'/usr/local/bin/soffice',
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
];
for (const candidate of candidatePaths) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
try {
const output = execSync('which soffice || which libreoffice', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
const firstLine = output.trim().split(/\r?\n/)[0];
if (firstLine && fs.existsSync(firstLine)) {
return firstLine;
}
} catch (_) {}
}
return null;
}
/**
* Checks if LibreOffice is available on the system.
* @returns {boolean}
*/
static isLibreOfficeAvailable() {
return this.getExecutablePath() !== null;
}
/**
* Checks if the conversion engine is available.
* @returns {boolean}
*/
static isAvailable() {
return this.isLibreOfficeAvailable();
}
/**
* Preprocesses an Excel file using ExcelJS to configure all visible worksheets:
* 1. Paper size = 14 (Folio 8.5" x 13" / Long Bond Paper standard).
* 2. Removes any static/shrunken zoom scale (e.g. scale: 36%) so fitToPage takes effect.
* 3. Sets fitToWidth = 1 so content fills the entire page width.
* 4. Centers content horizontally with compact 0.25" margins.
*
* @param {string} inputFilePath - Source Excel path
* @param {string} outputDir - Directory to store temporary prepared Excel file
* @returns {Promise<string>} - Path to prepared Excel file (or original if not applicable)
*/
static async prepareWorkbookForLongBond(inputFilePath, outputDir) {
const ext = path.extname(inputFilePath).toLowerCase();
if (ext !== '.xlsx') {
return inputFilePath;
}
try {
const wb = new ExcelJS.Workbook();
await wb.xlsx.readFile(inputFilePath);
let modified = false;
wb.eachSheet((ws) => {
if (ws.state === 'hidden' || ws.state === 'veryHidden') return;
if (!ws.pageSetup) ws.pageSetup = {};
// OpenXML standard Folio: 8.5 x 13 in (Philippine Long Bond Paper)
ws.pageSetup.paperSize = 14;
// Remove hardcoded/shrunken zoom scale so fitToPage takes effect
delete ws.pageSetup.scale;
ws.pageSetup.fitToPage = true;
ws.pageSetup.fitToWidth = 1;
if (!ws.pageSetup.fitToHeight) {
ws.pageSetup.fitToHeight = 1;
}
// Center horizontally
ws.pageSetup.horizontalCentered = true;
modified = true;
});
if (modified) {
const prepFileName = `__prep_${Date.now()}_${path.basename(inputFilePath)}`;
const prepFilePath = path.join(outputDir, prepFileName);
await wb.xlsx.writeFile(prepFilePath);
return prepFilePath;
}
} catch (err) {
console.warn('[DocumentConverter] Workbook preprocessing skipped:', err.message);
}
return inputFilePath;
}
/**
* Converts an Excel file to PDF using headless LibreOffice.
*
* @param {string} inputFilePath - Absolute path to source Excel file
* @param {string} outputDir - Directory where converted PDF should be stored
* @param {string} expectedPdfPath - Expected final PDF destination path
* @returns {Promise<void>}
*/
static async convertWithLibreOffice(inputFilePath, outputDir, expectedPdfPath) {
const sofficeBin = this.getExecutablePath();
if (!sofficeBin) {
throw new Error('LibreOffice (soffice) was not found on the server.');
}
const normalizedBin = path.normalize(sofficeBin).replace(/\\/g, '/');
const normalizedOut = path.normalize(outputDir).replace(/\\/g, '/');
const normalizedIn = path.normalize(inputFilePath).replace(/\\/g, '/');
const command = `"${normalizedBin}" --headless --invisible --nologo --nodefault --nofirststartwizard --convert-to pdf --outdir "${normalizedOut}" "${normalizedIn}"`;
console.log(`[DocumentConverter:LibreOffice] Executing: ${command}`);
const { stdout, stderr } = await execAsync(command, {
timeout: 60000,
maxBuffer: 10 * 1024 * 1024,
});
if (stdout) console.log(`[DocumentConverter:LibreOffice] stdout: ${stdout.trim()}`);
if (stderr) console.warn(`[DocumentConverter:LibreOffice] stderr: ${stderr.trim()}`);
// LibreOffice names output PDF after input file basename
const generatedPdfName = `${path.basename(inputFilePath, path.extname(inputFilePath))}.pdf`;
const generatedPdfPath = path.join(outputDir, generatedPdfName);
if (generatedPdfPath !== expectedPdfPath && fs.existsSync(generatedPdfPath)) {
if (fs.existsSync(expectedPdfPath)) {
try { fs.unlinkSync(expectedPdfPath); } catch (_) {}
}
fs.renameSync(generatedPdfPath, expectedPdfPath);
}
if (!fs.existsSync(expectedPdfPath)) {
throw new Error(`LibreOffice finished but output PDF was not found at "${expectedPdfPath}".`);
}
}
/**
* Converts an Excel file (or compatible document) to PDF using Headless LibreOffice.
*
* @param {string} inputFilePath - Absolute path to the source Excel file
* @param {string} outputDir - Directory where the converted PDF should be stored
* @returns {Promise<{ pdfPath: string, pdfFileName: string, engine: string }>}
*/
static async convertToPdf(inputFilePath, outputDir) {
if (!fs.existsSync(inputFilePath)) {
throw new Error(`Source file not found at: ${inputFilePath}`);
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
if (!this.isLibreOfficeAvailable()) {
const isWindows = process.platform === 'win32';
const helpMessage = isWindows
? 'LibreOffice was not found. Please ensure LibreOffice is located in backend/bin/LibreOffice or install it from https://www.libreoffice.org.'
: 'LibreOffice was not found. Please install LibreOffice using "sudo apt-get install -y libreoffice".';
throw new Error(helpMessage);
}
const ext = path.extname(inputFilePath);
const baseName = path.basename(inputFilePath, ext);
const expectedPdfName = `${baseName}.pdf`;
const expectedPdfPath = path.join(outputDir, expectedPdfName);
// Remove any stale pre-existing file with the same temporary output name
if (fs.existsSync(expectedPdfPath)) {
try {
fs.unlinkSync(expectedPdfPath);
} catch (_) {}
}
// Preprocess workbook with ExcelJS to set paperSize = 14 (Folio 8.5x13) & fit-to-page
let preparedPath = inputFilePath;
let isTempPrepared = false;
try {
preparedPath = await this.prepareWorkbookForLongBond(inputFilePath, outputDir);
isTempPrepared = (preparedPath !== inputFilePath);
} catch (prepErr) {
console.warn('[DocumentConverter] Could not pre-process workbook:', prepErr.message);
}
try {
await this.convertWithLibreOffice(preparedPath, outputDir, expectedPdfPath);
console.log(`[DocumentConverter] Successfully converted "${baseName}" using Headless LibreOffice.`);
return {
pdfPath: expectedPdfPath,
pdfFileName: expectedPdfName,
engine: 'LIBREOFFICE',
};
} finally {
// Clean up temporary prepared workbook if created
if (isTempPrepared && fs.existsSync(preparedPath)) {
try { fs.unlinkSync(preparedPath); } catch (_) {}
}
}
}
}
module.exports = LibreOfficeService;
module.exports.DocumentConverterService = LibreOfficeService;
File diff suppressed because it is too large. Load diff
+96
View File
@@ -0,0 +1,96 @@
const { Server } = require('socket.io');
const jwt = require('jsonwebtoken');
let io = null;
/**
* Initializes Socket.IO on the given HTTP server.
* Supports both WebSocket and polling transports with open CORS for LAN and Cloudflare tunnel clients.
*/
function initSocket(httpServer) {
io = new Server(httpServer, {
cors: {
origin: '*',
methods: ['GET', 'POST'],
credentials: true,
},
transports: ['websocket', 'polling'],
allowEIO3: true,
});
// Optional auth middleware: verifies JWT if supplied, but does not block connection
io.use((socket, next) => {
const token =
socket.handshake.auth?.token ||
socket.handshake.headers?.authorization?.replace(/^Bearer\s+/i, '');
if (token) {
try {
const secret = process.env.JWT_SECRET || 'tis-rms-jwt-secret-key-development';
const decoded = jwt.verify(token, secret);
socket.user = decoded;
} catch (err) {
console.warn(`[Socket.IO] Token verification notice for socket ${socket.id}: ${err.message}`);
}
}
next();
});
io.on('connection', (socket) => {
const userDesc = socket.user ? `@${socket.user.username} (${socket.user.role})` : 'client';
console.log(`[Socket.IO] Client connected: ${socket.id} [${userDesc}]`);
socket.on('disconnect', (reason) => {
console.log(`[Socket.IO] Client disconnected: ${socket.id} [${reason}]`);
});
});
console.log('[Socket.IO] Real-time WebSocket server initialized successfully.');
return io;
}
function getIO() {
return io;
}
function broadcastEvent(eventName, payload) {
if (!io) return;
try {
io.emit(eventName, payload);
} catch (err) {
console.error(`[Socket.IO] Failed to emit "${eventName}":`, err.message);
}
}
// Real-Time Student Event Emitters
function emitStudentAdded(data) {
broadcastEvent('student_added', data);
}
function emitStudentUpdated(data) {
broadcastEvent('student_updated', data);
}
function emitStudentDeleted(data) {
broadcastEvent('student_deleted', data);
}
// Real-Time Notification & Document Emitters
function emitNotificationCreated(data) {
broadcastEvent('notification_created', data);
}
function emitDocumentChanged(data) {
broadcastEvent('document_changed', data);
}
module.exports = {
initSocket,
getIO,
broadcastEvent,
emitStudentAdded,
emitStudentUpdated,
emitStudentDeleted,
emitNotificationCreated,
emitDocumentChanged,
};
@@ -0,0 +1,246 @@
const db = require('../config/db');
const { sendTeacherAttentionReminder } = require('./emailService');
const logActivity = (userId, action, entityType, entityId, description) => {
try {
let uid = userId;
const userCheck = uid ? db.prepare("SELECT id FROM users WHERE id = ?").get(uid) : null;
if (!userCheck) {
const anyUser = db.prepare("SELECT id FROM users WHERE role = 'admin' LIMIT 1").get();
uid = anyUser ? anyUser.id : null;
}
db.prepare(`
INSERT INTO activity_log (user_id, action, entity_type, entity_id, description)
VALUES (?, ?, ?, ?, ?)
`).run(uid, action, entityType, entityId, description);
} catch (err) {
console.error('[teacherReminderService] logActivity error:', err.message);
}
};
/**
* Checks document requirements with due dates and sends automated reminder emails
* to advisory teachers whose enrolled students are missing those requirements.
*/
const checkAndRunTeacherDueDateReminders = async (forced = false) => {
try {
const enabledRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_enabled'").get();
const isEnabled = enabledRow?.value === 'true';
if (!isEnabled && !forced) {
return { executed: false, reason: 'Teacher due date reminders are disabled in system settings.' };
}
const daysRow = db.prepare("SELECT value FROM system_settings WHERE key = 'remind_teachers_due_date_days'").get();
const reminderDays = parseInt(daysRow?.value || '3', 10) || 3;
// Today's date string YYYY-MM-DD
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const todayStr = `${year}-${month}-${day}`;
// Check if already executed today
if (!forced) {
const lastRunRow = db.prepare("SELECT value FROM system_settings WHERE key = 'last_teacher_reminder_date'").get();
if (lastRunRow?.value === todayStr) {
return { executed: false, reason: 'Teacher due date reminders already ran today.' };
}
}
// Get enabled document requirements that have a due date set
const requirements = db.prepare(`
SELECT id, name, category, due_date, is_mandatory
FROM document_requirements
WHERE is_enabled = 1 AND due_date IS NOT NULL AND TRIM(due_date) != ''
`).all();
if (requirements.length === 0) {
if (!forced) {
db.prepare(`
INSERT INTO system_settings (key, value, updated_at)
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
`).run(todayStr);
}
return { executed: false, reason: 'No document requirements with due dates found.' };
}
// Filter requirements that are due within reminderDays (0 <= diffDays <= reminderDays)
const todayDateOnly = new Date(`${todayStr}T00:00:00`);
const dueSoonRequirements = requirements.filter(req => {
const reqDateStr = req.due_date.split('T')[0].split(' ')[0];
const reqDate = new Date(`${reqDateStr}T00:00:00`);
const diffTime = reqDate.getTime() - todayDateOnly.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return diffDays >= 0 && diffDays <= reminderDays;
});
if (dueSoonRequirements.length === 0) {
if (!forced) {
db.prepare(`
INSERT INTO system_settings (key, value, updated_at)
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
`).run(todayStr);
}
return { executed: false, reason: `No document requirements due within the next ${reminderDays} day(s).` };
}
// Get active teachers with emails
const teachers = db.prepare(`
SELECT id, username, first_name, middle_name, last_name, email
FROM users
WHERE role = 'teacher' AND is_active = 1 AND email IS NOT NULL AND TRIM(email) != ''
`).all();
if (teachers.length === 0) {
return { executed: false, reason: 'No active teachers with valid emails found.' };
}
let sentCount = 0;
let skippedCount = 0;
for (const teacher of teachers) {
const teacherName = `${teacher.first_name} ${teacher.last_name}`.trim();
// Get sections assigned to this teacher
const sections = db.prepare(`
SELECT s.id, s.name, s.grade_level
FROM teacher_sections ts
JOIN sections s ON ts.section_id = s.id
WHERE ts.teacher_id = ?
ORDER BY s.grade_level ASC, s.name ASC
`).all(teacher.id);
if (sections.length === 0) {
skippedCount++;
continue;
}
const sectionsWithAttentionStudents = [];
for (const section of sections) {
// Find enrolled students in this section
const students = db.prepare(`
SELECT s.id, s.lrn, s.first_name, s.middle_name, s.last_name, s.extension
FROM enrollments e
JOIN students s ON e.student_id = s.id
WHERE e.section_id = ? AND s.status = 'Enrolled'
ORDER BY s.last_name ASC, s.first_name ASC
`).all(section.id);
const sectionAttentionStudents = [];
for (const student of students) {
const studentFullName = [student.last_name, student.first_name, student.middle_name, student.extension]
.filter(Boolean)
.join(' ');
// Check missing requirements among the due soon requirements
const studentLevelCategory = section.grade_level <= 10 ? 'JHS' : 'SHS';
const matchingReqs = dueSoonRequirements.filter(r => r.category === studentLevelCategory);
if (matchingReqs.length === 0) continue;
const missingForStudent = [];
for (const req of matchingReqs) {
const hasCompleted = db.prepare(`
SELECT id FROM documents
WHERE student_id = ? AND requirement_id = ? AND status = 'Completed' AND deleted_at IS NULL
LIMIT 1
`).get(student.id, req.id);
if (!hasCompleted) {
const reqDateStr = req.due_date.split('T')[0].split(' ')[0];
missingForStudent.push(`${req.name} (Due: ${reqDateStr})`);
}
}
if (missingForStudent.length > 0) {
sectionAttentionStudents.push({
id: student.id,
lrn: student.lrn,
name: studentFullName,
missingDocs: missingForStudent
});
}
}
if (sectionAttentionStudents.length > 0) {
sectionsWithAttentionStudents.push({
sectionId: section.id,
sectionName: section.name,
gradeLevel: section.grade_level,
students: sectionAttentionStudents
});
}
}
if (sectionsWithAttentionStudents.length > 0) {
try {
// Validate teacher email before sending reminder (MEV / QEV)
let validEmail = true;
try {
const { validateEmailMEV } = require('../controllers/authController');
const mevResult = await validateEmailMEV(teacher.email);
if (!mevResult.valid) {
validEmail = false;
console.warn(`[TeacherReminderService] Skipping ${teacher.email}: ${mevResult.reason}`);
}
} catch (_) {}
if (!validEmail) {
skippedCount++;
continue;
}
await sendTeacherAttentionReminder({
to: teacher.email,
teacherName,
sectionsWithStudents: sectionsWithAttentionStudents
});
sentCount++;
} catch (err) {
console.error(`[TeacherReminderService] Failed to send reminder email to ${teacher.email}:`, err.message);
}
} else {
skippedCount++;
}
}
// Mark today as executed
db.prepare(`
INSERT INTO system_settings (key, value, updated_at)
VALUES ('last_teacher_reminder_date', ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
`).run(todayStr);
if (sentCount > 0) {
logActivity(
null,
'NOTIFY',
'system_settings',
null,
`Automated due date reminder emails sent to ${sentCount} teacher(s)`
);
console.log(`[TeacherReminderService] Successfully sent due date reminder emails to ${sentCount} teacher(s).`);
}
return {
executed: true,
sentCount,
skippedCount,
dueRequirementsCount: dueSoonRequirements.length
};
} catch (error) {
console.error('[TeacherReminderService] checkAndRunTeacherDueDateReminders error:', error);
return { executed: false, error: error.message };
}
};
module.exports = {
checkAndRunTeacherDueDateReminders
};
+302
View File
@@ -0,0 +1,302 @@
/**
* TIS-RMS Static Web Application Logic
* Interactive UI behavior for Windows and Android layouts
*/
let currentDevice = 'windows'; // 'windows' | 'android'
let currentScreen = 'dashboard';
// ── Screen Definitions ─────────────────────────────────────────
const SCREENS = [
{ id: 'dashboard', label: 'Dashboard', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>' },
{ id: 'students', label: 'Students', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>' },
{ id: 'documents', label: 'Documents', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>' },
{ id: 'archives', label: 'Archives', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"/></svg>' },
{ id: 'reports', label: 'Reports', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z"/><path d="M19 19H5V5h14v14m0-16H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>' },
{ id: 'users', label: 'Users', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>' },
{ id: 'history', label: 'History', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42A8.954 8.954 0 0 0 13 21a9 9 0 0 0 0-18zm-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8H12z"/></svg>' },
{ id: 'settings', label: 'Settings', icon: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>' },
];
document.addEventListener('DOMContentLoaded', () => {
setupDeviceSwitcher();
setupNavigation();
startClock();
setScreen('dashboard');
});
// ── Device Switcher ────────────────────────────────────────────
function setupDeviceSwitcher() {
const winBtn = document.getElementById('btn-view-windows');
const androidBtn = document.getElementById('btn-view-android');
const winWindow = document.getElementById('desktop-window');
const phoneMockup = document.getElementById('phone-frame');
winBtn.addEventListener('click', () => {
currentDevice = 'windows';
winBtn.classList.add('active');
androidBtn.classList.remove('active');
winWindow.style.display = 'flex';
phoneMockup.style.display = 'none';
syncScreenView();
});
androidBtn.addEventListener('click', () => {
currentDevice = 'android';
androidBtn.classList.add('active');
winBtn.classList.remove('active');
winWindow.style.display = 'none';
phoneMockup.style.display = 'flex';
syncScreenView();
});
}
// ── Navigation ─────────────────────────────────────────────────
function setupNavigation() {
// Desktop Sidebar
document.querySelectorAll('.win-nav-item').forEach(item => {
item.addEventListener('click', () => {
const scr = item.getAttribute('data-screen');
if (scr) setScreen(scr);
});
});
// Mobile Drawer items
document.querySelectorAll('.mobile-drawer-item').forEach(item => {
item.addEventListener('click', () => {
const scr = item.getAttribute('data-screen');
if (scr) {
setScreen(scr);
closeMobileDrawer();
}
});
});
}
function openMobileDrawer() {
const drawer = document.getElementById('mobile-drawer');
const backdrop = document.getElementById('mobile-drawer-backdrop');
if (drawer) drawer.classList.add('active');
if (backdrop) backdrop.classList.add('active');
}
function closeMobileDrawer() {
const drawer = document.getElementById('mobile-drawer');
const backdrop = document.getElementById('mobile-drawer-backdrop');
if (drawer) drawer.classList.remove('active');
if (backdrop) backdrop.classList.remove('active');
}
function setScreen(screenId) {
currentScreen = screenId;
syncScreenView();
}
function syncScreenView() {
// 1. Update active sidebar item on Windows
document.querySelectorAll('.win-nav-item').forEach(el => {
el.classList.toggle('active', el.getAttribute('data-screen') === currentScreen);
});
// 2. Update active drawer item on Android
document.querySelectorAll('.mobile-drawer-item').forEach(el => {
el.classList.toggle('active', el.getAttribute('data-screen') === currentScreen);
});
// 3. Update titles
const desktopTitles = {
dashboard: 'Dashboard Overview',
students: 'Students Directory',
documents: 'Document Folders',
archives: 'Archived Records',
reports: 'DepEd Transparency Board',
users: 'User Management',
history: 'System Audit Logs',
settings: 'Account Settings'
};
const mobileTitles = {
dashboard: 'Dashboard',
students: 'Students',
documents: 'Documents',
archives: 'Archives',
reports: 'Reports',
users: 'Users',
history: 'History',
settings: 'Settings'
};
const desktopTitle = document.getElementById('stage-screen-title');
if (desktopTitle) desktopTitle.textContent = desktopTitles[currentScreen] || 'Dashboard Overview';
const mobileTitle = document.getElementById('phone-screen-title');
if (mobileTitle) mobileTitle.textContent = mobileTitles[currentScreen] || 'TIS RMS';
// 4. Update mobile header action buttons (matching screenshots)
const mobileActions = document.getElementById('phone-header-actions');
if (mobileActions) {
switch (currentScreen) {
case 'dashboard':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-icon" title="Notifications">🔔</button>
<div class="avatar-badge" style="width: 28px; height: 28px; font-size: 10px;">AT</div>
`;
break;
case 'students':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-icon" title="Filters">🎛️</button>
<button class="phone-action-btn-green" onclick="openAddStudentModal()" title="Add Student">+ 👤</button>
`;
break;
case 'documents':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-icon" title="Print">🖨️</button>
<button class="phone-action-btn-green" title="Upload">☁️</button>
`;
break;
case 'archives':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-icon" title="Print">🖨️</button>
`;
break;
case 'reports':
mobileActions.innerHTML = `
<button class="phone-action-btn-green" style="padding: 4px 8px; font-size: 12px;" title="Swap View">⇄</button>
`;
break;
case 'users':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-btn-green" title="Add User">+ 👤</button>
`;
break;
case 'history':
mobileActions.innerHTML = `
<button class="phone-action-icon" title="Search">🔍</button>
<button class="phone-action-icon" title="Filter">🎛️</button>
<button class="phone-action-icon" title="Refresh">🔄</button>
`;
break;
case 'settings':
default:
mobileActions.innerHTML = ``;
break;
}
}
// 5. Show/hide desktop screens
document.querySelectorAll('.desktop-screen-content').forEach(view => {
view.style.display = view.id === `win-screen-${currentScreen}` ? 'flex' : 'none';
});
// 6. Show/hide mobile screens
document.querySelectorAll('.mobile-screen-content').forEach(view => {
view.style.display = view.id === `phone-screen-${currentScreen}` ? 'flex' : 'none';
});
// 7. Show "+ Add Student" button on Students screen (Desktop)
const addBtn = document.getElementById('btn-add-student-header');
if (addBtn) {
addBtn.style.display = currentScreen === 'students' ? 'flex' : 'none';
}
}
// ── Collapsible Settings Sections ──────────────────────────────
function toggleSettingsSection(id) {
const body = document.getElementById(`settings-body-${id}`);
const chevron = document.getElementById(`settings-chevron-${id}`);
if (!body) return;
const isHidden = body.style.display === 'none' || body.style.display === '';
body.style.display = isHidden ? 'flex' : 'none';
if (chevron) {
chevron.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';
}
}
// ── Interactive Modals ─────────────────────────────────────────
function openAddStudentModal() {
const modal = document.getElementById('add-student-modal');
if (modal) modal.classList.add('active');
}
function openStudentDetail(name, lrn, grade, status) {
const modal = document.getElementById('student-detail-modal');
const title = document.getElementById('detail-student-name');
const lrnEl = document.getElementById('detail-student-lrn');
const gradeEl = document.getElementById('detail-student-grade');
const statusEl = document.getElementById('detail-student-status');
if (title) title.textContent = name;
if (lrnEl) lrnEl.textContent = lrn;
if (gradeEl) gradeEl.textContent = grade;
if (statusEl) statusEl.textContent = status;
if (modal) modal.classList.add('active');
}
function closeModal(id) {
const modal = document.getElementById(id);
if (modal) modal.classList.remove('active');
}
// ── Check For Updates Simulation (Real feature test) ───────────
function simulateUpdateCheck() {
const btn = document.getElementById('btn-check-updates');
const banner = document.getElementById('update-result-banner');
const toast = document.getElementById('download-toast');
const toastFile = document.getElementById('toast-filename');
if (!btn) return;
btn.disabled = true;
btn.innerHTML = '<span>Checking for updates...</span>';
banner.style.display = 'none';
setTimeout(() => {
btn.disabled = false;
btn.innerHTML = '<span>Check for Updates</span>';
const isWin = currentDevice === 'windows';
const arch = isWin ? 'x64 (64-bit)' : 'ARM64 (64-bit)';
const file = isWin ? 'TIS_RMS_Setup_v1.0.24.exe' : 'TIS_RMS_64_v1.0.24.apk';
banner.style.display = 'block';
banner.innerHTML = `
<div style="font-weight: 700; color: var(--accent-emerald); margin-bottom: 4px;">
✓ New Update Available: v1.0.24
</div>
<div style="font-size: 12px; color: #cbd5e1; line-height: 1.4;">
Detected architecture: <strong>${arch}</strong><br/>
Matched package: <strong>${file}</strong><br/>
Launching download link in default browser...
</div>
`;
if (toast && toastFile) {
toastFile.textContent = file;
toast.classList.add('active');
setTimeout(() => {
toast.classList.remove('active');
}, 5000);
}
}, 1200);
}
// ── Clock for Android Status Bar ───────────────────────────────
function startClock() {
function updateTime() {
const el = document.getElementById('phone-clock');
if (!el) return;
const now = new Date();
let hours = now.getHours();
let mins = now.getMinutes();
mins = mins < 10 ? '0' + mins : mins;
el.textContent = `${hours}:${mins}`;
}
updateTime();
setInterval(updateTime, 30000);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

+1595
View File
File diff suppressed because it is too large. Load diff
+1753
View File
File diff suppressed because it is too large. Load diff
+422
View File
@@ -0,0 +1,422 @@
/**
* TIS-RMS DOCUMENTATION INTERACTIVE CONTROLLER
* Handles:
* 1. Eye-Friendly Dark/Light Theme Switching with LocalStorage
* 2. Dedicated Print Action
* 3. Windows / Android Platform Filter with Persistent State
* 4. Multi-Page Live Search with Keyboard Navigation (Arrows + Enter)
* 5. Mockup Platform Tab Switching
* 6. Responsive Mobile & Landscape Sidebar Navigation
* 7. Image Lightbox Zoom Modal
* 8. FAQ Accordion Interaction
*/
document.addEventListener('DOMContentLoaded', () => {
// ==========================================
// 1. THEME TOGGLE (EYE-FRIENDLY SLATE DARK / CLEAN LIGHT)
// ==========================================
const themeToggleBtn = document.getElementById('themeToggleBtn');
const savedTheme = localStorage.getItem('tis_doc_theme') ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeButton(savedTheme);
function updateThemeButton(theme) {
if (!themeToggleBtn) return;
const isDark = theme === 'dark';
themeToggleBtn.setAttribute('title', isDark ? 'Switch to light theme' : 'Switch to eye-friendly dark theme');
themeToggleBtn.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to eye-friendly dark theme');
}
if (themeToggleBtn) {
themeToggleBtn.addEventListener('click', () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('tis_doc_theme', newTheme);
updateThemeButton(newTheme);
});
}
// ==========================================
// 2. PRINT HANDLER
// ==========================================
const printBtn = document.getElementById('printBtn');
if (printBtn) {
printBtn.addEventListener('click', (e) => {
e.preventDefault();
window.print();
});
}
// ==========================================
// 3. GLOBAL PLATFORM FILTER (ALL / WINDOWS / ANDROID)
// ==========================================
const platformFilterBtns = document.querySelectorAll('.platform-pill-toggle .platform-btn');
const savedPlatform = localStorage.getItem('tis_doc_platform') || 'all';
function applyPlatformFilter(filter) {
// 1. Update button states in header
platformFilterBtns.forEach(btn => {
const bFilter = btn.getAttribute('data-platform-filter');
btn.classList.toggle('active', bFilter === filter);
});
// 2. Set root attribute for CSS styling
document.documentElement.setAttribute('data-doc-platform', filter);
// 3. Save to localStorage
localStorage.setItem('tis_doc_platform', filter);
// 4. Update mockup container tabs on the current page
document.querySelectorAll('.mockup-container').forEach(container => {
const winTabBtn = container.querySelector('[data-tab-target^="win-"]');
const andTabBtn = container.querySelector('[data-tab-target^="and-"]');
if (filter === 'windows' && winTabBtn) {
winTabBtn.click();
} else if (filter === 'android' && andTabBtn) {
andTabBtn.click();
} else if (filter === 'all' && winTabBtn) {
winTabBtn.click();
}
});
// 5. In shortcut tables or platform badges, adjust visual state
document.querySelectorAll('.shortcut-group-windows, .shortcut-group-android').forEach(el => {
if (filter === 'all') {
el.style.opacity = '';
} else if (filter === 'windows') {
el.style.opacity = el.classList.contains('shortcut-group-windows') ? '1' : '0.35';
} else if (filter === 'android') {
el.style.opacity = el.classList.contains('shortcut-group-android') ? '1' : '0.35';
}
});
}
// Initialize platform filter from storage
applyPlatformFilter(savedPlatform);
// Attach click listener to platform pill buttons
platformFilterBtns.forEach(btn => {
btn.addEventListener('click', () => {
const filter = btn.getAttribute('data-platform-filter');
applyPlatformFilter(filter);
});
});
// ==========================================
// 4. MOCKUP TAB SWITCHING (IN-PAGE WINDOWS / ANDROID)
// ==========================================
document.querySelectorAll('.mockup-container').forEach(container => {
const tabBtns = container.querySelectorAll('.mockup-tab-btn');
const panes = container.querySelectorAll('.mockup-pane');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetId = btn.getAttribute('data-tab-target');
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
panes.forEach(pane => {
if (pane.id === targetId) {
pane.style.display = 'block';
pane.classList.add('active');
} else {
pane.style.display = 'none';
pane.classList.remove('active');
}
});
});
});
});
// ==========================================
// 5. MOBILE NAVIGATION & SEARCH
// ==========================================
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const docSidebar = document.getElementById('docSidebar');
const sidebarBackdrop = document.getElementById('sidebarBackdrop');
const mobileSearchBtn = document.getElementById('mobileSearchBtn');
const headerSearchBox = document.getElementById('headerSearchBox');
const docSearchInput = document.getElementById('docSearchInput');
function openMobileSidebar() {
if (docSidebar) docSidebar.classList.add('open');
if (sidebarBackdrop) sidebarBackdrop.classList.add('active');
if (mobileMenuBtn) {
const ham = mobileMenuBtn.querySelector('.hamburger-icon');
const close = mobileMenuBtn.querySelector('.close-icon');
if (ham) ham.style.display = 'none';
if (close) close.style.display = 'block';
}
document.body.style.overflow = 'hidden';
}
function closeMobileSidebar() {
if (docSidebar) docSidebar.classList.remove('open');
if (sidebarBackdrop) sidebarBackdrop.classList.remove('active');
if (mobileMenuBtn) {
const ham = mobileMenuBtn.querySelector('.hamburger-icon');
const close = mobileMenuBtn.querySelector('.close-icon');
if (ham) ham.style.display = 'block';
if (close) close.style.display = 'none';
}
document.body.style.overflow = '';
}
if (mobileMenuBtn) {
mobileMenuBtn.addEventListener('click', () => {
const isOpen = docSidebar && docSidebar.classList.contains('open');
if (isOpen) {
closeMobileSidebar();
} else {
openMobileSidebar();
}
});
}
if (sidebarBackdrop) {
sidebarBackdrop.addEventListener('click', closeMobileSidebar);
}
// Close mobile sidebar when clicking any navigation link
document.querySelectorAll('.doc-sidebar .nav-link').forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth <= 1024) {
closeMobileSidebar();
}
});
});
// Mobile search toggle button in header
if (mobileSearchBtn && headerSearchBox) {
mobileSearchBtn.addEventListener('click', () => {
const isOpen = headerSearchBox.classList.toggle('mobile-open');
if (isOpen && docSearchInput) {
docSearchInput.focus();
}
});
}
// ==========================================
// 6. FAQ ACCORDION HANDLER
// ==========================================
document.querySelectorAll('.faq-item').forEach(item => {
const questionBtn = item.querySelector('.faq-question');
if (questionBtn) {
questionBtn.addEventListener('click', () => {
const isActive = item.classList.contains('active');
item.classList.toggle('active', !isActive);
});
}
});
// ==========================================
// 7. LIGHTBOX ZOOM MODAL
// ==========================================
const lightboxModal = document.getElementById('lightboxModal');
const lightboxImg = document.getElementById('lightboxImg');
const lightboxCaption = document.getElementById('lightboxCaption');
const lightboxCloseBtn = document.getElementById('lightboxCloseBtn');
function openLightbox(src, caption) {
if (!lightboxModal || !lightboxImg) return;
lightboxImg.src = src;
if (lightboxCaption) {
lightboxCaption.textContent = caption || '';
}
lightboxModal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
if (!lightboxModal) return;
lightboxModal.classList.remove('active');
document.body.style.overflow = '';
}
document.querySelectorAll('[data-lightbox]').forEach(el => {
el.addEventListener('click', () => {
const src = el.getAttribute('data-lightbox');
const caption = el.getAttribute('data-caption') || '';
openLightbox(src, caption);
});
});
if (lightboxCloseBtn) {
lightboxCloseBtn.addEventListener('click', closeLightbox);
}
if (lightboxModal) {
lightboxModal.addEventListener('click', (e) => {
if (e.target === lightboxModal) {
closeLightbox();
}
});
}
// ==========================================
// 8. MULTI-PAGE LIVE SEARCH & QUICK NAVIGATION
// ==========================================
const searchInput = document.getElementById('docSearchInput');
const searchableSections = document.querySelectorAll('.doc-section, .hero-banner');
const DOC_PAGES = [
{ title: 'System Overview', url: 'index.html', desc: 'System overview, school background, key capabilities, and handbook introduction' },
{ title: 'Staff Roles & Access', url: 'roles.html', desc: 'Administrator vs. Teacher permissions, advisory scoping, and registration permissions' },
{ title: 'Sign In & Getting Started', url: 'login.html', desc: 'Login credentials, Android & Windows app access, remote tunnel, and OTP recovery' },
{ title: 'Module 1: Dashboard', url: 'dashboard.html', desc: 'Real-time metrics, role toolbars, visual charts, top students, and attention lists' },
{ title: 'Module 2: Students', url: 'students.html', desc: 'Learner profiles, dynamic filters, SF9/SF10 OCR autofill, bulk CSV import, multi-select' },
{ title: 'Module 3: Documents', url: 'documents.html', desc: 'Virtual student folders, Android camera scanner, collapsible checklist, file previews' },
{ title: 'Module 4: Archives', url: 'archives.html', desc: 'Historical learner archives, file restoration, and automatic inactive archiving' },
{ title: 'Module 5: Batch Print', url: 'batchprint.html', desc: 'Multi-document print queue, Excel-to-PDF conversion, and email pickup notifications' },
{ title: 'Module 6: Reports & Analytics', url: 'reports.html', desc: 'DepEd transparency board, compliance breakdown, storage analytics, Excel export' },
{ title: 'Module 7: User Accounts', url: 'users.html', desc: 'Staff account management, teacher assignments, activation, and password resets' },
{ title: 'Module 8: Activity History', url: 'history.html', desc: 'System activity logs, user lifecycle history, active sessions, and IP address audits' },
{ title: 'Module 9: School Settings', url: 'settings.html', desc: 'Academic setup, dynamic document checklists, automated reminders, and graduation' },
{ title: 'Keyboard Shortcuts', url: 'shortcuts.html', desc: 'Hotkeys and touch gestures for Windows desktop and Android tablets' },
{ title: 'Frequently Asked Questions', url: 'faq.html', desc: 'Common questions regarding offline mode, remote access, OCR, and backups' },
{ title: 'Help & Troubleshooting', url: 'troubleshooting.html', desc: 'Diagnostic steps for LAN connections, scanner troubleshooting, and sync' }
];
let searchDropdown = document.getElementById('searchDropdown');
if (!searchDropdown && headerSearchBox) {
searchDropdown = document.createElement('div');
searchDropdown.id = 'searchDropdown';
searchDropdown.className = 'search-dropdown';
headerSearchBox.appendChild(searchDropdown);
}
let selectedResultIndex = -1;
function updateSelectedResult(items) {
items.forEach((item, index) => {
item.classList.toggle('selected', index === selectedResultIndex);
if (index === selectedResultIndex) {
item.scrollIntoView({ block: 'nearest' });
}
});
}
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim().toLowerCase();
selectedResultIndex = -1;
// Current page element filtering
if (!query) {
if (searchDropdown) {
searchDropdown.classList.remove('active');
searchDropdown.innerHTML = '';
}
searchableSections.forEach(sec => {
sec.style.display = '';
sec.querySelectorAll('.feature-card, .step-card, tr, .component-screenshot, .mockup-container').forEach(item => {
item.style.display = '';
});
});
return;
}
searchableSections.forEach(sec => {
const text = sec.textContent.toLowerCase();
if (text.includes(query)) {
sec.style.display = '';
sec.querySelectorAll('.feature-card, .step-card').forEach(card => {
const cardText = card.textContent.toLowerCase();
card.style.display = cardText.includes(query) ? '' : 'none';
});
}
});
// Multi-page suggestion dropdown
if (searchDropdown) {
const matches = DOC_PAGES.filter(p =>
p.title.toLowerCase().includes(query) ||
p.desc.toLowerCase().includes(query)
);
if (matches.length > 0) {
searchDropdown.innerHTML = matches.map((m, idx) => `
<a href="${m.url}" class="search-result-item" data-index="${idx}">
<span class="search-result-title">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>
${m.title}
</span>
<span class="search-result-desc">${m.desc}</span>
</a>
`).join('');
searchDropdown.classList.add('active');
} else {
searchDropdown.classList.remove('active');
searchDropdown.innerHTML = '';
}
}
});
// Keyboard navigation in search input: ArrowDown, ArrowUp, Enter, Escape
searchInput.addEventListener('keydown', (e) => {
if (!searchDropdown || !searchDropdown.classList.contains('active')) return;
const items = searchDropdown.querySelectorAll('.search-result-item');
if (items.length === 0) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedResultIndex = (selectedResultIndex + 1) % items.length;
updateSelectedResult(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedResultIndex = (selectedResultIndex - 1 + items.length) % items.length;
updateSelectedResult(items);
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedResultIndex >= 0 && selectedResultIndex < items.length) {
items[selectedResultIndex].click();
} else if (items.length > 0) {
items[0].click();
}
}
});
document.addEventListener('click', (e) => {
if (searchDropdown && !headerSearchBox.contains(e.target)) {
searchDropdown.classList.remove('active');
}
});
// Global keyboard shortcut: Ctrl+F, Ctrl+K, or / focuses search
window.addEventListener('keydown', (e) => {
if ((e.ctrlKey && (e.key.toLowerCase() === 'f' || e.key.toLowerCase() === 'k')) || (e.key === '/' && document.activeElement !== searchInput)) {
e.preventDefault();
searchInput.focus();
searchInput.select();
} else if (e.key === 'Escape') {
if (searchDropdown) searchDropdown.classList.remove('active');
if (lightboxModal && lightboxModal.classList.contains('active')) {
closeLightbox();
} else if (document.activeElement === searchInput) {
searchInput.blur();
}
}
});
}
// ==========================================
// 9. ACTIVE PAGE SIDEBAR HIGHLIGHTING & LANDSCAPE SCROLL
// ==========================================
const navLinks = document.querySelectorAll('.doc-sidebar .nav-link');
const currentPath = window.location.pathname.split('/').pop() || 'index.html';
navLinks.forEach(link => {
const linkHref = link.getAttribute('href');
if (linkHref === currentPath || (currentPath === '' && linkHref === 'index.html') || (currentPath === 'overview.html' && linkHref === 'index.html')) {
link.classList.add('active');
// On landscape or small screens, ensure active link is visible in scrollable sidebar
link.scrollIntoView({ block: 'nearest' });
}
});
});
+350
View File
@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 4: Student Archives - TIS-RMS User Guide</title>
<meta name="description" content="Historical record storage, archived virtual folders, file restoration, and automatic inactive student archiving configuration.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 8: Module 4: Student Archives -->
<section id="archives" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 4: Student Archives</h2>
<p class="section-subtitle">Separate historical record storage, virtual archive envelopes, and configurable automatic archiving.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>
Archives Highlights
</div>
<ul class="module-summary-list">
<li><strong>Isolated Historical Storage:</strong> Archived records are segregated from active rosters to prevent accidental modification while keeping records readily retrievable.</li>
<li><strong>Archived Virtual Folders:</strong> Inspect stored documents and past submissions inside an archived student's dedicated folder.</li>
<li><strong>Document Archiving &amp; Restoration:</strong> Move individual files to the archive or restore them back to active student checklists when permitted.</li>
<li><strong>Automatic Archiving:</strong> The system automatically evaluates learners without active enrollment after a configured grace period. Configure this in <a href="settings.html#settings-archiving">Automatic Archiving Settings</a>.</li>
</ul>
</div>
<p class="paragraph">
The Student Archives module safeguards historical learner folders, transcripts, and certificates for graduated, transferred, or inactive students. Archived records remain searchable by LRN or name, allowing the school registrar to issue credentials years after a student has departed.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-arc">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-arc">Android</button>
</div>
<div class="mockup-pane active" id="win-arc">
<figure class="doc-figure" data-lightbox="assets/images/windows/archives.webp" data-caption="Windows Archives showing inactive student records and search">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/archives.webp" alt="Windows Archives" loading="lazy">
</div>
<figcaption>
<span>Figure 5A: Windows Student Archives Directory</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-arc" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/archives.webp" data-caption="Android Archives View">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/archives.webp" alt="Android Archives" loading="lazy">
</div>
<figcaption>
<span>Figure 5B: Android Archives View</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Archive Records Management</h3>
<p class="paragraph">
TIS-RMS strictly separates archived records from current academic cohorts:
</p>
<ul class="bullet-list">
<li><strong>Separation of Records:</strong> Displays archived student records and documents independently from active records, ensuring that historical searches never clutter active classroom management.</li>
<li><strong>Student Archives Folder View:</strong> Administrators can open any archived student's folder to view, preview, or download their stored documents.</li>
<li><strong>Document Archiving &amp; Restoration:</strong> Active documents can be relocated to the archive repository, and previously archived documents can be restored back to active student checklists whenever permitted.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/archives_list.svg" data-caption="Archived Student Records Directory">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/archives_list.svg" alt="Archived Student Records Directory" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Archived Student Records Directory</span>
<p class='screenshot-sub'>Searchable archive repository keeping historical student profiles cleanly segregated from active rosters</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/archives_folder.svg" data-caption="Archived Student Virtual Folder &amp; Files">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/archives_folder.svg" alt="Archived Student Virtual Folder &amp; Files" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Archived Student Virtual Folder &amp; Files</span>
<p class='screenshot-sub'>Archived virtual folder view showing historical SF9/SF10 documents and one-click file restoration</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Automatic Archiving Engine</h3>
<p class="paragraph">
To minimize administrative overhead at the end of each school term, TIS-RMS features an automated background archiving engine:
</p>
<ul class="bullet-list">
<li><strong>Grace Period Evaluation:</strong> The system automatically identifies students who no longer possess an active enrollment in the current academic year after the expiration of a configurable grace period.</li>
<li><strong>Configurable Rules:</strong> Administrators can fine-tune the grace period and automatic schedule in the <a href="settings.html#settings-archiving">Automatic Archiving Settings</a> panel.</li>
</ul>
<nav class="page-navigation" aria-label="Page navigation">
<a href="documents.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 3: Documents & Student Folders</span>
</a>
<a href="batchprint.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 5: Batch Print Records</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

+342
View File
@@ -0,0 +1,342 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 5: Batch Print Records - TIS-RMS User Guide</title>
<meta name="description" content="Multi-document print queue management, Excel-to-PDF conversion, print history audit, and automated student/guardian pickup alerts.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 9: Module 5: Batch Print Records -->
<section id="batchprint" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 5: Batch Print Records</h2>
<p class="section-subtitle">Multi-document queue management, format conversion, and automated email pickup notifications.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
Batch Print Highlights
</div>
<ul class="module-summary-list">
<li><strong>Print List Queue:</strong> Review queued documents, remove items individually, or clear the entire queue before sending jobs to the printer.</li>
<li><strong>Student Pickup Notification:</strong> Automatically notify learners or parents by email with pickup date, designated office counter, and special instructions.</li>
<li><strong>Print History Audit:</strong> Maintain a searchable historical record of all completed print batches.</li>
<li><strong>Batch Printing with Format Conversion:</strong> Print multiple files across students in one pass; Excel files automatically convert to PDF with user confirmation.</li>
</ul>
</div>
<p class="paragraph">
The Batch Print module streamlines document fulfillment for transcript requests, graduation packages, and DepEd compliance portfolios. Staff can queue records directly from the document explorer or preview modal.
</p>
<h3 class="subheading">Admin &amp; Teacher Side: Managing the Print Queue</h3>
<p class="paragraph">
Both Administrators and Teachers can utilize the print list modal to manage print jobs:
</p>
<ul class="bullet-list">
<li><strong>Review &amp; Edit Queue:</strong> Opens a modal displaying all documents currently staged for printing. Staff can inspect file thumbnails, delete individual entries, or clear the entire queue.</li>
<li><strong>Direct Queue Ingestion:</strong> Documents can be added to the print queue instantly from document previews, folder views, or table action menus.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/print_queue.svg" data-caption="Batch Print Queue Modal &amp; Document Review">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/print_queue.svg" alt="Batch Print Queue Modal &amp; Document Review" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Batch Print Queue Modal &amp; Document Review</span>
<p class='screenshot-sub'>Print queue manager showing queued files, page counts, clear queue options, and batch print triggers</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Student &amp; Guardian Pickup Notification</h3>
<p class="paragraph">
When requested documents are printed and ready for collection, staff can send an automated notification directly from the print modal:
</p>
<ul class="bullet-list">
<li><strong>Student/Guardian Email:</strong> Enter or confirm the registered recipient email address.</li>
<li><strong>Pickup Date:</strong> Specify when the physical documents will be available for pickup at the school office.</li>
<li><strong>Office Notes or Instructions:</strong> Include special requirements such as valid IDs, authorization letters, or office counter locations.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/print_email.svg" data-caption="Student/Guardian Email Pickup Notification Form">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/print_email.svg" alt="Student/Guardian Email Pickup Notification Form" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Student/Guardian Email Pickup Notification Form</span>
<p class='screenshot-sub'>Integrated email alert form specifying pickup date, office instructions, and document readiness</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Print History &amp; Batch Conversion</h3>
<ul class="bullet-list">
<li><strong>Print History:</strong> Keeps an accessible audit log of past print jobs, showing filenames, requester, and timestamps.</li>
<li><strong>Batch Printing &amp; File Conversion:</strong> Send multiple learner records to the printer in a single coordinated print operation. When Excel-based records are queued, the system automatically converts them to PDF and displays a confirmation dialog.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/print_history.svg" data-caption="Print History Audit Log">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/print_history.svg" alt="Print History Audit Log" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Print History Audit Log</span>
<p class='screenshot-sub'>Chronological record of past print batches with requester names, document counts, and timestamps</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="archives.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 4: Student Archives</span>
</a>
<a href="reports.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 6: Reports & Analytics</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+509
View File
@@ -0,0 +1,509 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 1: Dashboard & Overview - TIS-RMS User Guide</title>
<meta name="description" content="Real-time school performance metrics, role-scoped notifications, visual compliance analytics, and top students monitoring.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 5: Module 1: Dashboard & Overview -->
<section id="dashboard" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 1: Dashboard &amp; Overview</h2>
<p class="section-subtitle">Real-time school performance metrics, role-based toolbars, and learner compliance tracking.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg>
Dashboard Highlights
</div>
<ul class="module-summary-list">
<li><strong>Admin &amp; Teacher Toolbars:</strong> Global student search, role-scoped notifications, and a comprehensive user profile dropdown.</li>
<li><strong>Quick-Access Hubs:</strong> Jump directly to Students, Documents, Users, and core settings from prominent dashboard shortcut cards.</li>
<li><strong>Visual Compliance Analytics:</strong> Real-time charts for JHS/SHS digitalization progress, document breakdown by grade level, 30-day upload trends, and disk storage consumption.</li>
<li><strong>Actionable Student Monitoring:</strong> Instant rankings for top compliant learners alongside flags for students needing immediate document compliance attention.</li>
</ul>
</div>
<p class="paragraph">
The Dashboard serves as the central command center in TIS-RMS. It aggregates critical school data into an accessible, intuitive interface that automatically adapts to whether an Administrator or Classroom Teacher is currently signed in.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-dash">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-dash">Android</button>
</div>
<div class="mockup-pane active" id="win-dash">
<figure class="doc-figure" data-lightbox="assets/images/windows/dashboard.webp" data-caption="Windows Dashboard with Summary Cards and Grade Level Distribution">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/dashboard.webp" alt="Windows Dashboard" loading="lazy">
</div>
<figcaption>
<span>Figure 2A: Windows Dashboard &amp; Overview</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-dash" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/dashboard.webp" data-caption="Android Dashboard Overview">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/dashboard.webp" alt="Android Dashboard" loading="lazy">
</div>
<figcaption>
<span>Figure 2B: Android Dashboard &amp; Overview</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Admin &amp; Teacher Shared Header Tools</h3>
<p class="paragraph">
Both Administrator and Teacher roles have immediate access to three essential navigation utilities in the top header:
</p>
<div class="feature-grid">
<div class="feature-card">
<div class="card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</div>
<h4 class="card-title">Global Student Search</h4>
<p class="card-desc">Search for students across the entire school by first name, last name, or 12-digit LRN, and navigate directly to the Students screen.</p>
</div>
<div class="feature-card">
<div class="card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
</div>
<h4 class="card-title">Role-Based Notifications</h4>
<p class="card-desc">Administrators receive school-wide system notices, while teachers receive targeted notifications related specifically to their assigned advisory sections.</p>
</div>
<div class="feature-card">
<div class="card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="7" r="4"></circle><path d="M5.5 21h13a2 2 0 0 0 2-2v-2a7 7 0 0 0-14 0v2a2 2 0 0 0 2 2z"></path></svg>
</div>
<h4 class="card-title">Profile Dropdown</h4>
<p class="card-desc">Displays user full name, assigned role, registered email address, and phone number, with direct buttons to navigate to Settings or Log Out.</p>
</div>
</div>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_search.svg" data-caption="Global Student Search Bar">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_search.svg" alt="Global Student Search Bar" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Global Student Search Bar</span>
<p class='screenshot-sub'>Direct learner lookup by name or LRN with instant transition to the Students module</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_notifications.svg" data-caption="Role-Based Notification Feed">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_notifications.svg" alt="Role-Based Notification Feed" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Role-Based Notification Feed</span>
<p class='screenshot-sub'>Audited system notifications filtered automatically by administrative or teacher scope</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_profile.svg" data-caption="User Profile &amp; Session Dropdown">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_profile.svg" alt="User Profile &amp; Session Dropdown" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">User Profile &amp; Session Dropdown</span>
<p class='screenshot-sub'>Quick-view profile details, contact information, Settings shortcut, and secure sign-out</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Administrator Side: Dashboard Overview</h3>
<p class="paragraph">
The Administrator Dashboard provides comprehensive high-level control with quick navigation shortcuts and multi-dimensional analytics:
</p>
<ul class="bullet-list">
<li><strong>Quick Navigation:</strong> Direct one-click links to the <a href="settings.html#settings-academic">Academic &amp; Class Management</a> screen and the <a href="settings.html#settings-requirements">Document Requirements</a> screen.</li>
<li><strong>Quick-Access Action Cards:</strong> Fast navigation tiles to the <a href="students.html">Students</a>, <a href="documents.html">Documents</a>, and <a href="users.html">Users</a> modules.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_admin.svg" data-caption="Administrator Dashboard Overview">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_admin.svg" alt="Administrator Dashboard Overview" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Administrator Dashboard Overview</span>
<p class='screenshot-sub'>Comprehensive control center featuring quick navigation cards, class setup shortcuts, and status counters</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h4 class="subheading" style="margin-top: 24px;">Administrator Visual Graphs &amp; Analytics</h4>
<p class="paragraph">
Administrators can monitor institution-wide health through five rich visual graphs updated live from the server:
</p>
<ul class="bullet-list">
<li><strong>Digitalization Progress for JHS and SHS:</strong> Tracks the exact count of students who have completed all required documents, clearly separated into Junior High School (JHS) and Senior High School (SHS) cohorts.</li>
<li><strong>Overall Document Status:</strong> Displays the total count and status breakdown of uploaded files across the school.</li>
<li><strong>Document Breakdown by Grade Level:</strong> An interactive pie chart visualizing completed vs. submitted document requirements across Grade 7 through Grade 12.</li>
<li><strong>30-Day Upload Trend:</strong> A dynamic line graph showing document submission velocity over the past 30 days to identify submission spikes.</li>
<li><strong>Storage Analytics:</strong> Visualizes which document categories (SF9, SF10, PSA, etc.) consume the most server storage space.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_graphs.svg" data-caption="Visual Graphs &amp; KPI Analytics">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_graphs.svg" alt="Visual Graphs &amp; KPI Analytics" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Visual Graphs &amp; KPI Analytics</span>
<p class='screenshot-sub'>Interactive charts displaying JHS/SHS digitalization progress, grade distribution pie chart, upload trend line, and disk storage</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h4 class="subheading" style="margin-top: 24px;">Administrator Student Monitoring Lists</h4>
<div class="feature-grid">
<div class="feature-card">
<div class="card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
</div>
<h4 class="card-title">Top Students by Documents</h4>
<p class="card-desc">School-wide ranking of learners based on total uploaded documents, recognizing complete and compliant digital portfolios.</p>
</div>
<div class="feature-card">
<div class="card-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
</div>
<h4 class="card-title">Students Needing Attention</h4>
<p class="card-desc">Identifies students missing critical document requirements, allowing administrators to click directly and see which specific requirements are missing.</p>
</div>
</div>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_topstudents.svg" data-caption="Top Students by Documents Ranking">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_topstudents.svg" alt="Top Students by Documents Ranking" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Top Students by Documents Ranking</span>
<p class='screenshot-sub'>Leaderboard of learners with the highest submission counts across all grade levels</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_attention.svg" data-caption="Students Needing Attention &amp; Missing Documents Drilldown">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_attention.svg" alt="Students Needing Attention &amp; Missing Documents Drilldown" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Students Needing Attention &amp; Missing Documents Drilldown</span>
<p class='screenshot-sub'>Real-time list of learners with incomplete requirements, with one-click checklist inspection</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Teacher Side: Dashboard Overview</h3>
<p class="paragraph">
The Teacher Dashboard is streamlined to focus specifically on the teacher's assigned advisory classes:
</p>
<ul class="bullet-list">
<li><strong>Quick-Access Cards:</strong> Direct navigation buttons to the <a href="students.html">Students</a> and <a href="documents.html">Documents</a> modules scoped to their advisory classes.</li>
<li><strong>Top Students by Documents:</strong> Shows a ranking of students based on the number of uploaded documents strictly from their assigned advisory sections.</li>
<li><strong>Students Needing Attention:</strong> Highlights learners in their assigned sections with missing documents, allowing teachers to view exactly which requirements need to be submitted.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/dashboard_teacher.svg" data-caption="Teacher Advisory Section Dashboard">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/dashboard_teacher.svg" alt="Teacher Advisory Section Dashboard" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Teacher Advisory Section Dashboard</span>
<p class='screenshot-sub'>Tailored classroom dashboard with section switchers, advisory compliance metrics, and missing document flags</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="login.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Sign In & Getting Started</span>
</a>
<a href="students.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 2: Student Information</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+409
View File
@@ -0,0 +1,409 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 3: Documents & Student Folders - TIS-RMS User Guide</title>
<meta name="description" content="Virtual learner document folders, camera scanning on Android, dynamic checklists, PDF previews, and batch print list integration.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 7: Module 3: Documents & Student Folders -->
<section id="documents" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 3: Documents &amp; Student Folders</h2>
<p class="section-subtitle">Virtual learner document folders, camera scanning on Android, dynamic checklists, and built-in previews.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>
Documents Module Highlights
</div>
<ul class="module-summary-list">
<li><strong>Folder &amp; All Documents Views:</strong> Browse records organized into individual student folders or view all uploaded files in a searchable school-wide list.</li>
<li><strong>Smart Context-Aware Uploads:</strong> Uploading from inside a folder automatically tags the student's LRN. Outside a folder, the system clearly notes that the document is unassociated.</li>
<li><strong>Collapsible Requirement Checklist:</strong> View completed vs. pending documents right in the upload dialog to avoid missing files.</li>
<li><strong>Mobile Scanner &amp; File Preview:</strong> On Android, scan documents using the camera scanner. Preview PDFs and view images directly in the built-in viewer.</li>
<li><strong>Print List Integration:</strong> Add documents directly to the batch print list with one click.</li>
</ul>
</div>
<p class="paragraph">
The Documents module replaces physical filing cabinets with structured digital envelopes. Document requirements are synchronized live from the backend &mdash; visit the <a href="settings.html#settings-requirements">Document Requirements Guide</a> to learn how administrators configure requirements.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-doc">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-doc">Android</button>
</div>
<div class="mockup-pane active" id="win-doc">
<figure class="doc-figure" data-lightbox="assets/images/windows/documents.webp" data-caption="Windows Documents Explorer showing school year folders and student files">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/documents.webp" alt="Windows Documents Explorer" loading="lazy">
</div>
<figcaption>
<span>Figure 4A: Windows Documents Explorer &amp; Virtual Folder Hierarchy</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-doc" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/documents.webp" data-caption="Android Documents View with easy document preview">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/documents.webp" alt="Android Documents View" loading="lazy">
</div>
<figcaption>
<span>Figure 4B: Android Documents View &amp; Mobile Scanner</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Admin &amp; Teacher Side: Folder &amp; Document Navigation</h3>
<p class="paragraph">
Both Administrators and Teachers can search and navigate documents and student folders:
</p>
<ul class="bullet-list">
<li><strong>Search:</strong> Quickly search documents and virtual folders by <strong>student name</strong> or <strong>LRN</strong>.</li>
<li><strong>Folder Tab:</strong> Displays student digital folders with completion chips, and supports filtering by <strong>Grade Level</strong> and <strong>Section</strong>.</li>
<li><strong>All Documents Tab:</strong> Displays all uploaded documents across the school or advisory classes, with filtering by <strong>document requirements</strong> retrieved from the backend.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/documents_folders.svg" data-caption="Student Virtual Folders Grid &amp; Filtering">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/documents_folders.svg" alt="Student Virtual Folders Grid &amp; Filtering" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Student Virtual Folders Grid &amp; Filtering</span>
<p class='screenshot-sub'>Virtual document envelopes organized by Grade Level and Section with real-time completion chips</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/documents_all.svg" data-caption="All Documents Repository &amp; Category Filters">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/documents_all.svg" alt="All Documents Repository &amp; Category Filters" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">All Documents Repository &amp; Category Filters</span>
<p class='screenshot-sub'>Master list of uploaded files filterable by DepEd requirement types (SF9, SF10, PSA, etc.)</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Document Upload Workflow</h3>
<p class="paragraph">
Users can upload documents from files or via Android camera document scanner:
</p>
<ul class="bullet-list">
<li><strong>Context Detection:</strong> When uploading outside a student folder, the system shows a note indicating that the file is not currently associated with a student. When uploading from inside a folder, the system automatically detects and links the student's LRN.</li>
<li><strong>Collapsible Requirements List:</strong> The upload dialog features an interactive collapsible list displaying mandatory and completed documents.</li>
<li><strong>Platform Flexibility:</strong> On Android, staff can use the built-in document scanner with automatic border cropping and contrast enhancement. On both Windows and Android, files can be chosen directly from disk.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/documents_upload.svg" data-caption="Document Upload Modal &amp; Dynamic Checklist">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/documents_upload.svg" alt="Document Upload Modal &amp; Dynamic Checklist" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Document Upload Modal &amp; Dynamic Checklist</span>
<p class='screenshot-sub'>Upload dialog featuring collapsible requirement status, LRN auto-tagging, and camera scanner support</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">In-App Preview, Print Queue &amp; Actions</h3>
<ul class="bullet-list">
<li><strong>Document Preview:</strong> Displays thumbnails and built-in previews for supported PDF and image files without requiring external software.</li>
<li><strong>Add to Print List:</strong> Add selected documents directly to the batch print queue with a single click. For details on managing the print queue, visit the <a href="batchprint.html">Batch Print Records</a> module.</li>
<li><strong>Document Actions &amp; Multi-Select:</strong> Actions include preview, download, add to print list, archive, and delete. Multiple documents can be selected for batch operations.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/documents_preview.svg" data-caption="Built-In Document Preview &amp; File Viewer">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/documents_preview.svg" alt="Built-In Document Preview &amp; File Viewer" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Built-In Document Preview &amp; File Viewer</span>
<p class='screenshot-sub'>Native PDF reader and high-resolution image viewer with zoom, rotation, and print actions</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/documents_actions.svg" data-caption="Document Actions Menu &amp; Multi-Select Batch Operations">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/documents_actions.svg" alt="Document Actions Menu &amp; Multi-Select Batch Operations" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Document Actions Menu &amp; Multi-Select Batch Operations</span>
<p class='screenshot-sub'>Right-click or multi-select menu for batch downloading, printing, archiving, and deletion</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="students.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 2: Student Information</span>
</a>
<a href="archives.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 4: Student Archives</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+331
View File
@@ -0,0 +1,331 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frequently Asked Questions - TIS-RMS User Guide</title>
<meta name="description" content="Common questions and clear answers regarding offline mode, remote internet access, OCR scanning, permissions, and backups.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 15: Frequently Asked Questions -->
<section id="faq" class="doc-section">
<div class="section-header">
<h2 class="section-title">Frequently Asked Questions</h2>
<p class="section-subtitle">Answers to common questions about using TIS-RMS.</p>
</div>
<div class="faq-list">
<div class="faq-item">
<button class="faq-question">
<span>Can TIS-RMS be used without an internet connection?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>Yes, absolutely. TIS-RMS is engineered offline-first. It operates over your school's local area network (LAN/Wi-Fi). All records, scanned documents, and student folders are stored on your local school server without requiring an active internet connection.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Can teachers access TIS-RMS from home or outside the school network?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>Yes. TIS-RMS supports secure encrypted tunnel connections that allow authorized staff to securely access the system over the internet from outside the school campus when configured by the system administrator.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Does scanning an SF9 or SF10 with OCR automatically save the file?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>No. OCR (Optical Character Recognition) is strictly designed to extract printed text (such as Learner Reference Number, Full Name, Birth Date, and Sex) to autofill form fields and eliminate typing errors. To store the physical image or PDF in the student's virtual folder, use the document upload feature.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>Can teachers register new students?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>Yes. Teachers can register new students into their assigned advisory sections. Formal enrollment standing adjustments (such as graduating, transferring, or dropping students) remain restricted to Administrators to maintain official registrar standards.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>What happens when an administrator disables a document requirement?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>When a document requirement is disabled in School Settings, it is cleanly removed from active student checklists so it no longer penalizes compliance rates. Importantly, any documents previously uploaded under that requirement remain safely preserved in the database and are never deleted.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>How does the student pickup email notification work?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>In the Batch Print module, after printing certified documents, staff can enter a pickup date and office instructions in the pickup alert form. Clicking Send Notification transmits a formatted email to the student or guardian informing them that their physical documents are ready at the registrar's office.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>How does automatic archiving and graduation work?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>Automatic Archiving evaluates students with no active enrollment after the configured grace period and moves their records to the historical archive repository. Automatic Graduation evaluates Grade 10 completers and Grade 12 graduating candidates based on the active school year calendar, and can be triggered automatically or run manually by an Administrator.</p>
</div>
</div>
<div class="faq-item">
<button class="faq-question">
<span>How do I reset my password if I forget it?</span>
<svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>
</button>
<div class="faq-answer">
<p>Click "Forgot Password?" on the sign-in screen. Enter your registered email address. The system will email you a secure One-Time PIN (OTP). Enter the OTP on screen to verify your identity and immediately set a new password.</p>
</div>
</div>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="shortcuts.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Keyboard Shortcuts</span>
</a>
<a href="troubleshooting.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Help & Troubleshooting</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+366
View File
@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 8: Activity History - TIS-RMS User Guide</title>
<meta name="description" content="Complete system activity logs, user account lifecycle history, active login sessions, IP address tracking, and audit trail.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 12: Module 8: Activity History -->
<section id="history" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 8: Activity History</h2>
<p class="section-subtitle">Comprehensive system activity logs, account lifecycle audits, and active session monitoring.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
Activity History Highlights
</div>
<ul class="module-summary-list">
<li><strong>Complete Activity Logs:</strong> Records all system operations (student creation, updates, document uploads, archiving, and deletion) with timestamps and actor details.</li>
<li><strong>User Account History:</strong> Tracks the entire lifecycle of staff accounts, showing which administrator created, modified, activated, or deactivated an account.</li>
<li><strong>Login &amp; Session History:</strong> Monitors active user sessions with client platform, IP address, login timestamp, and session validity.</li>
<li><strong>Granular Search &amp; Date Range Filters:</strong> Filter logs by date range, action type, user name, and user role.</li>
<li><strong>Role-Based Access:</strong> Sessions and account history are restricted to Administrators; Teachers can review activity records within their permitted scope.</li>
</ul>
</div>
<p class="paragraph">
Module 8 ensures strict data accountability and compliance with government data privacy standards. Every transaction in TIS-RMS creates an immutable log entry that administrators can inspect at any time.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-his">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-his">Android</button>
</div>
<div class="mockup-pane active" id="win-his">
<figure class="doc-figure" data-lightbox="assets/images/windows/history.webp" data-caption="Windows Activity History with searchable event logs">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/history.webp" alt="Windows Activity History" loading="lazy">
</div>
<figcaption>
<span>Figure 8A: Windows System Activity Audit Trail</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-his" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/history.webp" data-caption="Android Activity History">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/history.webp" alt="Android Activity History" loading="lazy">
</div>
<figcaption>
<span>Figure 8B: Android Activity History</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">System Activity Logs</h3>
<p class="paragraph">
The Activity Logs tab provides a detailed timeline of actions taken across the system:
</p>
<ul class="bullet-list">
<li><strong>Logged Actions:</strong> Records student additions, edits, enrollment promotions, document uploads, file deletions, and archiving actions.</li>
<li><strong>Audit Metadata:</strong> Each log entry records the specific action performed, target record (learner name or LRN), acting staff member, and exact timestamp.</li>
<li><strong>Detail Inspector:</strong> Administrators can click any entry to open a modal detailing before-and-after values and payload data.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/history_activities.svg" data-caption="System Activity Audit Trail &amp; Detail Inspector">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/history_activities.svg" alt="System Activity Audit Trail &amp; Detail Inspector" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">System Activity Audit Trail &amp; Detail Inspector</span>
<p class='screenshot-sub'>Searchable activity ledger detailing user actions, timestamps, affected records, and inspect modal</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">User Account History &amp; Login Sessions</h3>
<ul class="bullet-list">
<li><strong>User Account History:</strong> Details account provisioning events including user creation, profile edits, activation/deactivation, and password resets, identifying both the target account and the acting administrator.</li>
<li><strong>Login &amp; Session History:</strong> Displays active and historical user sessions, including username, full name, platform (Windows or Android), client IP address, login timestamp, and session status.</li>
<li><strong>Search &amp; Pagination:</strong> Both views feature date range pickers, user filters, and responsive pagination to keep large logs manageable.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/history_users.svg" data-caption="User Account Lifecycle History">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/history_users.svg" alt="User Account Lifecycle History" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">User Account Lifecycle History</span>
<p class='screenshot-sub'>Account modification audit log showing provisioning events, role adjustments, and admin actors</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/history_sessions.svg" data-caption="Active Login Sessions &amp; IP Address Logs">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/history_sessions.svg" alt="Active Login Sessions &amp; IP Address Logs" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Active Login Sessions &amp; IP Address Logs</span>
<p class='screenshot-sub'>Session tracking table displaying client platforms, network IP addresses, login timestamps, and status</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="users.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 7: User Accounts</span>
</a>
<a href="settings.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 9: School Settings</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+269
View File
@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Overview - TIS-RMS User Guide</title>
<meta name="description" content="Official user guide and comprehensive handbook for Talisay Integrated School records management system on Windows and Android.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 2: Overview Tab -->
<section id="overview" class="hero-banner">
<span class="hero-tag">Official User Guide &amp; Technical Handbook</span>
<h1 class="hero-title">Talisay Integrated School - Records Management System</h1>
<p class="hero-subtitle">
A dedicated, offline-first records and document management system built for Talisay Integrated School. Manage learner records with official 12-digit LRN tracking, scan SF9 and SF10 forms with automated OCR text recognition, track dynamic JHS &amp; SHS compliance checklists, view DepEd statistical reports, and execute batch print requests &mdash; accessible on Windows desktop computers and Android mobile devices across the local school network or securely over the internet.
</p>
<div class="hero-badges">
<span class="badge-item">Windows Desktop</span>
<span class="badge-item">Android App</span>
<span class="badge-item">Works Offline &amp; Online</span>
<span class="badge-item">Real-Time Sync</span>
<span class="badge-item">SF9 &amp; SF10 OCR Scanning</span>
<span class="badge-item">DepEd Compliance Reports</span>
<span class="badge-item">Batch Print Queue</span>
<span class="badge-item">Dynamic Checklists</span>
</div>
<div class="module-summary" style="margin-top: 36px; text-align: left;">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
System Highlights &amp; Scope
</div>
<ul class="module-summary-list">
<li><strong>Cross-Platform Parity:</strong> Full-featured Windows desktop suite paired with an Android tablet app tailored for classroom mobility.</li>
<li><strong>Offline-First Resilience:</strong> Works 100% offline within the school local area network (LAN/Wi-Fi); zero dependence on cloud availability.</li>
<li><strong>Secure Remote Internet Access:</strong> Configurable encrypted internet tunnels allow teachers and administrators to work remotely from home.</li>
<li><strong>DepEd Standardization:</strong> Designed around Department of Education learner standards, Form 137 (SF10), Form 138 (SF9), and transparency boards.</li>
</ul>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="roles.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Staff Roles & Access</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+352
View File
@@ -0,0 +1,352 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sign In & Getting Started - TIS-RMS User Guide</title>
<meta name="description" content="Accessing TIS-RMS on Windows and Android via local network and secure internet tunnel, login credentials, and OTP password recovery.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 4: Sign In & Getting Started -->
<section id="login" class="doc-section">
<div class="section-header">
<h2 class="section-title">Sign In &amp; Getting Started</h2>
<p class="section-subtitle">How to access and sign into TIS-RMS on Windows desktop computers and Android mobile devices.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
Sign In &amp; Access Summary
</div>
<ul class="module-summary-list">
<li><strong>Cross-Platform Availability:</strong> TIS-RMS is officially available as a high-performance Windows desktop application and an intuitive Android mobile/tablet app.</li>
<li><strong>Local Network &amp; Remote Internet Access:</strong> The system operates seamlessly over the school's local area network (LAN/Wi-Fi) without requiring internet connectivity, and can also be accessed outside the local network through a secure connection that makes the system accessible over the internet.</li>
<li><strong>Secure Authentication:</strong> Sign in with your assigned username and password. User privileges and interface navigation adjust automatically based on your role.</li>
<li><strong>Self-Service Password Recovery:</strong> If you forget your password, click <strong>Forgot Password?</strong> to receive a one-time verification code (email OTP) to safely reset your credentials.</li>
</ul>
</div>
<p class="paragraph">
Whether you are working from a school desktop in the faculty room, on an administrative workstation in the registrar's office, or using an Android tablet inside the classroom, TIS-RMS provides a unified and responsive sign-in interface.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-login">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-login">Android</button>
</div>
<div class="mockup-pane active" id="win-login">
<figure class="doc-figure" data-lightbox="assets/images/windows/login.webp" data-caption="Windows Sign In Screen">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/login.webp" alt="Windows Sign In Screen" loading="lazy">
</div>
<figcaption>
<span>Figure 1A: Windows Sign In Screen</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-login" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/login.webp" data-caption="Android Sign In Screen">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/login.webp" alt="Android Sign In Screen" loading="lazy">
</div>
<figcaption>
<span>Figure 1B: Android Sign In Screen</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Sign In Interface Component</h3>
<p class="paragraph">
The sign-in interface features clean input fields, password visibility toggle, quick server status indication, and direct link to self-service password recovery:
</p>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/login_screen.svg" data-caption="TIS-RMS Cross-Platform Sign In Interface">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/login_screen.svg" alt="TIS-RMS Cross-Platform Sign In Interface" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">TIS-RMS Cross-Platform Sign In Interface</span>
<p class='screenshot-sub'>Unified login modal with password visibility toggle and OTP forgot password recovery</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Step-by-Step: Signing In</h3>
<div class="steps-container">
<div class="step-card">
<div class="step-number">1</div>
<div class="step-content">
<h4>Launch the Application</h4>
<p>Open TIS-RMS on your Windows workstation or Android mobile device. The application connects automatically to the local school server or your secure remote gateway.</p>
</div>
</div>
<div class="step-card">
<div class="step-number">2</div>
<div class="step-content">
<h4>Enter Your Credentials</h4>
<p>Type your registered <strong>Username</strong> and <strong>Password</strong>. If logging in for the first time, use the temporary credentials provided by your system administrator.</p>
</div>
</div>
<div class="step-card">
<div class="step-number">3</div>
<div class="step-content">
<h4>Click Sign In</h4>
<p>Click or tap the <strong>Sign In</strong> button. Upon successful verification, you will be redirected to the Dashboard populated with your role-specific metrics and tools.</p>
</div>
</div>
<div class="step-card">
<div class="step-number">4</div>
<div class="step-content">
<h4>Forgot Password (Self-Service Recovery)</h4>
<p>If you cannot remember your password, click <strong>Forgot Password?</strong> on the login screen. Enter your registered email address to receive a secure One-Time PIN (OTP) to reset your password without waiting for an administrator.</p>
</div>
</div>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="roles.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Staff Roles & Access</span>
</a>
<a href="dashboard.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 1: Dashboard & Overview</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0; url=index.html">
<title>Redirecting to System Overview...</title>
<script>window.location.href = "index.html";</script>
</head>
<body>
<p>Redirecting to <a href="index.html">System Overview</a>...</p>
</body>
</html>
+393
View File
@@ -0,0 +1,393 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 6: Reports & Analytics - TIS-RMS User Guide</title>
<meta name="description" content="DepEd transparency board, document compliance metrics, grade-level comparisons, server disk storage analytics, and Excel export.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 10: Module 6: Reports & Analytics -->
<section id="reports" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 6: Reports &amp; Analytics</h2>
<p class="section-subtitle">School-wide compliance monitoring, DepEd statistical tables, storage analytics, and Excel export.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg>
Reports Highlights (Administrator Access)
</div>
<ul class="module-summary-list">
<li><strong>Live Backend Refresh:</strong> All statistics, compliance counts, and percentages are fetched live from the backend database to reflect up-to-the-minute records.</li>
<li><strong>Document Compliance Breakdown:</strong> Identify non-compliant students and see exact counts of missing requirement types.</li>
<li><strong>Grade-Level &amp; Historical Comparisons:</strong> Compare compliance across grade levels and analyze trends between academic years.</li>
<li><strong>Storage Analytics:</strong> Monitor hard disk utilization by document categories to plan storage capacity.</li>
<li><strong>Excel (.xlsx) Export:</strong> Export formatted DepEd compliance spreadsheets with live previews.</li>
</ul>
</div>
<p class="paragraph">
Module 6 gives administrators a bird's-eye view of institutional performance, DepEd compliance metrics, and file system storage. All report data is synchronized in real time with server records.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-rep">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-rep">Android</button>
</div>
<div class="mockup-pane active" id="win-rep">
<figure class="doc-figure" data-lightbox="assets/images/windows/reports.webp" data-caption="Windows Reports showing compliance statistics and export">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/reports.webp" alt="Windows Reports" loading="lazy">
</div>
<figcaption>
<span>Figure 6A: Windows Reports &amp; DepEd Transparency Board</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-rep" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/reports.webp" data-caption="Android Reports View">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/reports.webp" alt="Android Reports" loading="lazy">
</div>
<figcaption>
<span>Figure 6B: Android Reports View</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Student Statistics &amp; Document Compliance</h3>
<p class="paragraph">
Administrators can monitor learner enrollment status and document completion across cohorts:
</p>
<ul class="bullet-list">
<li><strong>Student Statistics:</strong> Real-time counts for Active / Enrolled, Dropped, Transferred, and Graduated learners.</li>
<li><strong>Document Compliance:</strong> Displays overall compliance rates, flags students with missing requirements, and provides an itemized breakdown of missing documents by requirement type.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/reports_compliance.svg" data-caption="Document Compliance Dashboard &amp; Missing Requirements">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/reports_compliance.svg" alt="Document Compliance Dashboard &amp; Missing Requirements" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Document Compliance Dashboard &amp; Missing Requirements</span>
<p class='screenshot-sub'>Compliance metrics showing overall completion percentages and category-by-category missing counts</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Grade-Level Compliance, Trends &amp; Storage Analytics</h3>
<ul class="bullet-list">
<li><strong>Grade-Level Compliance:</strong> Pinpoints grade cohorts with higher numbers of incomplete portfolios.</li>
<li><strong>Document Status Analytics:</strong> Dynamic visual charts summarizing submission progress.</li>
<li><strong>Academic Year Comparison:</strong> Evaluates historical changes in student population and document completion across school years.</li>
<li><strong>Storage Analytics:</strong> Details disk storage consumption across categories (SF9, SF10, PSA, 137, etc.) to ensure server resources remain optimal.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/reports_analytics.svg" data-caption="Visual Analytics, Grade Compliance &amp; Storage Metrics">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/reports_analytics.svg" alt="Visual Analytics, Grade Compliance &amp; Storage Metrics" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Visual Analytics, Grade Compliance &amp; Storage Metrics</span>
<p class='screenshot-sub'>Interactive charts illustrating grade-level submission rates, multi-year comparisons, and disk space usage</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Student Compliance Table &amp; Excel Export</h3>
<p class="paragraph">
The compliance table allows granular inspection of individual learner statuses:
</p>
<ul class="bullet-list">
<li><strong>Search &amp; Sort:</strong> Search by name or LRN, and sort by missing document count to quickly isolate students needing intervention.</li>
<li><strong>Report Filters:</strong> Filter the table by academic year, grade level, section, or enrollment status.</li>
<li><strong>Excel Export (.xlsx):</strong> Click <strong>Export to Excel</strong> to generate a complete spreadsheet containing student statistics, missing document tallies, and full student compliance rosters. The system displays an interactive in-app preview before saving.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/reports_deped.svg" data-caption="DepEd Record Analytics &amp; Transparency Board">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/reports_deped.svg" alt="DepEd Record Analytics &amp; Transparency Board" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">DepEd Record Analytics &amp; Transparency Board</span>
<p class='screenshot-sub'>Searchable learner compliance table with grade, section, status, and missing document counts</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/reports_export.svg" data-caption="Excel Export Modal with Report Preview">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/reports_export.svg" alt="Excel Export Modal with Report Preview" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Excel Export Modal with Report Preview</span>
<p class='screenshot-sub'>In-app preview of generated DepEd compliance spreadsheet prior to downloading as an .xlsx file</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="batchprint.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 5: Batch Print Records</span>
</a>
<a href="users.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 7: User Accounts</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+358
View File
@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Staff Roles & Access - TIS-RMS User Guide</title>
<meta name="description" content="Detailed guide to Administrator and Teacher permissions, class scoping, and learner registration access in TIS-RMS.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 3: Staff Roles & Access -->
<section id="roles" class="doc-section">
<div class="section-header">
<h2 class="section-title">Staff Roles &amp; Access</h2>
<p class="section-subtitle">Dedicated access controls for School Administrators and Classroom Teachers.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Roles &amp; Permissions Overview
</div>
<ul class="module-summary-list">
<li><strong>Administrator (Registrar):</strong> Full school-wide oversight across all grade levels and sections. Registers new learners, edits all student fields, modifies formal enrollment standing, manages school settings and document requirements, creates staff accounts, audits all system logs, and exports DepEd reports.</li>
<li><strong>Teacher / Adviser:</strong> Focused access scoped to assigned advisory sections. Teachers can register new students, view assigned section rosters and virtual folders, edit permitted learner details (LRN, Name, Sex, Birth Date, 4Ps), upload scanned documents, and submit batch print requests.</li>
<li><strong>Data Accountability &amp; Security:</strong> The system enforces role-based access control (RBAC) across both client interfaces and the backend API, logging every modification with timestamps and user identifiers.</li>
</ul>
</div>
<p class="paragraph">
TIS-RMS separates responsibilities into two distinct user roles to ensure smooth daily operation while safeguarding the integrity of official learner records:
</p>
<div class="table-wrap">
<table class="doc-table">
<thead>
<tr>
<th>Feature / Action</th>
<th>Administrator</th>
<th>Teacher / Adviser</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>View Dashboard</strong></td>
<td><span class="badge success">School-Wide</span></td>
<td><span class="badge info">Assigned Sections</span></td>
<td>Teachers see counts and attention lists for their assigned advisory sections</td>
</tr>
<tr>
<td><strong>Register New Students</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge success">Allowed</span></td>
<td>Both roles can register new learners into the system (Manual, OCR, or CSV)</td>
</tr>
<tr>
<td><strong>Edit Student Details</strong></td>
<td><span class="badge success">Full Edit</span></td>
<td><span class="badge info">Permitted Fields</span></td>
<td>Teachers can edit LRN, Name, Sex, Birth Date, and 4Ps status; Admin can edit all fields</td>
</tr>
<tr>
<td><strong>Edit Student Status / Enrollment Standing</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge danger">Restricted</span></td>
<td>Admin updates formal standing (Enrolled, Transferred, Dropped, Graduated, Inactive)</td>
</tr>
<tr>
<td><strong>Upload Student Documents (SF9, PSA)</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge success">Allowed</span></td>
<td>Both roles can upload files directly into student virtual folders</td>
</tr>
<tr>
<td><strong>View Student Folders</strong></td>
<td><span class="badge success">All Sections</span></td>
<td><span class="badge info">Assigned Sections</span></td>
<td>Scoped by teacher's advisory class section assignments</td>
</tr>
<tr>
<td><strong>Archive &amp; Restore Records</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge danger">Restricted</span></td>
<td>Admin archives inactive students or restores archived files</td>
</tr>
<tr>
<td><strong>Batch Print Records &amp; Pickup Alerts</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge success">Allowed</span></td>
<td>Both roles can queue documents, print batches, and email pickup notifications</td>
</tr>
<tr>
<td><strong>Reports &amp; DepEd Transparency Board</strong></td>
<td><span class="badge success">Full Access</span></td>
<td><span class="badge danger">Restricted</span></td>
<td>Admin-only module for compliance metrics, storage usage, and Excel export</td>
</tr>
<tr>
<td><strong>User Account Management</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge danger">Restricted</span></td>
<td>Admin creates staff accounts, toggles activation, and resets passwords</td>
</tr>
<tr>
<td><strong>Activity History Auditing</strong></td>
<td><span class="badge success">Full (All Logs)</span></td>
<td><span class="badge info">Permitted Logs</span></td>
<td>Admin audits all logs and login sessions; Teachers view their permitted actions</td>
</tr>
<tr>
<td><strong>Manage Settings &amp; Requirements</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge danger">Restricted</span></td>
<td>Admin configures academic setup, document checklist items, and auto-archiving</td>
</tr>
<tr>
<td><strong>Manage Own Profile &amp; Password</strong></td>
<td><span class="badge success">Allowed</span></td>
<td><span class="badge success">Allowed</span></td>
<td>Both roles can update their personal profile info and change passwords</td>
</tr>
</tbody>
</table>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="index.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">System Overview</span>
</a>
<a href="login.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Sign In & Getting Started</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+478
View File
@@ -0,0 +1,478 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 9: School Settings - TIS-RMS User Guide</title>
<meta name="description" content="Academic year setup, grade levels, sections, advisers, dynamic document checklist rules, auto-archiving, and graduation engine.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 13: Module 9: School Settings & Document Checklist -->
<section id="settings" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 9: School Settings &amp; Document Checklist</h2>
<p class="section-subtitle">Academic setup, dynamic document checklists, automated reminders, archiving configuration, and graduation rules.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
School Settings Highlights
</div>
<ul class="module-summary-list">
<li><strong>Academic &amp; Class Management:</strong> Configure Academic Years, Grade Levels, Sections, and Class Adviser assignments that govern teacher permissions across the app.</li>
<li><strong>Dynamic Document Requirements:</strong> Add, edit, or toggle requirement definitions (JHS/SHS scope, mandatory flags, accepted file types, and due dates) synchronized live from the backend.</li>
<li><strong>Teacher Document Reminders:</strong> Configure automatic reminder thresholds to notify advisers about upcoming document deadlines.</li>
<li><strong>Automated Archiving &amp; Graduation:</strong> Establish inactive student archiving grace periods and run graduation checks for Grade 10 and Grade 12 cohorts.</li>
</ul>
</div>
<p class="paragraph">
Module 9 is the operational engine of TIS-RMS. Administrators configure academic structures, document rules, and automated maintenance workflows, while staff can manage their personal profiles.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-set">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-set">Android</button>
</div>
<div class="mockup-pane active" id="win-set">
<figure class="doc-figure" data-lightbox="assets/images/windows/settings.webp" data-caption="Windows Settings showing checklist and academic configuration">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/settings.webp" alt="Windows Settings" loading="lazy">
</div>
<figcaption>
<span>Figure 9A: Windows School Settings Hub</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-set" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/settings.webp" data-caption="Android Settings View">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/settings.webp" alt="Android Settings View" loading="lazy">
</div>
<figcaption>
<span>Figure 9B: Android Settings View</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<!-- Academic & Class Management with dedicated anchor id -->
<div id="settings-academic" style="padding-top: 16px;">
<h3 class="subheading">Academic &amp; Class Management</h3>
<p class="paragraph">
Administrators manage the school's foundational academic hierarchy:
</p>
<ul class="bullet-list">
<li><strong>Manage Structures:</strong> Create and modify <strong>Academic Years</strong>, <strong>Grade Levels</strong>, <strong>Sections</strong>, and <strong>Class Advisers / Teacher Assignments</strong>.</li>
<li><strong>Advisory Scope Control:</strong> Class adviser assignments dictate which sections teachers can access, view, and manage throughout the application.</li>
<li><strong>System Synchronization:</strong> Changes to academic years, grade levels, and sections are immediately reflected in enrollment forms, filters, and reports across the system.</li>
<li><strong>Graduation Support:</strong> Academic year setup underpins the automatic graduation evaluation process.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_academic.svg" data-caption="Academic &amp; Class Management Interface">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_academic.svg" alt="Academic &amp; Class Management Interface" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Academic &amp; Class Management Interface</span>
<p class='screenshot-sub'>Setup academic years, grade levels, sections, and assign classroom teachers as advisers</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
</div>
<!-- Document Requirements with dedicated anchor id -->
<div id="settings-requirements" style="padding-top: 16px;">
<h3 class="subheading">Document Requirements Management</h3>
<p class="paragraph">
Administrators configure the dynamic document checklist used throughout the system:
</p>
<ul class="bullet-list">
<li><strong>Add &amp; Edit Requirements:</strong> Create new document requirements or modify existing ones.</li>
<li><strong>Enable / Disable:</strong> Enabled requirements appear in active student checklists. Disabling a requirement removes it from active checklists without deleting previously uploaded documents.</li>
<li><strong>Mandatory Flag:</strong> Set whether a document is strictly required for DepEd compliance or optional.</li>
<li><strong>Level Assignment:</strong> Restrict requirements to <strong>JHS</strong> (Grade 7&ndash;10) or <strong>SHS</strong> (Grade 11&ndash;12).</li>
<li><strong>File Constraints:</strong> Define accepted file formats (PDF, JPG, PNG) and the maximum number of file uploads allowed.</li>
<li><strong>Due Dates:</strong> Assign official submission deadlines for automated compliance alerts.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_requirements.svg" data-caption="Document Checklist Configuration &amp; File Constraints">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_requirements.svg" alt="Document Checklist Configuration &amp; File Constraints" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Document Checklist Configuration &amp; File Constraints</span>
<p class='screenshot-sub'>Dynamic requirements editor with mandatory toggles, JHS/SHS scope, accepted formats, and due dates</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
</div>
<h3 class="subheading">Teacher Document Reminders</h3>
<p class="paragraph">
Administrators can configure automated email and system reminders for teachers:
</p>
<ul class="bullet-list">
<li><strong>Automated Reminder Toggle:</strong> Enable or disable automatic reminders for teachers.</li>
<li><strong>Threshold Days:</strong> Configure how many days before a document due date teachers should receive alerts.</li>
<li><strong>Section Targeting:</strong> Reminders notify teachers specifically about missing requirements in their assigned sections.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_reminders.svg" data-caption="Automated Due Date Reminder Settings">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_reminders.svg" alt="Automated Due Date Reminder Settings" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Automated Due Date Reminder Settings</span>
<p class='screenshot-sub'>Configure automated notification schedules to alert classroom teachers of approaching document deadlines</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<!-- Automatic Archiving with dedicated anchor id -->
<div id="settings-archiving" style="padding-top: 16px;">
<h3 class="subheading">Automatic Archiving Configuration</h3>
<p class="paragraph">
Administrators can automate student record archiving to keep active rosters clean:
</p>
<ul class="bullet-list">
<li><strong>Enable / Disable Engine:</strong> Toggle automated background archiving on or off.</li>
<li><strong>Grace Period Configuration:</strong> Define the number of grace days after an academic year closes before un-enrolled students are automatically archived.</li>
<li><strong>Academic Schedule Integration:</strong> The system leverages the active academic-year calendar to determine eligible records.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_archiving.svg" data-caption="Automatic Inactive Student Archiving &amp; Grace Period">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_archiving.svg" alt="Automatic Inactive Student Archiving &amp; Grace Period" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Automatic Inactive Student Archiving &amp; Grace Period</span>
<p class='screenshot-sub'>Configure automated archival schedules and grace periods for students without active enrollment</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
</div>
<h3 class="subheading">Automatic Graduation Engine</h3>
<p class="paragraph">
TIS-RMS supports an automated graduation process for completing cohorts:
</p>
<ul class="bullet-list">
<li><strong>Cohort Evaluation:</strong> Automatically evaluates eligible <strong>Grade 10</strong> (Completers) and <strong>Grade 12</strong> (Graduates) students based on the configured academic-year schedule.</li>
<li><strong>Manual Execution:</strong> Administrators can manually trigger the graduation check at any time. The system reports whether the process was executed or skipped based on schedule criteria.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_graduation.svg" data-caption="Automated Grade 10 &amp; 12 Graduation Evaluation">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_graduation.svg" alt="Automated Grade 10 &amp; 12 Graduation Evaluation" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Automated Grade 10 &amp; 12 Graduation Evaluation</span>
<p class='screenshot-sub'>Automated graduation processing interface with manual trigger button and execution status reports</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Account Settings, About &amp; Support</h3>
<ul class="bullet-list">
<li><strong>Account Settings:</strong> Users can update their first name, middle name, last name, extension, phone number, and email address, as well as change their login passwords.</li>
<li><strong>About, Support &amp; System Information:</strong> Displays official application release details, software version, and technical developer support information.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_account.svg" data-caption="User Profile &amp; Security Settings">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_account.svg" alt="User Profile &amp; Security Settings" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">User Profile &amp; Security Settings</span>
<p class='screenshot-sub'>Personal user profile editor with contact information updates and self-service password changes</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/settings_about.svg" data-caption="About TIS-RMS &amp; Technical Support Information">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/settings_about.svg" alt="About TIS-RMS &amp; Technical Support Information" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">About TIS-RMS &amp; Technical Support Information</span>
<p class='screenshot-sub'>System version details, school credits, and technical assistance contact channels</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<nav class="page-navigation" aria-label="Page navigation">
<a href="history.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 8: Activity History</span>
</a>
<a href="shortcuts.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Keyboard Shortcuts</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+312
View File
@@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Keyboard Shortcuts - TIS-RMS User Guide</title>
<meta name="description" content="Comprehensive hotkeys and touch gestures for Windows desktop and Android tablet workflows in TIS-RMS.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 14: Keyboard Shortcuts -->
<section id="shortcuts" class="doc-section">
<div class="section-header">
<h2 class="section-title">Keyboard Shortcuts</h2>
<p class="section-subtitle">Speed up your daily workflow with these desktop and tablet hotkeys.</p>
</div>
<div class="table-wrap">
<table class="doc-table">
<thead>
<tr>
<th>Shortcut</th>
<th>Platform</th>
<th>Action</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>F</kbd></td>
<td>Windows / Desktop</td>
<td>Global Search</td>
<td>Focuses the global learner search bar from any screen</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>U</kbd></td>
<td>Windows / Desktop</td>
<td>Upload Document</td>
<td>Opens the document upload dialog immediately</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>P</kbd></td>
<td>Windows / Desktop</td>
<td>Batch Print</td>
<td>Opens the batch print queue review dialog</td>
</tr>
<tr>
<td><kbd>Ctrl</kbd> + <kbd>N</kbd></td>
<td>Windows / Desktop</td>
<td>New Student</td>
<td>Opens the new student registration modal</td>
</tr>
<tr>
<td><kbd>Esc</kbd></td>
<td>Windows / Desktop</td>
<td>Close Modal / Viewer</td>
<td>Dismisses active dialogs, previews, or zoomed lightbox views</td>
</tr>
<tr>
<td><kbd>Right-Click</kbd></td>
<td>Windows / Desktop</td>
<td>Multi-Select / Context Menu</td>
<td>Selects records and opens batch operation menus</td>
</tr>
<tr>
<td><kbd>Long-Press</kbd></td>
<td>Android / Tablet</td>
<td>Multi-Select / Batch Mode</td>
<td>Enters multi-select mode for student records or documents</td>
</tr>
<tr>
<td><kbd>Pull-to-Refresh</kbd></td>
<td>Android / Tablet</td>
<td>Refresh Roster</td>
<td>Synchronizes active class list with the local server</td>
</tr>
</tbody>
</table>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="settings.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 9: School Settings</span>
</a>
<a href="faq.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Frequently Asked Questions</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+499
View File
@@ -0,0 +1,499 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 2: Student Information - TIS-RMS User Guide</title>
<meta name="description" content="Learner profiles, advanced multi-criteria filters, DepEd SF9/SF10 OCR autofill, bulk registration, and multi-select batch status updates.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 6: Module 2: Student Information -->
<section id="students" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 2: Student Information</h2>
<p class="section-subtitle">Comprehensive learner profiles, advanced multi-criteria filtering, SF9/SF10 OCR autofill, and bulk registration.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle></svg>
Student Information Highlights
</div>
<ul class="module-summary-list">
<li><strong>Flexible Search &amp; Multi-Filtering:</strong> Search by name, LRN, grade level, or section; filter dynamically by Grade Level, Section, 4Ps Beneficiary, Document Status, and pagination count.</li>
<li><strong>In-Depth Learner Profile:</strong> Displays complete demographics, enrollment timeline, JHS/SHS document checklist chips, submission ratio, and one-click folder navigation.</li>
<li><strong>OCR Accelerated Registration:</strong> Extract Name, LRN, Birth Date, and Sex automatically from DepEd SF9 and SF10 forms to speed up text-field entry.</li>
<li><strong>Bulk Registration (OCR &amp; CSV):</strong> Ingest full class cohorts at once using multiple SF9/SF10 documents or formatted CSV spreadsheets.</li>
<li><strong>Multi-Select Actions:</strong> Right-click on Windows or long-press on Android to select multiple learners for batch status updates.</li>
</ul>
</div>
<p class="paragraph">
The Student Information module is the central database of all learners at Talisay Integrated School. Each student is uniquely identified by their official 12-digit Learner Reference Number (LRN).
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-stud">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-stud">Android</button>
</div>
<div class="mockup-pane active" id="win-stud">
<figure class="doc-figure" data-lightbox="assets/images/windows/students.webp" data-caption="Student List with Quick Search and Action Menu">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/students.webp" alt="Windows Student List" loading="lazy">
</div>
<figcaption>
<span>Figure 3A: Windows Students List &amp; Roster Management</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-stud" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/students.webp" data-caption="Android Student List with Section Filters">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/students.webp" alt="Android Student List" loading="lazy">
</div>
<figcaption>
<span>Figure 3B: Android Students Roster View</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Admin &amp; Teacher Side: Search, Filtering &amp; Profile Inspection</h3>
<p class="paragraph">
Both Administrators and Teachers can search, filter, and inspect learner details within their permitted scopes:
</p>
<ul class="bullet-list">
<li><strong>Multi-Field Search:</strong> Instant live lookup by learner last name, first name, LRN, grade level, or section.</li>
<li><strong>Dynamic Filters:</strong>
<ul style="margin-top:6px;">
<li><strong>Grade Level:</strong> Displays grade levels populated dynamically from backend data.</li>
<li><strong>Section:</strong> Displays sections populated dynamically from backend data.</li>
<li><strong>4Ps Beneficiary:</strong> Filter learners by Yes or No status.</li>
<li><strong>Document Status / Attention:</strong> Filter by missing requirement severity:
<ul style="margin-top:4px;">
<li><em>Default:</em> Displays all students.</li>
<li><em>Complete:</em> Filters students with fewer or zero missing documents.</li>
<li><em>Pending:</em> Filters students with more missing documents needing follow-up.</li>
</ul>
</li>
<li><strong>Students per Page:</strong> Configure pagination size (e.g., 10, 25, 50, or 100 learners per page).</li>
</ul>
</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_list.svg" data-caption="Student Information List &amp; Live Search">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_list.svg" alt="Student Information List &amp; Live Search" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Student Information List &amp; Live Search</span>
<p class='screenshot-sub'>Paginated directory of learners with real-time keyword search and dynamic status badges</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_filter.svg" data-caption="Multi-Criteria Filtering Bar">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_filter.svg" alt="Multi-Criteria Filtering Bar" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Multi-Criteria Filtering Bar</span>
<p class='screenshot-sub'>Filter learners by Grade Level, Section, 4Ps Beneficiary, Document Status, and pagination count</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h4 class="subheading" style="margin-top: 24px;">Student Information View (Detail Inspector)</h4>
<p class="paragraph">
Clicking or tapping any student card or table row opens the comprehensive Student Information View:
</p>
<ul class="bullet-list">
<li><strong>Core Identity &amp; Demographics:</strong> Full Name, 12-digit LRN, Status (Enrolled, Graduated, Dropout, Transferee, or Inactive), Sex, Birth Date, and 4Ps Beneficiary designation.</li>
<li><strong>Enrollment History:</strong> Multi-year enrollment timeline indicating past school years, grade levels, and sections attended.</li>
<li><strong>Document Requirements Checklist:</strong> Clearly segregated into <strong>JHS Requirements</strong> and <strong>SHS Requirements</strong>.</li>
<li><strong>Document Status Ratio:</strong> Displays submitted count over total required count (e.g., "3/5 Submitted"). Clicking or hovering over the badge reveals a full breakdown of completed and missing requirements.</li>
<li><strong>Open Folder Button:</strong> Directly navigates into the student's virtual digital document repository.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_profile.svg" data-caption="Student Profile View &amp; Document Checklist">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_profile.svg" alt="Student Profile View &amp; Document Checklist" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Student Profile View &amp; Document Checklist</span>
<p class='screenshot-sub'>Complete learner card showing demographics, JHS/SHS checklist breakdown, status ratio, and Open Folder link</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Administrator Side: Registration, Editing &amp; Batch Tools</h3>
<p class="paragraph">
Administrators have full management authority over student registration, profile updates, and enrollment adjustments:
</p>
<ul class="bullet-list">
<li><strong>Add Students (Manual &amp; OCR):</strong> Administrators can enter learner details manually or utilize optical character recognition (OCR) from DepEd SF9 or SF10 documents to automatically extract and populate text fields.</li>
<li><strong>Bulk Student Registration:</strong> Register entire cohorts at once by scanning multiple SF9/SF10 documents from different students, or importing a standard CSV roster. For full instructions, visit the <a href="students.html#students-add-csv">Bulk Add Using OCR and CSV</a> guide below.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_add.svg" data-caption="Add Student Dialog with Manual &amp; OCR Options">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_add.svg" alt="Add Student Dialog with Manual &amp; OCR Options" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Add Student Dialog with Manual &amp; OCR Options</span>
<p class='screenshot-sub'>Register new learners manually or autofill text fields by scanning DepEd SF9 or SF10 forms</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<!-- Sub-section with specific anchor for Bulk Add using OCR and CSV -->
<div id="students-add-csv" style="padding-top: 16px;">
<h4 class="subheading">Bulk Add Using OCR and CSV</h4>
<p class="paragraph">
The Bulk Add feature dramatically accelerates student registration and section intake. Administrators can upload batches of SF9 and SF10 documents to extract multiple learner profiles simultaneously, or upload a formatted CSV spreadsheet containing names, LRNs, birth dates, sexes, and 4Ps designations. The system validates LRN format, checks for duplicates, and creates records in a single operation.
</p>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_add_csv.svg" data-caption="Bulk Student Registration via Multi-File OCR &amp; CSV Import">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_add_csv.svg" alt="Bulk Student Registration via Multi-File OCR &amp; CSV Import" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Bulk Student Registration via Multi-File OCR &amp; CSV Import</span>
<p class='screenshot-sub'>Mass-enrollment workflow supporting multi-file SF9/SF10 OCR extraction and CSV spreadsheet ingestion</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
</div>
<h4 class="subheading" style="margin-top: 24px;">Editing Student Details &amp; Enrollment Standing</h4>
<ul class="bullet-list">
<li><strong>Edit Student Details:</strong> Administrators can modify any learner attribute: LRN, Name, Sex, Status (Enrolled, Transferred, Dropped, Inactive), Birth Date, and 4Ps Beneficiary status.</li>
<li><strong>Edit Enrollment:</strong> Administrators can adjust academic year enrollment, scan uploaded documents associated with the student using OCR (SF9 and SF10 supported), or apply bulk enrollment updates via CSV.</li>
<li><strong>Multi-Select Actions:</strong> Long-press on Android tablets or right-click on Windows PCs to select multiple learners simultaneously. A floating action bar allows administrators to update enrollment statuses in bulk.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_edit.svg" data-caption="Edit Student Details Modal">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_edit.svg" alt="Edit Student Details Modal" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Edit Student Details Modal</span>
<p class='screenshot-sub'>Update learner demographics, active enrollment standing, and 4Ps beneficiary classification</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_enrollment.svg" data-caption="Edit Enrollment &amp; OCR Scan History">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_enrollment.svg" alt="Edit Enrollment &amp; OCR Scan History" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Edit Enrollment &amp; OCR Scan History</span>
<p class='screenshot-sub'>Modify academic year enrollment records and verify against OCR-scanned SF9/SF10 documents</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/students_multiselect.svg" data-caption="Multi-Select Student Actions Bar">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/students_multiselect.svg" alt="Multi-Select Student Actions Bar" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Multi-Select Student Actions Bar</span>
<p class='screenshot-sub'>Batch status modification for multiple selected learners via right-click or long-press</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Teacher Side: Student Management</h3>
<p class="paragraph">
Teachers have access to key student management tools for their assigned advisory sections:
</p>
<ul class="bullet-list">
<li><strong>Register New Students:</strong> Teachers can register new students entering their assigned advisory sections.</li>
<li><strong>Edit Permitted Details:</strong> Teachers can correct basic learner details including LRN, Full Name, Sex, Birth Date, and 4Ps Beneficiary status. Formal status modifications (such as Transferred, Dropped, or Graduated) remain restricted to Administrators.</li>
</ul>
<nav class="page-navigation" aria-label="Page navigation">
<a href="dashboard.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 1: Dashboard & Overview</span>
</a>
<a href="documents.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 3: Documents & Student Folders</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
File diff suppressed because it is too large. Load diff
+303
View File
@@ -0,0 +1,303 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help & Troubleshooting - TIS-RMS User Guide</title>
<meta name="description" content="Troubleshooting guides for local network server connections, Android camera scanning, password resets, and WebSocket sync.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 16: Help & Troubleshooting -->
<section id="troubleshooting" class="doc-section">
<div class="section-header">
<h2 class="section-title">Help &amp; Troubleshooting</h2>
<p class="section-subtitle">Solutions to common technical issues and connectivity checks.</p>
</div>
<div class="troubleshoot-grid">
<div class="troubleshoot-card">
<div class="troubleshoot-header">
<span class="troubleshoot-tag">Connectivity</span>
<h4>Cannot Connect to School Server</h4>
</div>
<p>If the app displays a "Server Offline" or "Network Error" banner upon launch:</p>
<ul>
<li>Confirm your computer or tablet is connected to the official school Wi-Fi or LAN cable.</li>
<li>Check that the host server computer is powered on and the server program is active.</li>
<li>If working from home, verify that your secure internet tunnel client is connected.</li>
</ul>
</div>
<div class="troubleshoot-card">
<div class="troubleshoot-header">
<span class="troubleshoot-tag">Document Scanner</span>
<h4>Camera Scanner Blurry or Not Cropping</h4>
</div>
<p>If document scanning on your Android tablet produces unreadable files:</p>
<ul>
<li>Place the DepEd form flat on a contrasting dark background.</li>
<li>Ensure adequate, uniform lighting without overhead shadows or glare.</li>
<li>Hold the tablet steady until the auto-shutter captures and crops the edges.</li>
</ul>
</div>
<div class="troubleshoot-card">
<div class="troubleshoot-header">
<span class="troubleshoot-tag">Authentication</span>
<h4>OTP Code Not Received</h4>
</div>
<p>If you requested a password recovery OTP but have not received it:</p>
<ul>
<li>Check your Spam or Junk email folder for the automated verification email.</li>
<li>Verify with the system administrator that your account is registered with the correct email address.</li>
<li>Administrators can also directly initiate a password reset link from the User Accounts module.</li>
</ul>
</div>
<div class="troubleshoot-card">
<div class="troubleshoot-header">
<span class="troubleshoot-tag">Sync &amp; Performance</span>
<h4>Changes Not Updating Instantly</h4>
</div>
<p>If numbers or uploads do not reflect immediately on your screen:</p>
<ul>
<li>Check the live connection indicator icon in the top header.</li>
<li>Press <kbd>F5</kbd> on Windows or pull-to-refresh on Android to resynchronize data with the backend.</li>
<li>Ensure no firewall software is blocking local WebSocket connections.</li>
</ul>
</div>
</div>
<nav class="page-navigation" aria-label="Page navigation">
<a href="faq.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Frequently Asked Questions</span>
</a>
<a href="index.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Back to Overview</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+366
View File
@@ -0,0 +1,366 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Module 7: User Accounts - TIS-RMS User Guide</title>
<meta name="description" content="Staff account provisioning for Administrators and Teachers, role-based section scoping, password reset links, and security controls.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="assets/logo.png">
</head>
<body id="top">
<!-- Top Sticky Header -->
<header class="doc-header">
<div class="header-left">
<button id="mobileMenuBtn" class="mobile-menu-btn" title="Open Navigation Menu" aria-label="Open Navigation Menu">
<svg class="hamburger-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
<svg class="close-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
<a href="index.html" class="header-brand">
<img src="assets/logo.png" alt="TIS-RMS Logo">
<div class="brand-text">
<h1>TIS-RMS Guide</h1>
<span>Talisay Integrated School</span>
</div>
</a>
</div>
<div class="header-search" id="headerSearchBox">
<div class="search-input-wrap">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
<input type="text" id="docSearchInput" class="search-input" placeholder="Search guide, modules, features..." autocomplete="off">
<span class="search-kbd">Ctrl+F</span>
</div>
</div>
<div class="header-actions">
<button id="mobileSearchBtn" class="mobile-search-btn" title="Search" aria-label="Search Documentation">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
</button>
<div class="platform-pill-toggle">
<button type="button" class="platform-btn active" data-platform-filter="all">All</button>
<button type="button" class="platform-btn" data-platform-filter="windows">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.901-1.799"/></svg>
<span class="platform-text">Windows</span>
</button>
<button type="button" class="platform-btn" data-platform-filter="android">
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><path d="M17.523 15.3414c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.551 0 .9993.4482.9993.9993.0001.5511-.4483.9997-.9993.9997m-11.046 0c-.5511 0-.9993-.4486-.9993-.9997s.4482-.9993.9993-.9993c.5511 0 .9993.4482.9993.9993 0 .5511-.4482.9997-.9993.9997m11.4045-6.02l1.9973-3.4592a.416.416 0 00-.1521-.5676.416.416 0 00-.5676.1521l-2.0223 3.503C15.5902 8.4116 13.8533 8.084 12 8.084s-3.5902.3276-5.1368.8657L4.8409 5.4467a.4161.4161 0 00-.5677-.1521.4157.4157 0 00-.1521.5676l1.9973 3.4592C2.6889 11.1867.3432 14.6589 0 18.761h24c-.3432-4.1021-2.6889-7.5743-6.1185-9.4396"/></svg>
<span class="platform-text">Android</span>
</button>
</div>
<button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Theme" aria-label="Toggle Theme">
<svg class="sun-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line></svg>
<svg class="moon-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</button>
<button id="printBtn" class="print-btn" title="Print Documentation" aria-label="Print Documentation">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg>
</button>
</div>
</header>
<!-- Sidebar Backdrop Overlay for Mobile -->
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
<!-- Main Layout -->
<div class="doc-layout">
<!-- Sticky Navigation Sidebar -->
<aside class="doc-sidebar" id="docSidebar">
<div class="sidebar-heading">Getting Started</div>
<ul class="sidebar-nav">
<li>
<a href="index.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg></span>
System Overview
<span class="nav-badge">Guide</span>
</a>
</li>
<li>
<a href="roles.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg></span>
Staff Roles &amp; Access
<span class="nav-badge">Roles</span>
</a>
</li>
<li>
<a href="login.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg></span>
Sign In &amp; Getting Started
<span class="nav-badge">Sign In</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Core Modules</div>
<ul class="sidebar-nav">
<li>
<a href="dashboard.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="5"></rect><rect x="14" y="12" width="7" height="9"></rect><rect x="3" y="16" width="7" height="5"></rect></svg></span>
Module 1: Dashboard
</a>
</li>
<li>
<a href="students.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg></span>
Module 2: Students
</a>
</li>
<li>
<a href="documents.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg></span>
Module 3: Documents
</a>
</li>
<li>
<a href="archives.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg></span>
Module 4: Archives
</a>
</li>
<li>
<a href="batchprint.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></span>
Module 5: Batch Print
</a>
</li>
<li>
<a href="reports.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg></span>
Module 6: Reports &amp; Analytics
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="users.html" class="nav-link active">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg></span>
Module 7: User Accounts
<span class="nav-badge">Admin</span>
</a>
</li>
<li>
<a href="history.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg></span>
Module 8: Activity History
</a>
</li>
<li>
<a href="settings.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span>
Module 9: School Settings
<span class="nav-badge">Admin</span>
</a>
</li>
</ul>
<div class="sidebar-heading">Reference &amp; Support</div>
<ul class="sidebar-nav">
<li>
<a href="shortcuts.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2" ry="2"></rect><line x1="6" y1="8" x2="6" y2="8"></line><line x1="10" y1="8" x2="10" y2="8"></line><line x1="14" y1="8" x2="14" y2="8"></line><line x1="18" y1="8" x2="18" y2="8"></line><line x1="6" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="18" y2="12"></line><line x1="6" y1="16" x2="18" y2="16"></line></svg></span>
Keyboard Shortcuts
</a>
</li>
<li>
<a href="faq.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></span>
Frequently Asked Questions
<span class="nav-badge">FAQs</span>
</a>
</li>
<li>
<a href="troubleshooting.html" class="nav-link">
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg></span>
Help &amp; Troubleshooting
</a>
</li>
</ul>
</aside>
<!-- Main Content Body -->
<main class="doc-main">
<!-- Section 11: Module 7: User Accounts -->
<section id="users" class="doc-section">
<div class="section-header">
<h2 class="section-title">Module 7: User Accounts</h2>
<p class="section-subtitle">Staff account provisioning, role assignments, security controls, and password resets.</p>
</div>
<div class="module-summary">
<div class="module-summary-label">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
User Management Highlights (Administrator Access)
</div>
<ul class="module-summary-list">
<li><strong>Staff Role Provisioning:</strong> Administrators can create accounts for Administrators and Teachers with automatic temporary credential generation.</li>
<li><strong>Search &amp; Status Filtering:</strong> Filter staff accounts by role (Admin vs. Teacher) and account status (Active vs. Deactivated).</li>
<li><strong>Account Lifecycle Actions:</strong> Edit user details, toggle account activation, and trigger password reset links to registered email addresses.</li>
<li><strong>Scoped Teacher Permissions:</strong> Teachers access only their assigned advisory sections and can edit permitted student details, while formal enrollment standing remains locked to Administrators.</li>
</ul>
</div>
<p class="paragraph">
Module 7 allows administrators to control access to TIS-RMS. Administrators configure who can log in, which advisory classes teachers can manage, and maintain robust credential security across all workstations.
</p>
<!-- Platform Mockups -->
<div class="mockup-container">
<div class="mockup-tabs">
<button class="mockup-tab-btn active" data-tab-target="win-usr">Windows</button>
<button class="mockup-tab-btn" data-tab-target="and-usr">Android</button>
</div>
<div class="mockup-pane active" id="win-usr">
<figure class="doc-figure" data-lightbox="assets/images/windows/users.webp" data-caption="Windows User Management showing staff accounts and role badges">
<div class="doc-figure-img-wrap">
<img src="assets/images/windows/users.webp" alt="Windows User Management" loading="lazy">
</div>
<figcaption>
<span>Figure 7A: Windows Staff Accounts Directory</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
<div class="mockup-pane" id="and-usr" style="display:none;">
<figure class="doc-figure doc-figure-android" data-lightbox="assets/images/android/users.webp" data-caption="Android Users View">
<div class="doc-figure-img-wrap">
<img src="assets/images/android/users.webp" alt="Android Users View" loading="lazy">
</div>
<figcaption>
<span>Figure 7B: Android Users View</span>
<span class="zoom-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg> Click to enlarge</span>
</figcaption>
</figure>
</div>
</div>
<h3 class="subheading">Administrator Account Management Workflow</h3>
<p class="paragraph">
Administrators maintain full administrative control over all staff accounts:
</p>
<ul class="bullet-list">
<li><strong>Creating Accounts:</strong> Administrators can register new accounts for Administrators and Teachers, assigning them unique usernames, official school email addresses, and phone numbers.</li>
<li><strong>Editing Information:</strong> Modify user details, phone numbers, or correct email addresses when staff information changes.</li>
<li><strong>Status Controls:</strong> Instantly activate or deactivate accounts. Deactivating an account immediately blocks the user from logging in on any device.</li>
<li><strong>Password Resets:</strong> Administrators can send secure password reset links directly to a staff member's registered email address.</li>
</ul>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/users_list.svg" data-caption="Staff Accounts Directory &amp; Filter">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/users_list.svg" alt="Staff Accounts Directory &amp; Filter" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Staff Accounts Directory &amp; Filter</span>
<p class='screenshot-sub'>Master directory of school personnel with search, role badges, and status toggles</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/users_add.svg" data-caption="Create Staff Account Dialog">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/users_add.svg" alt="Create Staff Account Dialog" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Create Staff Account Dialog</span>
<p class='screenshot-sub'>Staff registration form for creating Administrator and Teacher credentials</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<figure class="doc-figure component-screenshot" data-lightbox="assets/images/screenshots/users_edit.svg" data-caption="Edit Account Details &amp; Password Reset">
<div class="doc-figure-img-wrap">
<img src="assets/images/screenshots/users_edit.svg" alt="Edit Account Details &amp; Password Reset" loading="lazy">
</div>
<figcaption>
<div>
<span class="screenshot-title">Edit Account Details &amp; Password Reset</span>
<p class='screenshot-sub'>Manage user credentials, trigger password resets, and adjust account active status</p>
</div>
<span class="zoom-hint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>
Click to enlarge
</span>
</figcaption>
</figure>
<h3 class="subheading">Teacher Access Scopes &amp; Account Security</h3>
<ul class="bullet-list">
<li><strong>Section Scope Enforcement:</strong> Teachers can access student records strictly associated with their assigned advisory sections. Advisory assignments are configured in <a href="settings.html#settings-academic">Academic &amp; Class Management</a>.</li>
<li><strong>Permitted Student Edits:</strong> Teachers can update allowed learner information (LRN, Name, Sex, Birth Date, and 4Ps) and register new learners for their advisory sections, but cannot alter formal enrollment standings or delete student profiles.</li>
<li><strong>Self-Service Recovery:</strong> All users can update their own personal profiles and recover lost passwords via email One-Time PIN (OTP) verification.</li>
<li><strong>Audit Trail:</strong> All account events (creation, profile updates, status changes, and password resets) are recorded in the system history.</li>
</ul>
<nav class="page-navigation" aria-label="Page navigation">
<a href="reports.html" class="page-nav-link prev">
<span class="page-nav-label">&larr; Previous</span>
<span class="page-nav-title">Module 6: Reports & Analytics</span>
</a>
<a href="history.html" class="page-nav-link next">
<span class="page-nav-label">Next &rarr;</span>
<span class="page-nav-title">Module 8: Activity History</span>
</a>
</nav>
</section>
<!-- Responsive Document Footer -->
<footer class="doc-footer">
<div class="footer-left">
<img src="assets/logo.png" alt="TIS-RMS Seal" width="32" height="32">
<div>
<p class="footer-title">Talisay Integrated School — Records Management System</p>
<span class="footer-sub">Official User Guide &amp; Technical Handbook &bull; Windows &amp; Android Editions</span>
</div>
</div>
<div class="footer-right">
<a href="index.html">Overview</a>
<a href="shortcuts.html">Shortcuts</a>
<a href="faq.html">FAQ</a>
<a href="troubleshooting.html">Diagnostics</a>
<a href="#top" class="back-to-top">Back to Top &uarr;</a>
</div>
</footer>
</main>
</div>
<!-- Lightbox Modal for Fullscreen Image Zoom -->
<div id="lightboxModal" class="lightbox-modal" role="dialog" aria-modal="true" aria-label="Image Zoom">
<div class="lightbox-content">
<button id="lightboxCloseBtn" class="lightbox-close" aria-label="Close Zoom">&times;</button>
<img id="lightboxImg" src="" alt="Enlarged screenshot view">
<div id="lightboxCaption" class="lightbox-caption"></div>
</div>
</div>
<!-- Toast Notification Container -->
<div id="toastContainer" class="toast-container" aria-live="polite"></div>
<!-- Interactive Documentation Controller -->
<script src="app.js"></script>
</body>
</html>
+385 -17
View File
@@ -669,27 +669,395 @@ Files backup/restore - mirror mode or zip -->
- **Collapsible requirement list**: make the document requirement list in the modal collapsible/expandable so it does not take up excessive vertical space on smaller screens.
- **Responsive layout**: ensure the requirement list and its items resize/adapt properly across different screen widths (no overflow, no truncated text). -->
<!-- # REPORTS SCREEN
- DepEd Transparency Board separate section widgets:
- clean minimal header banner with title, academic year filter, and PDF export (no clutter/info overload).
- separate standalone section widgets/cards: 1. Data on Enrollment, 2. Dropouts & Transferees, and 3. 4Ps Beneficiaries.
- each section card integrates its visual chart and detailed comparative table seamlessly without sub-tabs.
# ADD "UPDATE POP UP" AFTER SPLASH SCREEN
- detect version based on installed app vs latest tag version from GitHub repo https://github.com/ssbg04/TIS_RMS.
- `AppUpdateService` queries latest release via GitHub API, compares semver (`PackageInfo`), and resolves platform asset (.exe / .apk).
- `UpdateAvailableDialog` with installed vs latest version pill, platform asset tag, release notes, and download link.
- `SplashScreen` check with 4-second timeout and non-blocking background error handling. -->
<!-- # STUDENT SCREEN
- student profile detail modal, remove the edit button beside the delete -> add separate edit button for student details and enrollment tab
- delete button change into inactive
- edit student modal, status remove the inactive
- multi modal action, remove inactive
- in android app, the click icon on student icon add short delay not a instant enables multi select
# REPORT SCREEN
- DEPED TRANSPARENCY BOARD, add collapse information for each widgets add short description for what each widget is for
- use tab design simple -->
<!--
# DASHBOARD SCREEN
- dont count the super admin in count
# SETTINGS SCREEN
- change password modal, rename the label UPDATE PASSWORD into UPDATE
# USER SCREEN
- user profile details, edit action buttons label use one word, make the button size the same -->
<!-- # REPORTS SCREEN
- add horizontal scroll bar for tab of sections in deped transparency board
- add in compliance and analytics in student document compliance missing status add bottom sheet modal and dropdown icon for missing status
- compliance & analytics tab:
- student document compliance, make the filter into modal, add it into screen header in android
- students per year, make the filter into modal, list all academic years and its checklist
# Automatic archiving
- not detect enrolled based on time limit on start date, complete the requirements to still enrolled (ex. 30 days), can dynamically change, no enrollment after the due date (auto archive), status (not enrolled, inactive)
# Audit Trail/History Screen and Tab
- new screen and tab, move user history and recent activities from dashboard to here
# Theme
- default the theme to light mode, not based on device theme -->
<!-- # DASHBOARD SCREEN
- document breakdown fix the overflowed
- remove from the frontend user history and recent activities
# STUDENT SCREEN
- fix overflow on edit details button on android app
# DOCUMENTS AND ARCHIVES SCREEN
- fix that on grid view, only large folder icon and under it the name and the status chip and count of docs ex. (3)
# REPORTS SCREEN
- academic year filter on deped transparency board not readable
- compliance & analytics export label color is not theme responsive and reduce size
# SETTINGS SCREEN
- default the auto update enrollment turned off
- add input for automatic archiving grace period
- change password update label color not theme responsive
- the danger zone is transparent fix it -->
<!-- # STUDENT SCREEN
- add student button make the icon button more attractive
- edit student modal 'x' button from left move to the right
- student profile add little bit attrativeness on edit details and edit enrollment
# DOCUMENTS AND ARCHIVES SCREEN
- in grid view for folders, F:\SumbrerongBato\tis_rms_server\screenshots\folders.png check this screenshot check only folder icons copy that design the icon the label below it, then the two chip of JSH and SHS requirement are inside of the icon of folder
- fix the multi select menu that didnt match width same as the list widget check this image F:\SumbrerongBato\tis_rms_server\screenshots\Screenshot 2026-09-19 163043.png
# HISTORY SCREEN
- "F:\SumbrerongBato\tis_rms_server\screenshots\Screenshot 2026-09-19 164307.png" fix this windows app cant list the widgets because and fix too overflow and cant list the widgets in android app F:\SumbrerongBato\tis_rms_server\screenshots\history.png -->
<!-- - report screen -> compliance -> fix fliter button label is in half
- settings screen -> acadmeic year and auto graduation -> edit schedule date modal -> fix button labels is in half
# DOCUMENT SCREEN
- file preview screen -> open with is not working
# WINDOWS SIDE NAV BAR
- windows app -> side bar nav move the archive tab to overview group -->
<!-- # SETTINGS SCREEN
- self deactivation -> fix the _dependents.isEmpty is not true after submit DEACTIVATE type confirmation modal
# REPORTS SCREEN
- android app, hide the collapsable filter, keep the filter button and fix the filter button label is at half
# USER SCREEN
- email validation api myemailverifier for add/edit users before sending the all email transactions
- use the MEV_API_KEY in .env
- example curl:
curl "https://api.myemailverifier.com/api/validate_single.php?apikey=114d4e64cdc3c060568b7f466f4161b1&email=example@example.com"
- Example Response:
{
"Address":"support@myemailverifier.com",
"catch_all": "false",
"Status":"Valid",
"Disposable_Domain":"false",
"Role_Based":"true",
"Free_Domain":"false",
"Greylisted":"false",
"Diagnosis":"Mailbox Exists and Active"
}
# DOCUMENTS SCREEN
- android app: document preview modal -> edit not working, open and open with works
- remove the excel to pdf using backend -> remove in the backend the api and libre office
# HISTORY SCREEN
- make the header of body compact, use the same search icon and filter from other screen for this screen
- in sessions tab the time use 12H format
- users tab fix the time format to 12H format, card title the user that perform to add/edit/deactivate/activate move to the sub-sub-description use basic words, change the "performed by" into -> action by -->
<!-- # SETTINGS SCREEN -> SECURITY collapse widget
- make the security collapse into screen
# HISTORY SCREEN
- remove the header icon and title replace/move the tabs (activites, users, sessions)
# DASHBOARD
- make the layout bento grids for windows app for mobile use normal responsive grid ( columns) -->
<!-- # REPORT SCREEN
- make the layout bento grids for windows app for mobile use normal responsive grid -->
<!-- # STUDENT SCREEN
- edit/add student after success -> still has a loading at background under the tabs
# SETTINGS SCREEN
- move the automatic archiving grace period under the academic & auto graduation collapse -> rename the collapse use basic word -> remove picking academic year, make it automatic choose the active year, dont make it dropdown just choose or pick the active year -->
<!-- # ANDROID / DOCUMENT SCANNER
- reduce android app size: configure google mlkit document scanner to unbundled mode (Google Play Services) while keeping automatic edge detection and enhance
# PDF GENERATION & MANIPULATION
- remove PDF manipulation and generation from frontend, keep PDF viewing
- move PDF manipulation and generation to backend
- move the image assets from this frontend to backend needed for generation
- for PDF manipulation settings for merging pdf and image 0 padding 0 margins, and use short bond paper size -->
<!-- # CAPSTONE MEMBERS SCREEN
- optimize the assets image before using convert into webp, remove the old
- backdrops
- group pictures
- solo pictures
- add these emails for corresponding member
- elliojames01@gmail.com
- antonioclaramaris.21@gmail.com
- ermhardevera11@gmail.com
- chineepasigan@gmail.com
- crischarlesgarcia345@gmail.com
- keep empty for rhina (alibutod)
- add horizontal scrollable list of pictures, slide preview, only one then add cooldown to next pic in 2 seconds, add previous and next buttons, when the last pic next goes back to the first image
- use the group pictures for horizontal scrollable list
- use the solo for individual member profile picture -->
<!-- # STUDENT SCREEN
- add new student scan has no preview of uploaded/scanned document and scan another in android app
- add new student uploading/scanning loading hide the manual input button when loading
- add new student modal the x button from left move to the right -->
<!-- # SETTINGS SCREEN
- auto archiving grace period -> set cutoff date via date picker with calculated days helper preview instead of dynamic days input
# REPORTS SCREEN
- deped tab -> export pdf button per sections on windows app is not responsive
# DOCUMENTS SCREEN
- upload document default requirements section closed/collapsed (manually expandable/collapsible)
# SETTINGS SCREEN
- make the fade at top and bottom fade show when it has item under it if not dont show -->
<!-- # SETTINGS / TEACHER REMINDERS
- settings screen -> add new collapsible card "Remind Teachers" for document requirements due date:
- settings toggle to enable/disable automated email notifications to teachers before due date
- configurable days input/selector (default: 3 days before due date, can manually change)
- sends email notification to advisory teachers with list of missing students per section
- teachers & academic setup screen -> remove "Remind All Teachers" button -->
<!-- # CSV BULK IMPORT ENHANCEMENTS
- bulk student import (add bulk student modal / screen):
- add on-screen CSV format example & template guide (headers: LRN, First Name, Middle Name, Last Name, Extension, Sex, Birth Date, 4Ps)
- add live type / paste CSV input area with "Parse & Preview" to submit directly without needing a separate file
- validation table / live preview showing valid rows and detected errors before confirming bulk import
- academic & sections setup (teachers & academic setup screen):
- add bulk CSV import for academic structure:
- format: Academic Year, Grade Level, Section Name (e.g., `2024-2025, 10, Diamond`)
- add on-screen CSV format example / reference & sample rows
- add live type / paste CSV input area with instant parse, validation preview, and bulk submit -->
<!-- # ACADEMIC & CLASS MANAGEMENT
- make the list reponsive in academic tab for android app
# STUDENTS SCREEN -> ADD NEW STUDENT & BULK IMPORT (ANDROID & DESKTOP)
- UI / Entry Point & Android Accessibility:
- Expose "Bulk Import" on Android (currently restricted to Windows only in `students_screen.dart:1247`)
- Provide a responsive action choice on Android: "Add Single Student" (Stepper Form) vs "Bulk Student Import" (CSV/OCR)
- Ensure single student modal form is fully responsive on Android (handles virtual keyboard, scrollable views, full-width touch targets)
- Target Class / Enrollment Context (Required by Backend):
- Add section selector header: Academic Year, Grade Level, and Section dropdowns (auto-populates current active school year)
- All valid imported students will be enrolled directly into this target section (matching DepEd SF1 masterlist workflow)
- Dual Input Methods (Android-Friendly):
- Method 1: Live Type / Paste CSV area with "Paste from Clipboard" button (zero file-permission friction on mobile)
- Method 2: Compatible CSV file upload via `file_picker` with proper MIME types (`text/csv`, `text/plain`, `*/*`)
- CSV Specifications & Smart Normalization:
- Expected headers: `LRN, First Name, Middle Name, Last Name, Extension, Sex, Birth Date, 4Ps`
- On-screen quick guide & copyable sample template button (`Copy Sample CSV`)
- Auto-normalizations:
- Sex: accepts `M`, `F`, `Male`, `Female` (case-insensitive)
- Birth Date: flexible parsing (`YYYY-MM-DD`, `MM/DD/YYYY`, `MM-DD-YYYY`)
- 4Ps: accepts `Yes`/`No`, `Y`/`N`, `True`/`False`, `1`/`0`
- Names & Extension: auto-trims whitespace and uppercase formatting
- Live Validation & Error Preview:
- Client-side validation before sending to backend:
- LRN: exactly 12 digits, numeric only, check for duplicate LRNs within the batch
- First Name & Last Name: mandatory non-empty strings
- Birth Date: valid calendar date, cannot be in the future
- Live summary stats badge: `Total Parsed`, `Valid (Ready to Import)`, `Errors (Needs Attention)`
- Responsive Mobile Preview:
- On mobile Android: Display list of validation cards (green check for valid, red badge with exact error message for invalid)
- On desktop/tablet: Data table with inline row status indicators
- Action Controls:
- "Parse & Validate", "Clear", and "Import [X] Valid Students" (disabled if 0 valid records or section unselected)
- Post-import summary dialog showing successfully created count vs skipped duplicates
- Auto uploads OCR file scanned after adding -> rename the scanned file of what type is the document -->
<!-- # HISTORY SCREEN
- [DONE] history -> session one ip different account shows active even 1 current login account
- Backend fix in authController.js login(): when a new login occurs, any open sessions from the same IP address belonging to a different user are automatically stamped with logout_at. This prevents stale "ACTIVE NOW" entries in the Sessions history tab.
# USERS SCREEN
- [DONE] remove super admin account
- Frontend fix in users_screen.dart _filter(): added a guard to exclude any user whose role is not 'admin' or 'teacher' (e.g. legacy super_admin accounts) from all views. Backend already filters is_hidden users via WHERE COALESCE(u.is_hidden, 0) = 0.
- [DONE] remove the developer account
- Removed developer seeding in schema.js and replaced with auto-migration to delete any existing `developer` user record.
- Removed `is_hidden` protection guards blocking deletion/status changes in userController.js and authController.js.
- Removed developer exemption in frontend security_section.dart so danger zone card is consistently presented.
# STUDENT SCREEN
- [DONE] student screen tab current year/enrolled students, and all students
- Added _StudentViewTab enum and a segmented tab switcher control below the header.
- "Enrolled · <CurrentYear>" tab auto-applies status=Enrolled + active academic year filter.
- "All Students" tab clears those filters and shows all student records.
- Tab resets to Enrolled on each navigation away/back to Students screen.
# STUDENT SCREEN
- [DONE] loads accurate active year students
- Fixed backend studentController.js to use LEFT JOIN sections (prevents dropping enrolled students with unassigned sections).
- Added academicYearsListProvider listener in students_screen.dart so active academic year is reliably applied to the query as soon as provider data resolves.
- Added fallback await on academicYearsListProvider.future in _applyViewTab to avoid empty year range on initial load.
- [DONE] when searching navigates to all tab
- When typing or submitting a search, students_screen automatically switches view tab to "All Students" and clears specific status/year filters.
- Clicking the "Enrolled" tab automatically clears the search field and restores the active year enrolled filter.
- Also cleared view-tab filters when search is triggered from the mobile search dialog in android_bottom_nav_layout.dart. -->
<!-- # ANDROID APP
- [DONE] hamburger nav bar slide too much touch area on the left
- In `android_bottom_nav_layout.dart`, reduced `drawerEdgeDragWidth` from `MediaQuery.of(context).size.width * 0.45` down to a strict `28.0` edge touch area to prevent accidental drawer opening when interacting with screen content.
- [DONE] report screen in deped tab export pdf cannot download the generated pdf, because of permission denied in android app
- In `download_service.dart`, added `saveBytes()` with adaptive permission management for Android: requests `Permission.manageExternalStorage` on Android 11+ (API 30+) and standard storage permission on Android 10 and below.
- Added multi-tier storage fallback: if public `/Download/TIS_RMS` is restricted by Android OS or denied by the user, gracefully falls back to app-specific external download/document storage directories that never throw permission errors.
- In `AndroidManifest.xml`, added `android:requestLegacyExternalStorage="true"` to `<application>` for legacy storage support on Android 10 (API 29).
- Updated `transparency_board_section.dart` and `reports_screen.dart` to use `DownloadService.saveBytes()`.
# STUDENT SCREEN
- [DONE] remove the "Student Directory" and copy the design of top header tab on attached picture
- Removed "Students Directory" page title heading and unified the header into a floating card (`height: 52`, `margin: horizontal 16/24, vertical 8`, rounded 12 with border and subtle shadow).
- Left side contains segmented tab switcher (`Enrolled` & `All Students`), right side contains search button, filter button with badge, elevated `Add Student` primary CTA button, and bulk import button.
- [DONE] students tab make the filter not the same for enrolled and all, in the enrolled remove the status and school year, now in all tab remove the enrolled status
- Enrolled tab: Filter dialog hides both "School Year" and "Status" sections (locked to current active school year + Enrolled status). Resetting filters clears grade/section/4Ps/sorting/limit without unsetting the enrolled school year or status.
- All Students tab: Filter dialog displays "School Year" dropdown and "Status" filter chips with `'Enrolled'` removed (options: `'All Status'`, `'Graduated'`, `'Transferred'`, `'Dropped'`, `'Inactive'`).
- Added `studentViewTabProvider` and updated `studentActiveFilterCountProvider` so the filter badge count accurately counts active user overrides per tab.
# USERS SCREEN
- [DONE] use tabs for filter of all, admin, teacher, inactive -> copy the design on attached picture
- Converted the old role filter chips and scrollbar into unified segmented tabs (`All`, `Admin`, `Teacher`, `Inactive`) with matching active/inactive states.
- Placed inside a matching top header card (`height: 52`, rounded 12) with Search button and `Add User` CTA on the right.
# SETTINGS SCREEN
- [DONE] descriptions of the system client, contact developers -> settings screen support
- Upgraded the collapsible card in `settings_screen.dart` to "About, Support & System Information".
- Added Client/System description header for Talisay Integrated School Records Management System.
- Added Developer & Technical Support section for Dalubhasaan ng Lungsod ng San Pablo (DLSP) BSIT Capstone Team with lead developer email (`crischarlesgarcia345@gmail.com`).
- Added direct action CTAs: "Contact via Email" (mailto launcher / clipboard fallback), "Report an Issue" (GitHub issues link), "GitHub Repository" (project repository link), and "Meet the Team" (navigates to `CapstoneMembersScreen`).
- [DONE] make the about, support, system information in settings responsive
- System/Client Description Header now adapts using `LayoutBuilder` (<460px): renders icon + title in a row and full-width description below on mobile screens, avoiding horizontal squishing.
- System Information items (`_buildAboutInfoRow`) dynamically switch from a 2-column fixed-label layout to stacked label & selectable value on compact viewports (<460px), preventing text compression.
- Developer Contact container adapts on viewports <500px: stacks label and email with a clean indentation to prevent horizontal overflows. Action buttons seamlessly wrap with `Wrap`. -->
<!-- # SETTINGS SCREEN
- [DONE] android app cannot open links in about, support & information buttons cannot redirect to browser
- Added `<queries>` intent declarations for `https`, `http`, and `mailto` schemes to `AndroidManifest.xml` (required on Android 11+ / API 30+ for `url_launcher` visibility).
- Enhanced external link handling in `settings_screen.dart` with dual launch mode fallback (`LaunchMode.externalApplication` -> `LaunchMode.platformDefault`) and clipboard copy fallback with user feedback snackbars if no browser or email client responds.
# STUDENT SCREEN
- [DONE] in android list remove the bottom space
- Removed obsolete 76px bottom spacer on Android when `totalPages <= 1` in `students_screen.dart`.
- Adjusted Android card list bottom padding from 24px to 8px, eliminating the dead whitespace at the bottom of the list.
- [DONE] move the filter button into the header tabs
- Removed the filter button from the Android top AppBar in `android_bottom_nav_layout.dart`.
- Integrated the filter button into the header tabs row in `students_screen.dart` (alongside the view switcher tabs) with active filter badge and direct tap target.
# DOCUMENT AND ARCHIVE SCREEN
- [DONE] make the filter realtime from the backend
- Added `GET /api/documents/types` endpoint on backend (`documentController.getDocumentTypes` and `routes/documents.js`) returning distinct active document types and requirements categorized into JHS, SHS, and General in real time directly from the database.
- Enhanced `documentController.getAllDocuments` and `archiveController.getArchivedDocuments` to handle category prefixes, raw names, and clean names seamlessly when filtering.
- Added `realtimeDocumentTypesProvider` in Flutter and updated `studentFoldersProvider` to perform real-time backend search queries.
- Updated `documents_screen.dart` and `archives_screen.dart` PopupMenuButton to invalidate and pull fresh types on open, displaying real-time JHS, SHS, and Other document categories with instant reactive queries. -->
<!-- # STATIC WEBSITE
- [DONE] for a system documentation how to use/ guide
- Created dedicated standalone directory [documentation/](file:///f:/SumbrerongBato/tis_rms_server/documentation) completely decoupled from frontend and backend builds.
- Implemented high-aesthetic responsive user guide [documentation/index.html](file:///f:/SumbrerongBato/tis_rms_server/documentation/index.html) with modern typography (`Inter`), Emerald green (`#00B074`) & Slate dark/light theme, and offline-capable architecture.
- Structured modular sections: System Overview, RBAC Roles & Security Matrix, Interactive Virtual Tour, Modules 1–8 (Dashboard, Students & Multi-Select, Virtual Folders & Document Taxonomy, Retention & Real-time Archives, Batch Print Queue & Duplex Merging, DepEd SF10 Excel-to-PDF Engine, Users & Immutable RA 10173 Audit Trail, Settings & Mandatory Checklists), Keyboard Shortcuts, and Connection Diagnostics & FAQ.
- Integrated dual-platform mockup frames: Windows desktop titlebars and Android tablet/notch frames with live tab switcher.
- Sourced and centralized all real system assets under `documentation/assets/images/windows/`, `documentation/assets/images/android/`, `documentation/assets/images/features/`, and `documentation/assets/videos/`.
- Built interactive JavaScript engine [documentation/app.js](file:///f:/SumbrerongBato/tis_rms_server/documentation/app.js): interactive frame-by-frame virtual tour reel with HUD overlay and timeline progress, real-time client search with `Ctrl+K` shortcut, image zoom Lightbox modal, theme switcher with `localStorage` persistence, and scrollspy navigation.
- Verified in browser with subagent: zero console errors, smooth tab transitions, working zoom lightbox, live search, and dark mode toggle. -->
<!-- # USER SCREEN -> PASSWORD UPON CREATE AN USER [DONE]
- [x] dont make the password `username123` for new admin user, make it randomized
- Implemented 10-character cryptographically secure randomized password generator for new admin accounts in `userController.js`.
- Exposed and highlighted generated temporary password in user creation success dialog in `users_screen.dart`.
# STUDENT SCREEN -> add student using OCR [DONE]
- [x] dont upload the document that used for OCR in their folder
- Removed document auto-upload logic from `add_student_modal.dart` and `bulk_ocr_import_dialog.dart` after creating students.
# KEYBOARD SHORTCUTS [DONE]
- [x] Ctrl + N in student screen not working (wired to `_openModal()` in `students_screen.dart`)
- [x] Ctrl + U in document screen not working (wired to `UploadOcrModal.show(...)` in `documents_screen.dart`)
- [x] Ctrl + P in document and archive screen not working (wired to `PrintQueueModal.show(context)` in `documents_screen.dart` and `archives_screen.dart`)
- [x] remove Ctrl + B for backup (removed from documentation shortcuts table and FAQ)
- [x] remove Ctrl + A for filter (removed from documentation shortcuts table) -->
<!-- # HISTORY SCREEN [DONE]
- [x] PRINTED_HISTORY card has a overflowed for date and time
- Made card headers in `audit_trail_screen.dart` responsive using `Wrap` layout so badges, entity types, and timestamps never overflow on any screen size.
# Documentation website [DONE]
- [x] fix FAQs make sure its on the current system features (added Socket.IO real-time sync, port 18484, 10-char randomized temporary admin passwords, OCR data extraction separated from raw folder upload, server-side lossless WebP PDF previews)
- [x] connect the documentation based on the current system features (updated port 18484, real-time Socket.IO event broadcasts across LAN, and accurate keyboard shortcuts)
- [x] dont speficy the android is for tablet just android (replaced all references of "Android Tablet", "tablet terminals", and "touch tablet" with "Android" / "Android device")
- [x] redesign the entire documentation make it minimalist no extra design just make it normal documentation (removed fake window chrome titlebars/colored dots, removed simulated mobile phone bezels/notches, converted screenshots to standard figures with semantic figcaptions and zoom modals, streamlined typography and styling) -->
<!--
# DASHBOARD SCREEN
- [x] auto play the document breakdown on android dont pause (enabled continuous auto-rotation on all platforms including Android)
- [x] do not hide the set up required (removed banner dismissal action so Setup Required banner remains permanently visible to administrators, with minimize/expand preserved)
# REPORTS SCREEN
- [x] export of compliance is csv make it on excel (migrated compliance report export from CSV to native Excel .xlsx with Summary and Student Compliance List sheets via excel package)
- [x] fix the graph in android app add huge space for graphs to fix the hover cannot read (increased chart heights, expanded topTitles reserved headroom, and enabled fitInsideHorizontally/Vertically for tooltips across all report and DepEd charts)
- [x] make the horizontal bar the same design in deped tab (standardized custom styled horizontal scrollbar across Dropout/Transferee and 4Ps equity charts matching the rest of the DepEd tab)
- [x] make the export pdf of deped tab include all graphs and tables like on in individual pdf export (implemented 3-page comprehensive DepEd PDF report with vector bar charts and full comparative, gender, dropout, transferee, retention, and 4Ps tables across both full and individual exports)
- [x] deped tab add collapsable short description each sections, default is closed (added animated collapsible info guide pills for Enrollment, Dropouts/Transferees, and 4Ps Equity sections defaulting to closed) -->
<!-- # USERS SCREEN
- [x] add new user make the button the same size (standardized Cancel and Create User buttons in the Add User modal with identical height 48px, consistent 14px typography, and matching border radius) -->
<!-- # HISTORY SCREEN
- [x] add keyboard shortcut Ctrl + F for search (implemented CallbackShortcuts and FocusNode in AuditTrailScreen to invoke search dialog on Ctrl+F) -->
<!-- # STUDENT SCREEN
- [x] student profile modal enrollment tab, (Admin Only: Administrators can add and edit enrollments, with support for CSV bulk enrollment for 1 student to enroll multiple records across school years with live progression verification with backend. Features interactive editable CSV text area with sample format reference, Fill Example, and real-time backend verification)
- Admin Only: Administrators can add and edit enrollments, with support for CSV bulk enrollment and live verification with the backend.
- csv input add sample format then they can type on it -->
# STUDENT SCREEN
- [x] csv bulk enrollment make the UI responsive for android app
# SETTINGS SCREEN
- [x] make the download update downloads in app doesnt redirect to others apps github or chrome, and ask to download via default installer of android, for windows download it then open the installer and close the windows app when the installer is open
- check all the screens and
---
# DONT DO
<!-- - documents screen -> add how many files can be uploaded into a document requirement, the current limitation is only 1, so we can dynamically choose number files that can be uploaded into that document, update both frontend and backend, but first give me suggestion where will put the setting of this, ask me first -->
- CSV bulk add student, academic and section [acad year, grade level, sections]
- print list history, print email to the student make it optional, add date of print, add status (done, not done, in progress, picked up), add message to email can be manual message typed by registrar/staff,
- teacher no assigned section theme color
- bug in bulk add student enrollment
- recycle bin -> search history not delete history
- add missing students by document type kpi
- notification separate admin and teacher
- bug in bulk add student enrollment
- report all filter and export
---
# TIS_RMS Server Manager (dont do)
+21 -7
View File
@@ -4,25 +4,39 @@ This guide provides instructions for installing, building, and running the **TIS
---
## 1. Android Installation (Universal APK)
## 1. Android Installation (Split & Universal APKs)
The Android application is distributed as a single universal APK supporting both **32-bit (armeabi-v7a)** and **64-bit (arm64-v8a, x86_64)** devices.
The Android application is distributed in architecture-specific split APKs (for smaller download size and faster performance) as well as a universal APK:
### Universal APK:
### Available APKs:
| APK File | Architecture | Description |
| :--- | :--- | :--- |
| **`TIS_RMS_Android_Universal_v<version>.apk`** | **Universal (32-bit & 64-bit)** | Single standalone installer compatible with all modern and legacy Android devices (Android 7.0+). |
| **`TIS_RMS_64_v<version>.apk`** | **64-bit ARM (`arm64-v8a`)** | **Recommended.** Best performance and smallest size (~40% smaller) for modern Android phones and tablets. |
| **`TIS_RMS_32bit_v<version>.apk`** | **32-bit ARM (`armeabi-v7a`)** | Optimized for older or entry-level 32-bit Android devices. |
| **`TIS_RMS_Universal_v<version>.apk`** | **Universal (32-bit & 64-bit)** | Single standalone installer containing all native architectures (runs on all Android 7.0+ devices). |
### Build Command (Flutter):
### Build Commands (Flutter):
#### A. Build Split APKs (Generates 32-bit & 64-bit standalone APKs):
```bash
cd frontend
flutter build apk --release --split-per-abi --obfuscate --split-debug-info=build/app/outputs/symbols
```
*Outputs located in `frontend/build/app/outputs/flutter-apk/`:*
- `app-arm64-v8a-release.apk` &rarr; **`TIS_RMS_64_v<version>.apk`**
- `app-armeabi-v7a-release.apk` &rarr; **`TIS_RMS_32bit_v<version>.apk`**
#### B. Build Universal APK (Contains all architectures):
```bash
cd frontend
flutter build apk --release --obfuscate --split-debug-info=build/app/outputs/symbols
```
*Output APK located in `frontend/build/app/outputs/flutter-apk/app-release.apk`.*
*Output located in `frontend/build/app/outputs/flutter-apk/`:*
- `app-release.apk` &rarr; **`TIS_RMS_Universal_v<version>.apk`**
### Installation Steps:
1. Transfer `TIS_RMS_Android_Universal_v<version>.apk` (e.g. `TIS_RMS_Android_Universal_v1.0.0.apk`) to your Android device (via USB, local network share, or download).
1. Transfer the matching APK (`TIS_RMS_64_v<version>.apk` for modern devices, `TIS_RMS_32bit_v<version>.apk` for older devices, or `TIS_RMS_Universal_v<version>.apk`) to your Android device (via USB, local network share, or download).
2. Tap the `.apk` file in your file manager.
3. If prompted, enable **"Install unknown apps"** or **"Allow from this source"** in Android Settings.
4. Tap **Install** and open the app.
+12
View File
@@ -38,6 +38,9 @@ android {
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
// Strip unused language strings from transitive third-party libraries (saves ~2-4 MB)
resourceConfigurations += listOf("en")
}
signingConfigs {
@@ -51,6 +54,15 @@ android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "NONE"
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
+32
View File
@@ -0,0 +1,32 @@
# Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Prevent obfuscation warnings for Flutter embedding
-dontwarn io.flutter.embedding.**
# Firebase
-dontwarn com.google.firebase.**
-keep class com.google.firebase.** { *; }
# Google ML Kit Document Scanner & Vision
-keep class com.google.mlkit.** { *; }
-dontwarn com.google.mlkit.**
-keep class com.google.android.gms.tasks.** { *; }
# Preserve Annotations & Reflection Metadata
-keepattributes *Annotation*,EnclosingMethod,Signature,InnerClasses
# Preserve JNI Native Methods
-keepclasseswithmembernames class * {
native <methods>;
}
# Preserve Flutter Services and Receivers
-keep class * implements io.flutter.plugin.common.PluginRegistry$PluginRegistrantCallback { *; }
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
@@ -19,11 +19,16 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:label="TIS RMS"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="document_scanner" />
<meta-data
android:name="com.pravera.flutter_foreground_task.notification_icon"
android:resource="@mipmap/ic_launcher" />
@@ -89,5 +94,27 @@
<data
android:mimeType="text/plain"/>
</intent>
<!-- Required for url_launcher on Android 11+ (API 30+) -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
<!-- Required for installing APK updates on Android 11+ (API 30+) -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent>
<intent>
<action android:name="android.intent.action.INSTALL_PACKAGE" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent>
</queries>
</manifest>
@@ -82,7 +82,15 @@ class AlarmReceiver : BroadcastReceiver() {
}
var newHighestId = highestOldId
ensureChannel(context)
val soundEnabled = prefs.getBoolean("flutter.pref_sound_enabled", false)
val vibrationEnabled = prefs.getBoolean("flutter.pref_vibration_enabled", false)
val channelId = when {
soundEnabled && vibrationEnabled -> "tis_rms_activities_sound_vibrate"
soundEnabled -> "tis_rms_activities_sound_only"
vibrationEnabled -> "tis_rms_activities_vibrate_only"
else -> "tis_rms_activities_silent"
}
ensureChannel(context, channelId, soundEnabled, vibrationEnabled)
val notifManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
for (i in 0 until list.length()) {
@@ -107,7 +115,7 @@ class AlarmReceiver : BroadcastReceiver() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = Notification.Builder(context, CHANNEL_ID)
val notification = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(title)
.setContentText(message)
@@ -126,16 +134,26 @@ class AlarmReceiver : BroadcastReceiver() {
}
}
private fun ensureChannel(context: Context) {
private fun ensureChannel(context: Context, channelId: String, soundEnabled: Boolean, vibrationEnabled: Boolean) {
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
if (manager.getNotificationChannel(channelId) != null) return
val channelName = when (channelId) {
"tis_rms_activities_sound_vibrate" -> "Recent Activities (Sound & Vibrate)"
"tis_rms_activities_sound_only" -> "Recent Activities (Sound Only)"
"tis_rms_activities_vibrate_only" -> "Recent Activities (Vibrate Only)"
else -> "Recent Activities (Silent)"
}
val importance = if (soundEnabled || vibrationEnabled) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(
CHANNEL_ID,
"Recent Activities",
NotificationManager.IMPORTANCE_HIGH
channelId,
channelName,
importance
).apply {
description = "Notifications for recent activities and system events"
enableVibration(true)
enableVibration(vibrationEnabled)
if (!soundEnabled) {
setSound(null, null)
}
}
manager.createNotificationChannel(channel)
}
@@ -16,16 +16,57 @@ class MainActivity : FlutterActivity() {
private fun ensureNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "tis_rms_activities_channel"
val channelName = "Recent Activities"
val channelDescription = "Notifications for recent activities and system events"
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(channelId, channelName, importance).apply {
description = channelDescription
enableVibration(true)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
val channels = listOf(
NotificationChannel(
"tis_rms_activities_sound_vibrate",
"Recent Activities (Sound & Vibrate)",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications with sound and vibration"
enableVibration(true)
},
NotificationChannel(
"tis_rms_activities_sound_only",
"Recent Activities (Sound Only)",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications with sound only"
enableVibration(false)
},
NotificationChannel(
"tis_rms_activities_vibrate_only",
"Recent Activities (Vibrate Only)",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications with vibration only"
enableVibration(true)
setSound(null, null)
},
NotificationChannel(
"tis_rms_activities_silent",
"Recent Activities (Silent)",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Silent notifications without sound or vibration"
enableVibration(false)
setSound(null, null)
},
NotificationChannel(
"tis_rms_activities_channel",
"Recent Activities",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Notifications for recent activities and system events"
enableVibration(false)
setSound(null, null)
}
)
for (ch in channels) {
notificationManager.createNotificationChannel(ch)
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Loaded 100 of 205 files, more files were not shown because too many files have changed in this diff. Show more