Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74e708661a | ||
|
|
8799d58280 | ||
|
|
87c4aff693 | ||
|
|
848b3b133b | ||
|
|
939ff3e0fb | ||
|
|
e947e5ccb0 | ||
|
|
0c4c7e4972 | ||
|
|
be10ed43de | ||
|
|
b5402f34b5 | ||
|
|
d126ecaef1 | ||
|
|
a8473c23b7 | ||
|
|
0058decaef | ||
|
|
3e71448add | ||
|
|
d4d80bf9e6 | ||
|
|
35f56f925a | ||
|
|
e6e8188312 | ||
|
|
3a3e0dc7b8 | ||
|
|
a74e0dc7fe | ||
|
|
79f4504694 | ||
|
|
b04981d7fb | ||
|
|
f30faeecff | ||
|
|
5952de21b2 | ||
|
|
bcfa1a0919 | ||
|
|
3e2a52770c | ||
|
|
4f20a67fda | ||
|
|
c085154b8a | ||
|
|
e533b60769 | ||
|
|
fca3a1f9d7 | ||
|
|
18abb9a6d8 | ||
|
|
d75d441441 | ||
|
|
6f95fd265b | ||
|
|
09c0e600e9 | ||
|
|
c6d5607811 | ||
|
|
8fcd875f38 | ||
|
|
3a1f872f54 | ||
|
|
30e260a357 | ||
|
|
58bf10b4ec | ||
|
|
bd2a8acbfe | ||
|
|
a9c2ab0da5 | ||
|
|
13bbdf9e26 | ||
|
|
32cff2431a | ||
|
|
ae5246f271 | ||
|
|
42d9a60805 | ||
|
|
ecfb98ac86 | ||
|
|
7faade70a9 | ||
|
|
2376e99217 | ||
|
|
99be1e8d79 | ||
|
|
71526fe61e | ||
|
|
13abad6563 | ||
|
|
689dbc48e7 | ||
|
|
1a20994749 | ||
|
|
e2f10f6260 | ||
|
|
69b4879ac2 | ||
|
|
2dc8ee4242 | ||
|
|
59e94192aa | ||
|
|
48fcf181af | ||
|
|
793bd340f1 | ||
|
|
029629c349 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/>.
|
||||
@@ -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/>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
[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 & 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.
|
||||
|
After Width: | Height: | Size: 36 KiB |
@@ -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');
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 ')})`);
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
// ==========================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 188 KiB |
@@ -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' });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Files">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/archives_folder.svg" alt="Archived Student Virtual Folder & Files" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Archived Student Virtual Folder & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 188 KiB |
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Document Review">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/print_queue.svg" alt="Batch Print Queue Modal & Document Review" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Batch Print Queue Modal & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Session Dropdown">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/dashboard_profile.svg" alt="User Profile & Session Dropdown" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">User Profile & 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 & 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 & 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 & KPI Analytics">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/dashboard_graphs.svg" alt="Visual Graphs & KPI Analytics" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Visual Graphs & 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 & Missing Documents Drilldown">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/dashboard_attention.svg" alt="Students Needing Attention & Missing Documents Drilldown" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Students Needing Attention & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 — 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 & 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 & 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 & Teacher Side: Folder & 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 & Filtering">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/documents_folders.svg" alt="Student Virtual Folders Grid & Filtering" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Student Virtual Folders Grid & 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 & Category Filters">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/documents_all.svg" alt="All Documents Repository & Category Filters" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">All Documents Repository & 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 & Dynamic Checklist">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/documents_upload.svg" alt="Document Upload Modal & Dynamic Checklist" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Document Upload Modal & 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 & 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 & 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 & File Viewer">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/documents_preview.svg" alt="Built-In Document Preview & File Viewer" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Built-In Document Preview & 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 & Multi-Select Batch Operations">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/documents_actions.svg" alt="Document Actions Menu & Multi-Select Batch Operations" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Document Actions Menu & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & Session History:</strong> Monitors active user sessions with client platform, IP address, login timestamp, and session validity.</li>
|
||||
<li><strong>Granular Search & 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 & Detail Inspector">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/history_activities.svg" alt="System Activity Audit Trail & Detail Inspector" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">System Activity Audit Trail & 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 & 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 & 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 & 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 & IP Address Logs">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/history_sessions.svg" alt="Active Login Sessions & IP Address Logs" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Active Login Sessions & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & SHS compliance checklists, view DepEd statistical reports, and execute batch print requests — 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 & Online</span>
|
||||
<span class="badge-item">Real-Time Sync</span>
|
||||
<span class="badge-item">SF9 & 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 & 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Missing Requirements">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/reports_compliance.svg" alt="Document Compliance Dashboard & Missing Requirements" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Document Compliance Dashboard & 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 & 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 & Storage Metrics">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/reports_analytics.svg" alt="Visual Analytics, Grade Compliance & Storage Metrics" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Visual Analytics, Grade Compliance & 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 & Excel Export</h3>
|
||||
<p class="paragraph">
|
||||
The compliance table allows granular inspection of individual learner statuses:
|
||||
</p>
|
||||
<ul class="bullet-list">
|
||||
<li><strong>Search & 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 & Transparency Board">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/reports_deped.svg" alt="DepEd Record Analytics & Transparency Board" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">DepEd Record Analytics & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Class Management Interface">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_academic.svg" alt="Academic & Class Management Interface" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Academic & 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 & 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–10) or <strong>SHS</strong> (Grade 11–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 & File Constraints">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_requirements.svg" alt="Document Checklist Configuration & File Constraints" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Document Checklist Configuration & 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 & Grace Period">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_archiving.svg" alt="Automatic Inactive Student Archiving & Grace Period" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Automatic Inactive Student Archiving & 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 & 12 Graduation Evaluation">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_graduation.svg" alt="Automated Grade 10 & 12 Graduation Evaluation" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Automated Grade 10 & 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 & 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 & 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 & Security Settings">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_account.svg" alt="User Profile & Security Settings" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">User Profile & 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 & Technical Support Information">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/settings_about.svg" alt="About TIS-RMS & Technical Support Information" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">About TIS-RMS & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & 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 & Teacher Side: Search, Filtering & 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 & Live Search">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/students_list.svg" alt="Student Information List & Live Search" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Student Information List & 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 & 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 & Document Checklist">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/students_profile.svg" alt="Student Profile View & Document Checklist" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Student Profile View & 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 & 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 & 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 & OCR Options">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/students_add.svg" alt="Add Student Dialog with Manual & OCR Options" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Add Student Dialog with Manual & 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 & 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 & CSV Import" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Bulk Student Registration via Multi-File OCR & 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 & 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 & OCR Scan History">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/students_enrollment.svg" alt="Edit Enrollment & OCR Scan History" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Edit Enrollment & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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 & 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 & 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 & 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 & 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 & 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 & 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 & Filter">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/users_list.svg" alt="Staff Accounts Directory & Filter" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Staff Accounts Directory & 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 & Password Reset">
|
||||
<div class="doc-figure-img-wrap">
|
||||
<img src="assets/images/screenshots/users_edit.svg" alt="Edit Account Details & Password Reset" loading="lazy">
|
||||
</div>
|
||||
<figcaption>
|
||||
<div>
|
||||
<span class="screenshot-title">Edit Account Details & 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 & 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 & 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">← 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 →</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 & Technical Handbook • Windows & 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 ↑</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">×</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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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` → **`TIS_RMS_64_v<version>.apk`**
|
||||
- `app-armeabi-v7a-release.apk` → **`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` → **`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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 392 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 343 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 96 KiB |