Initial commit

This commit is contained in:
2026-01-07 14:09:59 +08:00
commit 8a00aa71d5
939 changed files with 40616 additions and 0 deletions

293
src/admin/activities.php Normal file
View File

@@ -0,0 +1,293 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$errors = [];
$courses = $pdo->query('SELECT id, name FROM courses WHERE status = 1 ORDER BY name')->fetchAll();
$departments = $pdo->query('SELECT id, name FROM departments WHERE status = 1 ORDER BY name')->fetchAll();
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'create';
if ($action === 'delete') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM activities WHERE id = :id')->execute(['id' => $id]);
add_flash('success', 'Activity removed.');
redirect('admin/activities.php');
}
$payload = [
'name' => trim($_POST['name'] ?? ''),
'date' => $_POST['date'] ?? null,
'time_in' => $_POST['time_in'] ?? null,
'time_out' => $_POST['time_out'] ?? null,
'location' => trim($_POST['location'] ?? ''),
'required_students' => $_POST['required_students'] ?? 'all',
'course_id' => $_POST['required_students'] === 'specific_course' ? (int) ($_POST['course_id'] ?? 0) : null,
'department_id' => $_POST['required_students'] === 'specific_department' ? (int) ($_POST['department_id'] ?? 0) : null,
'description' => trim($_POST['description'] ?? ''),
'status' => (int) ($_POST['status'] ?? 1),
'created_by' => current_user()['id'],
];
if ($payload['name'] === '' || !$payload['date']) {
$errors[] = 'Activity name and date are required.';
}
if (!$errors) {
if ($action === 'update') {
$updatePayload = $payload;
$updatePayload['id'] = (int) ($_POST['id'] ?? 0);
unset($updatePayload['created_by']);
$stmt = $pdo->prepare(
'UPDATE activities SET name = :name, date = :date, time_in = :time_in, time_out = :time_out,
location = :location, required_students = :required_students, course_id = :course_id,
department_id = :department_id, description = :description, status = :status
WHERE id = :id'
);
$stmt->execute($updatePayload);
add_flash('success', 'Activity updated.');
} else {
$stmt = $pdo->prepare(
'INSERT INTO activities (name, date, time_in, time_out, location, required_students,
course_id, department_id, description, status, created_by)
VALUES (:name, :date, :time_in, :time_out, :location, :required_students,
:course_id, :department_id, :description, :status, :created_by)'
);
$stmt->execute($payload);
add_flash('success', 'Activity scheduled.');
}
redirect('admin/activities.php');
}
}
}
$activities = $pdo->query(
'SELECT a.*, c.name AS course_name, d.name AS department_name, u.full_name AS creator
FROM activities a
LEFT JOIN courses c ON a.course_id = c.id
LEFT JOIN departments d ON a.department_id = d.id
LEFT JOIN users u ON a.created_by = u.id
ORDER BY a.date DESC, a.time_in DESC'
)->fetchAll();
render_header('Activities', ['active' => 'activities']);
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Activities</h5>
<small class="text-muted"><?= count($activities) ?> scheduled</small>
</div>
<div class="d-flex gap-2 align-items-center">
<?php if ($errors): ?>
<span class="text-danger small"><?= sanitize(implode(' ', $errors)) ?></span>
<?php endif; ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#activityModal" data-mode="create">
Schedule Activity
</button>
</div>
</div>
<div class="card-body">
<?php if (!$activities): ?>
<p class="text-muted mb-0 text-center">No activities scheduled.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Audience</th>
<th>Location</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($activities as $activity): ?>
<?php
$recordPayload = encode_record([
'id' => (int) $activity['id'],
'name' => $activity['name'],
'date' => $activity['date'],
'time_in' => $activity['time_in'],
'time_out' => $activity['time_out'],
'location' => $activity['location'],
'required_students' => $activity['required_students'],
'course_id' => $activity['course_id'],
'department_id' => $activity['department_id'],
'description' => $activity['description'],
'status' => (int) $activity['status'],
]);
$timeIn = $activity['time_in'] !== null ? substr($activity['time_in'], 0, 5) : '—';
$timeOut = $activity['time_out'] !== null ? substr($activity['time_out'], 0, 5) : '—';
?>
<tr>
<td>
<strong><?= sanitize($activity['name']) ?></strong><br>
<span class="text-muted-sm">Created by <?= sanitize($activity['creator'] ?? 'N/A') ?></span>
</td>
<td>
<?= format_date($activity['date']) ?><br>
<span class="text-muted-sm"><?= sanitize($timeIn) ?> — <?= sanitize($timeOut) ?></span>
</td>
<td>
<?php
$audience = match ($activity['required_students']) {
'specific_course' => $activity['course_name'] ?? 'Course',
'specific_department' => $activity['department_name'] ?? 'Department',
default => 'All Students',
};
?>
<?= sanitize($audience) ?>
</td>
<td><?= sanitize($activity['location']) ?></td>
<td>
<span class="badge <?= $activity['status'] ? 'bg-success-subtle text-success' : 'bg-secondary' ?>">
<?= $activity['status'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#activityModal"
data-mode="edit"
data-record="<?= sanitize($recordPayload) ?>">
Edit
</button>
<button class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteActivityModal"
data-record-name="<?= sanitize($activity['name']) ?>"
data-record-ref="<?= sanitize(format_date($activity['date'])) ?>"
data-id="<?= (int) $activity['id'] ?>">
Delete
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Activity modal -->
<div class="modal fade" id="activityModal" tabindex="-1" aria-hidden="true"
data-create-title="Schedule Activity" data-edit-title="Edit Activity"
data-create-submit="Schedule Activity" data-edit-submit="Save Changes">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title" data-modal-title>Schedule Activity</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<input type="hidden" name="id" value="">
<div class="row g-3">
<div class="col-12">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-md-4">
<label class="form-label">Date</label>
<input type="date" name="date" class="form-control" required>
</div>
<div class="col-md-4">
<label class="form-label">Time In</label>
<input type="time" name="time_in" class="form-control">
</div>
<div class="col-md-4">
<label class="form-label">Time Out</label>
<input type="time" name="time_out" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Location</label>
<input type="text" name="location" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Required Participants</label>
<select name="required_students" class="form-select">
<option value="all" selected>All Students</option>
<option value="specific_course">Specific Course</option>
<option value="specific_department">Specific Department</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Course Filter</label>
<select name="course_id" class="form-select">
<option value="" selected>—</option>
<?php foreach ($courses as $course): ?>
<option value="<?= $course['id'] ?>"><?= sanitize($course['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Department Filter</label>
<select name="department_id" class="form-select">
<option value="" selected>—</option>
<?php foreach ($departments as $department): ?>
<option value="<?= $department['id'] ?>"><?= sanitize($department['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="1" selected>Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Schedule Activity</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete confirmation modal -->
<div class="modal fade" id="deleteActivityModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<div class="modal-header">
<h5 class="modal-title">Delete Activity</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="">
<p class="mb-1">Are you sure you want to delete this activity?</p>
<p class="fw-semibold mb-0" data-delete-name>Activity Name</p>
<small class="text-muted" data-delete-id data-label="Date">Date: —</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
<?php
render_footer();

131
src/admin/attendance.php Normal file
View File

@@ -0,0 +1,131 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
require_admin();
$pdo = db();
$activities = $pdo->query(
'SELECT id, name, date, time_in, time_out
FROM activities
WHERE status = 1 AND date >= CURDATE() - INTERVAL 1 DAY
ORDER BY date DESC, time_in DESC'
)->fetchAll();
$hasActivities = !empty($activities);
$todayAttendance = $pdo->query(
'SELECT att.*, s.full_name, s.student_id, act.name AS activity_name
FROM attendance att
INNER JOIN students s ON att.student_id = s.id
INNER JOIN activities act ON att.activity_id = act.id
WHERE DATE(att.time_in) = CURDATE()
ORDER BY att.time_in DESC'
)->fetchAll();
render_header('Scan Center', ['active' => 'attendance']);
?>
<div class="row g-4">
<div class="col-lg-5">
<div class="scan-panel h-100">
<h2>Admin Scan Center</h2>
<p class="mb-4 text-white-50">Only signed-in administrators can scan QR codes. Use the camera or type the token below.</p>
<?php if (!$hasActivities): ?>
<div class="alert alert-warning bg-warning-subtle text-dark">
No active activities available. Create one first to start scanning.
</div>
<?php endif; ?>
<form data-scan-form data-endpoint="<?= url_for('api/scan.php') ?>" class="v-stack gap-3">
<div>
<label class="form-label text-white">Activity</label>
<select name="activity_id" class="form-select form-select-lg" <?= $hasActivities ? '' : 'disabled' ?> required>
<?php if ($hasActivities): ?>
<?php foreach ($activities as $activity): ?>
<option value="<?= $activity['id'] ?>">
<?= sanitize($activity['name']) ?> — <?= format_date($activity['date'], 'M d') ?>
</option>
<?php endforeach; ?>
<?php else: ?>
<option value="">No activities</option>
<?php endif; ?>
</select>
</div>
<div>
<label class="form-label text-white">QR Token / Student ID</label>
<input type="text" name="qr_token" class="form-control form-control-lg"
placeholder="Scan or type token" autocomplete="off" <?= $hasActivities ? '' : 'disabled' ?> required>
</div>
<div>
<label class="form-label text-white">Notes (optional)</label>
<textarea name="notes" class="form-control" rows="2" placeholder="Remarks for this scan"></textarea>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="submit" class="btn btn-light btn-lg flex-grow-1" <?= $hasActivities ? '' : 'disabled' ?>>Record Attendance</button>
<button type="button" class="btn btn-outline-light btn-lg flex-grow-1" data-scan-start <?= $hasActivities ? '' : 'disabled' ?>>
Start Camera Scan
</button>
</div>
<p class="text-white-50 small mb-0">
The scanner auto-selects the best camera mode and falls back to a built-in detector if needed.
</p>
</form>
<div class="mt-4">
<p id="scan-status" class="fw-semibold mb-3"></p>
<div id="camera-viewer" class="camera-viewer bg-white rounded shadow-sm">
<div class="text-center text-muted small">Camera idle</div>
</div>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card shadow-sm h-100">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Todays Attendance</h5>
<span class="badge bg-primary-subtle text-primary"><?= count($todayAttendance) ?> captured</span>
</div>
<div class="card-body">
<div id="scan-result" class="mb-4"></div>
<?php if (!$todayAttendance): ?>
<p class="text-muted text-center mb-0">No scans recorded today.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Student</th>
<th>Activity</th>
<th>Time In</th>
<th>Time Out</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($todayAttendance as $row): ?>
<tr>
<td>
<strong><?= sanitize($row['full_name']) ?></strong><br>
<span class="text-muted-sm"><?= sanitize($row['student_id']) ?></span>
</td>
<td><?= sanitize($row['activity_name']) ?></td>
<td><?= format_datetime($row['time_in'], 'h:i A') ?></td>
<td><?= format_datetime($row['time_out'], 'h:i A') ?></td>
<td>
<span class="badge bg-success-subtle text-success badge-status">
<?= strtoupper(sanitize($row['status'])) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php
render_footer([
'extra_js' => [
'https://unpkg.com/html5-qrcode@2.3.10/html5-qrcode.min.js',
],
]);

223
src/admin/courses.php Normal file
View File

@@ -0,0 +1,223 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$errors = [];
$departments = $pdo->query('SELECT id, name FROM departments WHERE status = 1 ORDER BY name')->fetchAll();
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'create';
if ($action === 'delete') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM courses WHERE id = :id')->execute(['id' => $id]);
add_flash('success', 'Course removed.');
redirect('admin/courses.php');
}
$payload = [
'code' => strtoupper(trim($_POST['code'] ?? '')),
'name' => trim($_POST['name'] ?? ''),
'department_id' => (int) ($_POST['department_id'] ?? 0),
'description' => trim($_POST['description'] ?? ''),
'status' => (int) ($_POST['status'] ?? 1),
];
if ($payload['code'] === '' || $payload['name'] === '') {
$errors[] = 'Course code and name are required.';
}
if (!$errors) {
if ($action === 'update') {
$payload['id'] = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare(
'UPDATE courses SET code = :code, name = :name, department_id = :department_id,
description = :description, status = :status WHERE id = :id'
);
$stmt->execute($payload);
add_flash('success', 'Course updated.');
} else {
$stmt = $pdo->prepare(
'INSERT INTO courses (code, name, department_id, description, status)
VALUES (:code, :name, :department_id, :description, :status)'
);
$stmt->execute($payload);
add_flash('success', 'Course added.');
}
redirect('admin/courses.php');
}
}
}
$courses = $pdo->query(
'SELECT c.*, d.name AS department_name
FROM courses c
LEFT JOIN departments d ON c.department_id = d.id
ORDER BY c.name ASC'
)->fetchAll();
render_header('Courses', ['active' => 'courses']);
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Courses</h5>
<small class="text-muted"><?= count($courses) ?> total</small>
</div>
<div class="d-flex gap-2 align-items-center">
<?php if ($errors): ?>
<span class="text-danger small"><?= sanitize(implode(' ', $errors)) ?></span>
<?php endif; ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#courseModal" data-mode="create">
Add Course
</button>
</div>
</div>
<div class="card-body">
<?php if (!$courses): ?>
<p class="text-muted mb-0 text-center">No courses yet.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>Department</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($courses as $course): ?>
<?php
$recordPayload = encode_record([
'id' => (int) $course['id'],
'code' => $course['code'],
'name' => $course['name'],
'department_id' => (int) ($course['department_id'] ?? 0),
'description' => $course['description'],
'status' => (int) $course['status'],
]);
?>
<tr>
<td><code><?= sanitize($course['code']) ?></code></td>
<td><?= sanitize($course['name']) ?></td>
<td><?= sanitize($course['department_name'] ?? '—') ?></td>
<td>
<span class="badge <?= $course['status'] ? 'bg-success-subtle text-success' : 'bg-secondary' ?>">
<?= $course['status'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#courseModal"
data-mode="edit"
data-record="<?= sanitize($recordPayload) ?>">
Edit
</button>
<button class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteCourseModal"
data-record-name="<?= sanitize($course['name']) ?>"
data-record-ref="<?= sanitize($course['code']) ?>"
data-id="<?= (int) $course['id'] ?>">
Delete
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Course modal -->
<div class="modal fade" id="courseModal" tabindex="-1" aria-hidden="true"
data-create-title="Add Course" data-edit-title="Edit Course"
data-create-submit="Add Course" data-edit-submit="Save Changes">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title" data-modal-title>Add Course</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<input type="hidden" name="id" value="">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Code</label>
<input type="text" name="code" class="form-control text-uppercase" required>
</div>
<div class="col-md-6">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-12">
<label class="form-label">Department</label>
<select name="department_id" class="form-select">
<?php foreach ($departments as $department): ?>
<option value="<?= $department['id'] ?>"><?= sanitize($department['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="1" selected>Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Course</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete modal -->
<div class="modal fade" id="deleteCourseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<div class="modal-header">
<h5 class="modal-title">Delete Course</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="">
<p class="mb-1" data-delete-message>Delete this course?</p>
<p class="fw-semibold mb-0" data-delete-name>Course Name</p>
<small class="text-muted" data-delete-id data-label="Code">Code: —</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
<?php
render_footer();

148
src/admin/dashboard.php Normal file
View File

@@ -0,0 +1,148 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$stats = [
'students' => (int) $pdo->query('SELECT COUNT(*) FROM students')->fetchColumn(),
'activities' => (int) $pdo->query('SELECT COUNT(*) FROM activities WHERE status = 1')->fetchColumn(),
'attendance_today' => (int) $pdo->query('SELECT COUNT(*) FROM attendance WHERE DATE(time_in) = CURDATE()')->fetchColumn(),
'users' => (int) $pdo->query('SELECT COUNT(*) FROM users WHERE status = 1')->fetchColumn(),
];
$upcomingActivities = $pdo->query(
'SELECT a.*, d.name AS department_name, c.name AS course_name
FROM activities a
LEFT JOIN departments d ON a.department_id = d.id
LEFT JOIN courses c ON a.course_id = c.id
WHERE a.date >= CURDATE()
ORDER BY a.date ASC, a.time_in ASC
LIMIT 5'
)->fetchAll();
$recentAttendance = $pdo->query(
'SELECT att.*, s.full_name, act.name AS activity_name
FROM attendance att
INNER JOIN students s ON att.student_id = s.id
INNER JOIN activities act ON att.activity_id = act.id
ORDER BY att.updated_at DESC
LIMIT 10'
)->fetchAll();
render_header('Dashboard', ['active' => 'dashboard']);
?>
<div class="row g-4">
<div class="col-md-3">
<div class="stat-card">
<h6>Registered Students</h6>
<p class="stat-value"><?= $stats['students'] ?></p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Active Activities</h6>
<p class="stat-value"><?= $stats['activities'] ?></p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Attendance (Today)</h6>
<p class="stat-value"><?= $stats['attendance_today'] ?></p>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<h6>Active Users</h6>
<p class="stat-value"><?= $stats['users'] ?></p>
</div>
</div>
</div>
<div class="row mt-4 g-4">
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">Upcoming Activities</h5>
<a href="<?= url_for('admin/activities.php') ?>" class="btn btn-sm btn-outline-primary">Manage</a>
</div>
</div>
<div class="card-body">
<?php if (!$upcomingActivities): ?>
<p class="text-muted text-center mb-0">No scheduled activities yet.</p>
<?php else: ?>
<div class="list-group list-group-flush">
<?php foreach ($upcomingActivities as $activity): ?>
<div class="list-group-item">
<div class="d-flex justify-content-between">
<div>
<h6 class="mb-1"><?= sanitize($activity['name']) ?></h6>
<div class="text-muted-sm">
<?= format_date($activity['date']) ?> •
<?= sanitize(substr($activity['time_in'], 0, 5)) ?>
<?= sanitize(substr($activity['time_out'], 0, 5)) ?>
</div>
<?php if ($activity['required_students'] !== 'all'): ?>
<span class="chip mt-2">
<?= $activity['required_students'] === 'specific_course'
? sanitize($activity['course_name'] ?? 'Course Restricted')
: sanitize($activity['department_name'] ?? 'Department Restricted') ?>
</span>
<?php endif; ?>
</div>
<span class="badge rounded-pill bg-primary-subtle text-primary align-self-start">
<?= sanitize($activity['location']) ?>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0">Latest Attendance Logs</h5>
</div>
<div class="card-body">
<?php if (!$recentAttendance): ?>
<p class="text-muted text-center mb-0">No attendance captured yet.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>Student</th>
<th>Activity</th>
<th>Status</th>
<th>Recorded</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentAttendance as $row): ?>
<tr>
<td><?= sanitize($row['full_name']) ?></td>
<td><?= sanitize($row['activity_name']) ?></td>
<td>
<span class="badge bg-success-subtle text-success badge-status">
<?= strtoupper(sanitize($row['status'])) ?>
</span>
</td>
<td><?= format_datetime($row['updated_at']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php
render_footer();

223
src/admin/departments.php Normal file
View File

@@ -0,0 +1,223 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$errors = [];
$schools = $pdo->query('SELECT id, name FROM schools WHERE status = 1 ORDER BY name')->fetchAll();
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'create';
if ($action === 'delete') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM departments WHERE id = :id')->execute(['id' => $id]);
add_flash('success', 'Department removed.');
redirect('admin/departments.php');
}
$payload = [
'code' => strtoupper(trim($_POST['code'] ?? '')),
'name' => trim($_POST['name'] ?? ''),
'school_id' => (int) ($_POST['school_id'] ?? 0),
'description' => trim($_POST['description'] ?? ''),
'status' => (int) ($_POST['status'] ?? 1),
];
if ($payload['code'] === '' || $payload['name'] === '') {
$errors[] = 'Department code and name are required.';
}
if (!$errors) {
if ($action === 'update') {
$payload['id'] = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare(
'UPDATE departments SET code = :code, name = :name, school_id = :school_id,
description = :description, status = :status WHERE id = :id'
);
$stmt->execute($payload);
add_flash('success', 'Department updated.');
} else {
$stmt = $pdo->prepare(
'INSERT INTO departments (code, name, school_id, description, status)
VALUES (:code, :name, :school_id, :description, :status)'
);
$stmt->execute($payload);
add_flash('success', 'Department added.');
}
redirect('admin/departments.php');
}
}
}
$departments = $pdo->query(
'SELECT d.*, s.name AS school_name
FROM departments d
LEFT JOIN schools s ON d.school_id = s.id
ORDER BY d.name ASC'
)->fetchAll();
render_header('Departments', ['active' => 'departments']);
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Departments</h5>
<small class="text-muted"><?= count($departments) ?> total</small>
</div>
<div class="d-flex gap-2 align-items-center">
<?php if ($errors): ?>
<span class="text-danger small"><?= sanitize(implode(' ', $errors)) ?></span>
<?php endif; ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#departmentModal" data-mode="create">
Add Department
</button>
</div>
</div>
<div class="card-body">
<?php if (!$departments): ?>
<p class="text-muted mb-0 text-center">No departments yet.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>School</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($departments as $department): ?>
<?php
$recordPayload = encode_record([
'id' => (int) $department['id'],
'code' => $department['code'],
'name' => $department['name'],
'school_id' => (int) ($department['school_id'] ?? 0),
'description' => $department['description'],
'status' => (int) $department['status'],
]);
?>
<tr>
<td><code><?= sanitize($department['code']) ?></code></td>
<td><?= sanitize($department['name']) ?></td>
<td><?= sanitize($department['school_name'] ?? '—') ?></td>
<td>
<span class="badge <?= $department['status'] ? 'bg-success-subtle text-success' : 'bg-secondary' ?>">
<?= $department['status'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#departmentModal"
data-mode="edit"
data-record="<?= sanitize($recordPayload) ?>">
Edit
</button>
<button class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteDepartmentModal"
data-record-name="<?= sanitize($department['name']) ?>"
data-record-ref="<?= sanitize($department['code']) ?>"
data-id="<?= (int) $department['id'] ?>">
Delete
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Department modal -->
<div class="modal fade" id="departmentModal" tabindex="-1" aria-hidden="true"
data-create-title="Add Department" data-edit-title="Edit Department"
data-create-submit="Add Department" data-edit-submit="Save Changes">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title" data-modal-title>Add Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<input type="hidden" name="id" value="">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Code</label>
<input type="text" name="code" class="form-control text-uppercase" required>
</div>
<div class="col-md-6">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-12">
<label class="form-label">School</label>
<select name="school_id" class="form-select">
<?php foreach ($schools as $school): ?>
<option value="<?= $school['id'] ?>"><?= sanitize($school['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-12">
<label class="form-label">Description</label>
<textarea name="description" class="form-control" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="1" selected>Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Department</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete modal -->
<div class="modal fade" id="deleteDepartmentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<div class="modal-header">
<h5 class="modal-title">Delete Department</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="">
<p class="mb-1" data-delete-message>Delete this department?</p>
<p class="fw-semibold mb-0" data-delete-name>Department Name</p>
<small class="text-muted" data-delete-id data-label="Code">Code: —</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
<?php
render_footer();

146
src/admin/reports.php Normal file
View File

@@ -0,0 +1,146 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$activities = $pdo->query('SELECT id, name FROM activities ORDER BY date DESC')->fetchAll();
$filters = [
'start_date' => $_GET['start_date'] ?? date('Y-m-01'),
'end_date' => $_GET['end_date'] ?? date('Y-m-d'),
'activity_id' => $_GET['activity_id'] ?? '',
'status' => $_GET['status'] ?? '',
];
$conditions = [];
$params = [];
if ($filters['start_date']) {
$conditions[] = 'DATE(att.time_in) >= :start_date';
$params['start_date'] = $filters['start_date'];
}
if ($filters['end_date']) {
$conditions[] = 'DATE(att.time_in) <= :end_date';
$params['end_date'] = $filters['end_date'];
}
if ($filters['activity_id']) {
$conditions[] = 'att.activity_id = :activity_id';
$params['activity_id'] = (int) $filters['activity_id'];
}
if ($filters['status']) {
$conditions[] = 'att.status = :status';
$params['status'] = $filters['status'];
}
$where = $conditions ? 'WHERE ' . implode(' AND ', $conditions) : '';
$stmt = $pdo->prepare(
"SELECT att.*, s.full_name, s.student_id, act.name AS activity_name
FROM attendance att
INNER JOIN students s ON att.student_id = s.id
INNER JOIN activities act ON att.activity_id = act.id
$where
ORDER BY att.time_in DESC"
);
$stmt->execute($params);
$records = $stmt->fetchAll();
render_header('Reports', ['active' => 'reports']);
?>
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0">Attendance Filters</h5>
</div>
<div class="card-body">
<form method="get" class="row gy-3">
<div class="col-md-3">
<label class="form-label">Start Date</label>
<input type="date" name="start_date" class="form-control"
value="<?= sanitize($filters['start_date']) ?>">
</div>
<div class="col-md-3">
<label class="form-label">End Date</label>
<input type="date" name="end_date" class="form-control"
value="<?= sanitize($filters['end_date']) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Activity</label>
<select name="activity_id" class="form-select">
<option value="">All activities</option>
<?php foreach ($activities as $activity): ?>
<option value="<?= $activity['id'] ?>" <?= selected($filters['activity_id'], $activity['id']) ?>>
<?= sanitize($activity['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">Any status</option>
<option value="present" <?= selected($filters['status'], 'present') ?>>Present</option>
<option value="late" <?= selected($filters['status'], 'late') ?>>Late</option>
<option value="absent" <?= selected($filters['status'], 'absent') ?>>Absent</option>
<option value="excused" <?= selected($filters['status'], 'excused') ?>>Excused</option>
</select>
</div>
<div class="col-md-1 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Apply</button>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">Attendance Records</h5>
<a href="<?= url_for('api/manual_entry.php?'.http_build_query($filters)) ?>" class="btn btn-sm btn-outline-secondary" target="_blank">
Export JSON
</a>
</div>
<div class="card-body">
<?php if (!$records): ?>
<p class="text-muted text-center mb-0">No records match the filters.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Student</th>
<th>Activity</th>
<th>Status</th>
<th>In</th>
<th>Out</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<?php foreach ($records as $row): ?>
<tr>
<td>
<strong><?= sanitize($row['full_name']) ?></strong><br>
<span class="text-muted-sm"><?= sanitize($row['student_id']) ?></span>
</td>
<td><?= sanitize($row['activity_name']) ?></td>
<td>
<span class="badge <?= $row['status'] === 'present'
? 'bg-success-subtle text-success'
: 'bg-warning-subtle text-warning' ?>">
<?= strtoupper(sanitize($row['status'])) ?>
</span>
</td>
<td><?= format_datetime($row['time_in']) ?></td>
<td><?= format_datetime($row['time_out']) ?></td>
<td><?= sanitize($row['notes'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php
render_footer();

163
src/admin/settings.php Normal file
View File

@@ -0,0 +1,163 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
require_admin();
$pdo = db();
$errors = [];
$userId = current_user()['id'];
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'save';
if ($action === 'save') {
$settings = $_POST['settings'] ?? [];
$stmt = $pdo->prepare(
'INSERT INTO system_settings (setting_key, setting_value, description, updated_by)
VALUES (:key, :value, :description, :updated_by)
ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value), description = VALUES(description),
updated_by = VALUES(updated_by), updated_at = CURRENT_TIMESTAMP'
);
foreach ($settings as $key => $data) {
$stmt->execute([
'key' => $key,
'value' => $data['value'] ?? '',
'description' => $data['description'] ?? '',
'updated_by' => $userId,
]);
}
add_flash('success', 'Settings updated.');
redirect('admin/settings.php');
}
if ($action === 'create') {
$key = strtolower(trim($_POST['new_key'] ?? ''));
$value = trim($_POST['new_value'] ?? '');
$description = trim($_POST['new_description'] ?? '');
if ($key === '') {
$errors[] = 'Setting key is required.';
} else {
$stmt = $pdo->prepare(
'INSERT INTO system_settings (setting_key, setting_value, description, updated_by)
VALUES (:key, :value, :description, :updated_by)'
);
$stmt->execute([
'key' => $key,
'value' => $value,
'description' => $description,
'updated_by' => $userId,
]);
add_flash('success', 'Setting created.');
redirect('admin/settings.php');
}
}
}
}
$settings = $pdo->query(
'SELECT ss.*, u.full_name AS updated_by_name
FROM system_settings ss
LEFT JOIN users u ON ss.updated_by = u.id
ORDER BY ss.setting_key ASC'
)->fetchAll();
render_header('Settings', ['active' => 'settings']);
?>
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">System Settings</h5>
<form method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="save">
<button type="submit" class="btn btn-primary btn-sm">Save Changes</button>
</form>
</div>
<div class="card-body">
<?php if ($errors): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $error): ?>
<div><?= sanitize($error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!$settings): ?>
<p class="text-muted">No settings found. Use the form on the right to add one.</p>
<?php else: ?>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="save">
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Description</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
<?php foreach ($settings as $setting): ?>
<tr>
<td class="text-uppercase fw-semibold"><?= sanitize($setting['setting_key']) ?></td>
<td style="width: 25%;">
<input type="text" name="settings[<?= sanitize($setting['setting_key']) ?>][value]"
class="form-control" value="<?= sanitize($setting['setting_value']) ?>">
</td>
<td>
<input type="text" name="settings[<?= sanitize($setting['setting_key']) ?>][description]"
class="form-control" value="<?= sanitize($setting['description']) ?>">
</td>
<td class="text-muted-sm">
<?= format_datetime($setting['updated_at']) ?><br>
<?= sanitize($setting['updated_by_name'] ?? 'System') ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="text-end">
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white">
<h5 class="mb-0">Add Setting</h5>
</div>
<div class="card-body">
<form method="post" class="v-stack gap-3">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<div>
<label class="form-label">Key</label>
<input type="text" name="new_key" class="form-control text-lowercase"
placeholder="attendance.cutoff" required>
</div>
<div>
<label class="form-label">Value</label>
<input type="text" name="new_value" class="form-control" placeholder="Value">
</div>
<div>
<label class="form-label">Description</label>
<textarea name="new_description" class="form-control" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-outline-primary">Create Setting</button>
</form>
</div>
</div>
</div>
</div>
<?php
render_footer();

334
src/admin/students.php Normal file
View File

@@ -0,0 +1,334 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
$errors = [];
$courses = $pdo->query('SELECT id, code, name FROM courses WHERE status = 1 ORDER BY name')->fetchAll();
$departments = $pdo->query('SELECT id, code, name FROM departments WHERE status = 1 ORDER BY name')->fetchAll();
$genders = $pdo->query('SELECT id, name FROM genders ORDER BY id')->fetchAll();
$schools = $pdo->query('SELECT id, name FROM schools WHERE status = 1 ORDER BY name')->fetchAll();
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'create';
if ($action === 'delete') {
$id = (int) ($_POST['id'] ?? 0);
$pdo->prepare('DELETE FROM students WHERE id = :id')->execute(['id' => $id]);
add_flash('success', 'Student removed.');
redirect('admin/students.php');
}
$payload = [
'student_id' => trim($_POST['student_id'] ?? ''),
'full_name' => trim($_POST['full_name'] ?? ''),
'gender_id' => (int) ($_POST['gender_id'] ?? 0),
'year_level' => (int) ($_POST['year_level'] ?? 1),
'course_id' => (int) ($_POST['course_id'] ?? 0),
'department_id' => (int) ($_POST['department_id'] ?? 0),
'school_id' => (int) ($_POST['school_id'] ?? 0),
'email' => trim($_POST['email'] ?? ''),
'contact_number' => trim($_POST['contact_number'] ?? ''),
'address' => trim($_POST['address'] ?? ''),
'status' => (int) ($_POST['status'] ?? 1),
];
if ($payload['student_id'] === '' || $payload['full_name'] === '') {
$errors[] = 'Student ID and full name are required.';
}
if (!$errors) {
if ($action === 'update') {
$payload['id'] = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare(
'UPDATE students SET student_id = :student_id, full_name = :full_name, gender_id = :gender_id,
year_level = :year_level, course_id = :course_id, department_id = :department_id,
school_id = :school_id, email = :email, contact_number = :contact_number,
address = :address, status = :status
WHERE id = :id'
);
$stmt->execute($payload);
add_flash('success', 'Student updated.');
} else {
$qrToken = sprintf('STU-%s', strtoupper(random_token(6)));
$stmt = $pdo->prepare(
'INSERT INTO students (student_id, full_name, gender_id, year_level, course_id, department_id, school_id,
email, contact_number, address, qr_code, status)
VALUES (:student_id, :full_name, :gender_id, :year_level, :course_id, :department_id, :school_id,
:email, :contact_number, :address, :qr_code, :status)'
);
$stmt->execute(array_merge($payload, ['qr_code' => $qrToken]));
add_flash('success', 'Student registered.');
}
redirect('admin/students.php');
}
}
}
$pagination = paginate($pdo, 'SELECT COUNT(*) FROM students');
$stmt = $pdo->prepare(
'SELECT s.*, c.name AS course_name, d.name AS department_name
FROM students s
LEFT JOIN courses c ON s.course_id = c.id
LEFT JOIN departments d ON s.department_id = d.id
ORDER BY s.full_name ASC
LIMIT :limit OFFSET :offset'
);
$stmt->bindValue(':limit', $pagination['per_page'], PDO::PARAM_INT);
$stmt->bindValue(':offset', $pagination['offset'], PDO::PARAM_INT);
$stmt->execute();
$students = $stmt->fetchAll();
render_header('Students', ['active' => 'students']);
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Student Directory</h5>
<small class="text-muted">Page <?= $pagination['page'] ?> of <?= $pagination['pages'] ?></small>
</div>
<div class="d-flex gap-2 align-items-center">
<?php if ($errors): ?>
<span class="text-danger small"><?= sanitize(implode(' ', $errors)) ?></span>
<?php endif; ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#studentModal" data-mode="create">
Add Student
</button>
</div>
</div>
<div class="card-body">
<?php if (!$students): ?>
<p class="text-muted mb-0 text-center">No students yet.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Student</th>
<th>Course</th>
<th>Department</th>
<th>QR Token</th>
<th>Status</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($students as $student): ?>
<?php
$payload = encode_record([
'id' => (int) $student['id'],
'student_id' => $student['student_id'],
'full_name' => $student['full_name'],
'gender_id' => (int) $student['gender_id'],
'year_level' => (int) $student['year_level'],
'course_id' => (int) $student['course_id'],
'department_id' => (int) $student['department_id'],
'school_id' => (int) $student['school_id'],
'email' => $student['email'],
'contact_number' => $student['contact_number'],
'address' => $student['address'],
'status' => (int) $student['status'],
]);
?>
<tr>
<td>
<strong><?= sanitize($student['full_name']) ?></strong><br>
<span class="text-muted-sm"><?= sanitize($student['student_id']) ?></span>
</td>
<td><?= sanitize($student['course_name'] ?? '—') ?></td>
<td><?= sanitize($student['department_name'] ?? '—') ?></td>
<td>
<code class="d-block mb-1"><?= sanitize($student['qr_code']) ?></code>
<button type="button"
class="btn btn-sm btn-link px-0"
data-bs-toggle="modal"
data-bs-target="#qrModal"
data-qr-token="<?= sanitize($student['qr_code']) ?>"
data-student-id="<?= sanitize($student['student_id']) ?>"
data-student-name="<?= sanitize($student['full_name']) ?>">
View QR
</button>
</td>
<td>
<span class="badge <?= $student['status'] ? 'bg-success-subtle text-success' : 'bg-secondary' ?>">
<?= $student['status'] ? 'Active' : 'Inactive' ?>
</span>
</td>
<td class="text-end">
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#studentModal"
data-mode="edit"
data-record="<?= sanitize($payload) ?>">
Edit
</button>
<button class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal"
data-bs-target="#deleteStudentModal"
data-student-name="<?= sanitize($student['full_name']) ?>"
data-student-id="<?= sanitize($student['student_id']) ?>"
data-record-name="<?= sanitize($student['full_name']) ?>"
data-record-ref="<?= sanitize($student['student_id']) ?>"
data-id="<?= (int) $student['id'] ?>">
Delete
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_pagination($pagination); ?>
<?php endif; ?>
</div>
</div>
<!-- Student modal -->
<div class="modal fade" id="studentModal" tabindex="-1" aria-hidden="true"
data-create-title="Add Student" data-edit-title="Edit Student"
data-create-submit="Add Student" data-edit-submit="Save Changes">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title" data-modal-title>Add Student</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<input type="hidden" name="id" value="">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Student ID</label>
<input type="text" name="student_id" class="form-control" required>
</div>
<div class="col-md-6">
<label class="form-label">Full Name</label>
<input type="text" name="full_name" class="form-control" required>
</div>
<div class="col-md-6">
<label class="form-label">Gender</label>
<select name="gender_id" class="form-select">
<?php foreach ($genders as $gender): ?>
<option value="<?= $gender['id'] ?>"><?= sanitize($gender['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Year Level</label>
<input type="number" min="1" max="6" name="year_level" class="form-control" value="1">
</div>
<div class="col-md-6">
<label class="form-label">Course</label>
<select name="course_id" class="form-select">
<?php foreach ($courses as $course): ?>
<option value="<?= $course['id'] ?>"><?= sanitize($course['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Department</label>
<select name="department_id" class="form-select">
<?php foreach ($departments as $department): ?>
<option value="<?= $department['id'] ?>"><?= sanitize($department['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">School</label>
<select name="school_id" class="form-select">
<?php foreach ($schools as $school): ?>
<option value="<?= $school['id'] ?>"><?= sanitize($school['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Contact Number</label>
<input type="text" name="contact_number" class="form-control">
</div>
<div class="col-12">
<label class="form-label">Address</label>
<textarea name="address" class="form-control" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="1">Active</option>
<option value="0">Inactive</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Student</button>
</div>
</form>
</div>
</div>
</div>
<!-- QR preview modal -->
<div class="modal fade" id="qrModal" tabindex="-1" aria-hidden="true" data-generator="<?= url_for('qr/generate.php') ?>">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div>
<div class="text-muted small">QR Code</div>
<h5 class="modal-title mb-0" data-qr-name>Student Name</h5>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<div class="mb-2 text-muted small" data-qr-id>ID: —</div>
<div class="qr-preview mb-3">
<img src="" alt="QR Code" class="img-fluid" data-qr-image>
</div>
<code class="d-block" data-qr-token></code>
</div>
<div class="modal-footer">
<a href="#" class="btn btn-primary w-100" data-qr-download download>
Download PNG
</a>
</div>
</div>
</div>
</div>
<!-- Delete confirmation modal -->
<div class="modal fade" id="deleteStudentModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<div class="modal-header">
<h5 class="modal-title">Remove Student</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="">
<p class="mb-1">Are you sure you want to remove this student?</p>
<p class="fw-semibold mb-0" data-delete-name>Student Name</p>
<small class="text-muted" data-delete-id data-label="ID">ID: —</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
</div>
<?php
render_footer();

262
src/admin/users.php Normal file
View File

@@ -0,0 +1,262 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
require_admin();
$pdo = db();
$errors = [];
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token.';
} else {
$action = $_POST['action'] ?? 'create';
if ($action === 'create') {
$payload = [
'username' => strtolower(trim($_POST['username'] ?? '')),
'full_name' => trim($_POST['full_name'] ?? ''),
'email' => trim($_POST['email'] ?? ''),
'role' => $_POST['role'] ?? 'admin',
'password' => $_POST['password'] ?? '',
];
if ($payload['username'] === '' || $payload['password'] === '' || $payload['full_name'] === '') {
$errors[] = 'Username, full name, and password are required.';
} else {
$stmt = $pdo->prepare(
'INSERT INTO users (username, password, role, full_name, email, status)
VALUES (:username, :password, :role, :full_name, :email, 1)'
);
$stmt->execute([
'username' => $payload['username'],
'password' => password_hash($payload['password'], PASSWORD_BCRYPT),
'role' => $payload['role'],
'full_name' => $payload['full_name'],
'email' => $payload['email'],
]);
add_flash('success', 'User account created.');
redirect('admin/users.php');
}
}
if ($action === 'toggle') {
$id = (int) ($_POST['id'] ?? 0);
$stmt = $pdo->prepare('UPDATE users SET status = IF(status = 1, 0, 1) WHERE id = :id');
$stmt->execute(['id' => $id]);
add_flash('success', 'User status updated.');
redirect('admin/users.php');
}
if ($action === 'reset_password') {
$id = (int) ($_POST['id'] ?? 0);
$newPassword = trim($_POST['new_password'] ?? '');
if ($newPassword === '') {
$errors[] = 'New password is required.';
} else {
$stmt = $pdo->prepare('UPDATE users SET password = :password WHERE id = :id');
$stmt->execute([
'password' => password_hash($newPassword, PASSWORD_BCRYPT),
'id' => $id,
]);
add_flash('success', 'Password updated.');
redirect('admin/users.php');
}
}
}
}
$users = $pdo->query(
'SELECT id, username, full_name, email, role, status, created_at
FROM users ORDER BY created_at DESC'
)->fetchAll();
render_header('Users', ['active' => 'users']);
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">Users</h5>
<small class="text-muted"><?= count($users) ?> accounts</small>
</div>
<div class="d-flex gap-2 align-items-center">
<?php if ($errors): ?>
<span class="text-danger small"><?= sanitize(implode(' ', $errors)) ?></span>
<?php endif; ?>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#userModal">
Create User
</button>
</div>
</div>
<div class="card-body">
<?php if (!$users): ?>
<p class="text-muted text-center mb-0">No users found.</p>
<?php else: ?>
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Status</th>
<th>Created</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<?php
$statusLabel = $user['status'] ? 'Active' : 'Disabled';
$toggleAction = $user['status'] ? 'Disable' : 'Enable';
$toggleConfirm = $user['status'] ? 'Disable this user?' : 'Enable this user?';
?>
<tr>
<td>
<strong><?= sanitize($user['full_name']) ?></strong><br>
<span class="text-muted-sm"><?= sanitize($user['username']) ?> · <?= sanitize($user['email']) ?></span>
</td>
<td><?= strtoupper(sanitize($user['role'])) ?></td>
<td>
<span class="badge <?= $user['status'] ? 'bg-success-subtle text-success' : 'bg-secondary' ?>">
<?= $statusLabel ?>
</span>
</td>
<td><?= format_datetime($user['created_at']) ?></td>
<td class="text-end">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary"
data-bs-toggle="modal"
data-bs-target="#toggleUserModal"
data-id="<?= (int) $user['id'] ?>"
data-record-name="<?= sanitize($user['full_name']) ?>"
data-record-ref="<?= sanitize($user['username']) ?>"
data-confirm-text="<?= sanitize($toggleConfirm) ?>"
data-confirm-label="<?= sanitize($toggleAction) ?>">
<?= $toggleAction ?>
</button>
<button class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#resetPasswordModal"
data-id="<?= (int) $user['id'] ?>"
data-record-name="<?= sanitize($user['full_name']) ?>"
data-record-ref="<?= sanitize($user['username']) ?>">
Reset Password
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Create user modal -->
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true"
data-create-title="Create User" data-edit-title="Create User"
data-create-submit="Create Account" data-edit-submit="Create Account">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title" data-modal-title>Create User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="create">
<input type="hidden" name="id" value="">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control text-lowercase" required>
</div>
<div class="col-md-6">
<label class="form-label">Full Name</label>
<input type="text" name="full_name" class="form-control" required>
</div>
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control">
</div>
<div class="col-md-6">
<label class="form-label">Role</label>
<select name="role" class="form-select">
<option value="admin">Admin</option>
<option value="staff">Staff</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create Account</button>
</div>
</form>
</div>
</div>
</div>
<!-- Toggle status modal -->
<div class="modal fade" id="toggleUserModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post">
<div class="modal-header">
<h5 class="modal-title">Update Status</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="">
<p class="mb-1" data-delete-message>Update user status?</p>
<p class="fw-semibold mb-0" data-delete-name>User Name</p>
<small class="text-muted" data-delete-id data-label="Username">Username: —</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning" data-delete-confirm>Confirm</button>
</div>
</form>
</div>
</div>
</div>
<!-- Reset password modal -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post" class="needs-validation" novalidate>
<div class="modal-header">
<h5 class="modal-title">Reset Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<input type="hidden" name="action" value="reset_password">
<input type="hidden" name="id" value="">
<p class="mb-1">Reset password for:</p>
<p class="fw-semibold mb-0" data-delete-name>User Name</p>
<small class="text-muted" data-delete-id data-label="Username">Username: —</small>
<div class="mt-3">
<label class="form-label">New Password</label>
<input type="password" name="new_password" class="form-control" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary" data-delete-confirm>Update Password</button>
</div>
</form>
</div>
</div>
</div>
<?php
render_footer();

123
src/api/manual_entry.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
$pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
$studentIdentifier = trim($_POST['student'] ?? '');
$activityId = (int) ($_POST['activity_id'] ?? 0);
$status = $_POST['status'] ?? 'present';
$notes = trim($_POST['notes'] ?? 'Manual entry');
if ($studentIdentifier === '' || !$activityId) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => 'Student and activity are required.']);
exit;
}
$studentStmt = $pdo->prepare(
'SELECT * FROM students WHERE (student_id = :identifier OR qr_code = :identifier) LIMIT 1'
);
$studentStmt->execute(['identifier' => $studentIdentifier]);
$student = $studentStmt->fetch();
if (!$student) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Student not found.']);
exit;
}
$activityStmt = $pdo->prepare('SELECT * FROM activities WHERE id = :id LIMIT 1');
$activityStmt->execute(['id' => $activityId]);
$activity = $activityStmt->fetch();
if (!$activity) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Activity not found.']);
exit;
}
$now = (new DateTime())->format('Y-m-d H:i:s');
$attendanceStmt = $pdo->prepare(
'INSERT INTO attendance (student_id, activity_id, time_in, status, notes)
VALUES (:student_id, :activity_id, :time_in, :status, :notes)
ON DUPLICATE KEY UPDATE status = VALUES(status), notes = VALUES(notes),
updated_at = CURRENT_TIMESTAMP'
);
$attendanceStmt->execute([
'student_id' => $student['id'],
'activity_id' => $activityId,
'time_in' => $now,
'status' => $status,
'notes' => $notes,
]);
$attendanceId = (int) $pdo->lastInsertId();
if (!$attendanceId) {
$idLookup = $pdo->prepare(
'SELECT id FROM attendance WHERE student_id = :student_id AND activity_id = :activity_id LIMIT 1'
);
$idLookup->execute([
'student_id' => $student['id'],
'activity_id' => $activityId,
]);
$attendanceId = (int) $idLookup->fetchColumn();
}
$log = $pdo->prepare(
'INSERT INTO attendance_logs (attendance_id, action, old_value, new_value, changed_by, notes)
VALUES (:attendance_id, :action, :old_value, :new_value, :changed_by, :notes)'
);
$log->execute([
'attendance_id' => $attendanceId,
'action' => 'manual_entry',
'old_value' => null,
'new_value' => $now,
'changed_by' => current_user()['id'],
'notes' => $notes,
]);
echo json_encode(['success' => true, 'message' => 'Manual attendance recorded.']);
exit;
}
// GET export
$filters = [
'start_date' => $_GET['start_date'] ?? null,
'end_date' => $_GET['end_date'] ?? null,
'activity_id' => $_GET['activity_id'] ?? null,
'status' => $_GET['status'] ?? null,
];
$conditions = [];
$params = [];
if ($filters['start_date']) {
$conditions[] = 'DATE(att.time_in) >= :start_date';
$params['start_date'] = $filters['start_date'];
}
if ($filters['end_date']) {
$conditions[] = 'DATE(att.time_in) <= :end_date';
$params['end_date'] = $filters['end_date'];
}
if ($filters['activity_id']) {
$conditions[] = 'att.activity_id = :activity_id';
$params['activity_id'] = (int) $filters['activity_id'];
}
if ($filters['status']) {
$conditions[] = 'att.status = :status';
$params['status'] = $filters['status'];
}
$where = $conditions ? 'WHERE ' . implode(' AND ', $conditions) : '';
$stmt = $pdo->prepare(
"SELECT att.*, s.full_name, s.student_id, act.name AS activity_name
FROM attendance att
INNER JOIN students s ON att.student_id = s.id
INNER JOIN activities act ON att.activity_id = act.id
$where
ORDER BY att.time_in DESC"
);
$stmt->execute($params);
header('Content-Type: application/json');
echo json_encode($stmt->fetchAll());

137
src/api/scan.php Normal file
View File

@@ -0,0 +1,137 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
require_login();
require_admin();
header('Content-Type: application/json');
$payload = json_decode(file_get_contents('php://input'), true);
if (!is_array($payload)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid payload.']);
exit;
}
$qrToken = trim($payload['qr_token'] ?? '');
$activityId = (int) ($payload['activity_id'] ?? 0);
$notes = trim($payload['notes'] ?? '');
if ($qrToken === '' || !$activityId) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => 'QR token and activity are required.']);
exit;
}
$pdo = db();
$activityStmt = $pdo->prepare('SELECT * FROM activities WHERE id = :id AND status = 1 LIMIT 1');
$activityStmt->execute(['id' => $activityId]);
$activity = $activityStmt->fetch();
if (!$activity) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Activity not found or inactive.']);
exit;
}
$studentStmt = $pdo->prepare(
'SELECT * FROM students WHERE (qr_code = :token OR student_id = :token) AND status = 1 LIMIT 1'
);
$studentStmt->execute(['token' => $qrToken]);
$student = $studentStmt->fetch();
if (!$student) {
http_response_code(404);
echo json_encode(['success' => false, 'message' => 'Student not found or inactive.']);
exit;
}
$pdo->beginTransaction();
try {
$attendanceStmt = $pdo->prepare(
'SELECT * FROM attendance WHERE student_id = :student_id AND activity_id = :activity_id LIMIT 1'
);
$attendanceStmt->execute([
'student_id' => $student['id'],
'activity_id' => $activity['id'],
]);
$attendance = $attendanceStmt->fetch();
$now = (new DateTime())->format('Y-m-d H:i:s');
$action = '';
if (!$attendance) {
$insert = $pdo->prepare(
'INSERT INTO attendance (student_id, activity_id, time_in, status, notes)
VALUES (:student_id, :activity_id, :time_in, :status, :notes)'
);
$insert->execute([
'student_id' => $student['id'],
'activity_id' => $activity['id'],
'time_in' => $now,
'status' => 'present',
'notes' => $notes,
]);
$attendanceId = (int) $pdo->lastInsertId();
$action = 'time_in';
} elseif ($attendance['time_out'] === null) {
$update = $pdo->prepare(
'UPDATE attendance SET time_out = :time_out, notes = :notes, updated_at = CURRENT_TIMESTAMP
WHERE id = :id'
);
$update->execute([
'time_out' => $now,
'notes' => $notes ?: $attendance['notes'],
'id' => $attendance['id'],
]);
$attendanceId = (int) $attendance['id'];
$action = 'time_out';
} else {
$pdo->rollBack();
echo json_encode([
'success' => true,
'message' => 'Attendance already completed for this student.',
'meta' => [
'student' => $student['full_name'],
'activity' => $activity['name'],
'action' => 'ignored',
'timestamp' => format_datetime($attendance['updated_at']),
],
]);
exit;
}
$log = $pdo->prepare(
'INSERT INTO attendance_logs (attendance_id, action, old_value, new_value, changed_by, notes)
VALUES (:attendance_id, :action, :old_value, :new_value, :changed_by, :notes)'
);
$log->execute([
'attendance_id' => $attendanceId,
'action' => $action,
'old_value' => null,
'new_value' => $now,
'changed_by' => current_user()['id'],
'notes' => $notes,
]);
$pdo->commit();
echo json_encode([
'success' => true,
'message' => sprintf(
'Marked %s for %s.',
$action === 'time_in' ? 'time in' : 'time out',
$student['full_name']
),
'meta' => [
'student' => $student['full_name'],
'activity' => $activity['name'],
'action' => $action,
'timestamp' => format_datetime($now),
],
]);
} catch (Throwable $exception) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Unable to record attendance.']);
}

424
src/assets/css/style.css Normal file
View File

@@ -0,0 +1,424 @@
:root {
--brand-primary: #1b7f5c;
--brand-secondary: #3fc380;
--brand-accent: #a2d729;
--brand-muted: #edf6f0;
--bs-primary: var(--brand-primary);
--bs-primary-rgb: 27, 127, 92;
--bs-primary-bg-subtle: #d6f5ea;
--bs-primary-border-subtle: #a1dfc6;
--bs-link-color: var(--brand-primary);
--bs-link-hover-color: #146946;
}
body {
background-color: #f2f9f4;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
min-height: 100vh;
}
.auth-shell {
width: 100%;
max-width: 100%;
margin: 0 auto;
}
.google-auth-shell {
width: 100%;
min-height: calc(100vh - 4rem);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
padding: 2rem 1rem 3rem;
background:
radial-gradient(circle at top, rgba(66, 133, 244, 0.12), transparent 55%),
radial-gradient(circle at 20% 20%, rgba(52, 168, 83, 0.12), transparent 45%),
radial-gradient(circle at 80% 0%, rgba(234, 67, 53, 0.12), transparent 50%),
#f2f9f4;
}
.google-auth-card {
background: #fff;
border-radius: 16px;
border: 1px solid rgba(218, 220, 224, 0.8);
padding: 2.75rem 3.25rem;
max-width: 460px;
width: 100%;
box-shadow:
0 10px 30px rgba(26, 115, 232, 0.08),
0 2px 8px rgba(0, 0, 0, 0.04);
}
.google-auth-head .google-title {
font-weight: 500;
}
.google-subtitle {
color: #5f6368;
font-size: 0.95rem;
margin: 0;
}
.google-logo {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.55rem;
margin-bottom: 0.75rem;
}
.qr-logo-icon {
width: 64px;
height: 64px;
border-radius: 16px;
padding: 0.5rem;
background: linear-gradient(145deg, rgba(27, 127, 92, 0.12), rgba(15, 81, 50, 0.08));
border: 1px solid rgba(27, 127, 92, 0.3);
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.qr-logo-icon svg {
width: 100%;
height: 100%;
}
.google-logo-text {
font-weight: 600;
font-size: 0.9rem;
color: #5f6368;
}
.google-form {
display: flex;
flex-direction: column;
}
.google-field {
position: relative;
margin-bottom: 1.5rem;
transition: transform 0.15s ease;
}
.google-field:focus-within {
transform: translateY(-2px);
}
.google-input {
width: 100%;
border: 1px solid #dadce0;
border-radius: 10px;
padding: 1.4rem 3rem 0.6rem 1rem;
background: transparent;
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}
.google-input:focus {
border-color: #1a73e8;
box-shadow: 0 6px 18px rgba(26, 115, 232, 0.15);
background: #fff;
outline: none;
}
.google-input:not(:placeholder-shown) + .google-label,
.google-input:focus + .google-label {
top: 0.55rem;
font-size: 0.75rem;
color: #1a73e8;
}
.google-label {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.95rem;
color: #5f6368;
transition: all 0.15s ease;
pointer-events: none;
background: #fff;
padding: 0 0.25rem;
}
.google-toggle {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
border: none;
background: transparent;
color: #1a73e8;
font-weight: 600;
font-size: 0.9rem;
padding: 0;
cursor: pointer;
transition: color 0.15s ease;
}
.google-toggle:hover {
color: #174ea6;
}
.google-links {
display: flex;
justify-content: flex-end;
font-size: 0.9rem;
margin-bottom: 2rem;
}
.google-links a {
color: #1a73e8;
text-decoration: none;
font-weight: 600;
}
.google-links a:hover {
text-decoration: underline;
}
.google-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.google-alt-action {
color: #1a73e8;
text-decoration: none;
font-weight: 600;
padding-left: 0;
padding-right: 0;
transition: color 0.15s ease;
}
.google-alt-action:hover {
color: #174ea6;
}
.google-primary-action {
min-width: 110px;
border-radius: 50px;
font-weight: 600;
background-color: #1a73e8;
border-color: #1a73e8;
box-shadow: 0 6px 18px rgba(26, 115, 232, 0.25);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.google-primary-action:hover {
background-color: #1668d8;
border-color: #1668d8;
transform: translateY(-1px);
box-shadow: 0 8px 24px rgba(26, 115, 232, 0.3);
}
.google-footnote {
font-size: 0.85rem;
margin: 0;
}
@media (max-width: 575.98px) {
.google-auth-card {
padding: 2rem;
border-radius: 12px;
}
}
.app-shell {
display: flex;
min-height: 100vh;
background-color: #f5f7fb;
}
.sidebar {
width: 260px;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
flex-shrink: 0;
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
transition: all 0.25s ease;
}
.sidebar-brand {
display: flex;
align-items: center;
gap: 0.75rem;
}
.sidebar-icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
font-weight: 700;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sidebar-link {
border-radius: 0.75rem;
padding: 0.65rem 0.85rem;
color: #495057;
text-decoration: none;
transition: all 0.15s ease;
}
.sidebar-link:hover {
background-color: rgba(27, 127, 92, 0.1);
color: var(--brand-primary);
}
.sidebar-link.active {
background-color: var(--brand-primary);
color: #fff;
}
.sidebar-user {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
border-radius: 0.75rem;
background-color: var(--brand-muted);
border: 1px solid rgba(27, 127, 92, 0.15);
}
.content-area {
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
transition: margin-left 0.25s ease;
}
.content-body {
flex-grow: 1;
}
.camera-viewer {
min-height: 240px;
border: 1px dashed rgba(0, 0, 0, 0.15);
display: grid;
place-items: center;
overflow: hidden;
}
body.sidebar-collapsed .sidebar {
width: 0;
padding: 0;
margin: 0;
opacity: 0;
pointer-events: none;
}
body.sidebar-collapsed .content-area {
margin-left: 0;
}
.sidebar-toggle {
border-radius: 999px;
width: 34px;
height: 34px;
display: grid;
place-items: center;
padding: 0;
}
.navbar-brand {
color: var(--brand-primary) !important;
}
.stat-card {
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 1rem;
background: #fff;
padding: 1.5rem;
min-height: 130px;
}
.stat-card h6 {
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.75rem;
color: #6c757d;
margin-bottom: 0.5rem;
}
.stat-card .stat-value {
font-size: 2.25rem;
font-weight: 700;
color: var(--brand-primary);
margin: 0;
}
.card {
border-radius: 1rem;
border-color: rgba(0, 0, 0, 0.05);
}
.table thead {
background-color: var(--brand-muted);
}
.badge-status {
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.05em;
}
.scan-panel {
background: linear-gradient(135deg, var(--brand-primary), var(--brand-secondary));
color: #fff;
border-radius: 1.25rem;
padding: 2rem;
box-shadow: 0 20px 40px rgba(27, 127, 92, 0.25);
}
.scan-panel h2 {
font-weight: 700;
margin-bottom: 1rem;
}
.scan-panel .form-control,
.scan-panel .form-select {
border-radius: 0.75rem;
}
.qr-preview {
border: 1px dashed rgba(0, 0, 0, 0.2);
border-radius: 1rem;
padding: 1rem;
background: #fff;
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.35rem 0.85rem;
border-radius: 999px;
font-size: 0.75rem;
background-color: var(--brand-muted);
color: #1f5137;
}
.text-muted-sm {
font-size: 0.9rem;
color: #868e96 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 KiB

175
src/assets/js/admin.js Normal file
View File

@@ -0,0 +1,175 @@
(() => {
document.addEventListener('DOMContentLoaded', () => {
['studentModal', 'activityModal', 'courseModal', 'departmentModal', 'userModal']
.forEach(setupRecordModal);
setupSidebarToggle();
setupQrModal('qrModal');
['deleteStudentModal', 'deleteActivityModal', 'deleteCourseModal', 'deleteDepartmentModal', 'toggleUserModal', 'resetPasswordModal']
.forEach(setupDeleteModal);
});
function setupRecordModal(id) {
const modalEl = document.getElementById(id);
if (!modalEl) return;
const createTitle = modalEl.dataset.createTitle || 'Add Record';
const editTitle = modalEl.dataset.editTitle || 'Edit Record';
const createSubmit = modalEl.dataset.createSubmit || 'Add Record';
const editSubmit = modalEl.dataset.editSubmit || 'Save Changes';
modalEl.addEventListener('show.bs.modal', (event) => {
const button = event.relatedTarget;
const mode = button && button.dataset ? button.dataset.mode || 'create' : 'create';
const form = modalEl.querySelector('form');
if (!form) return;
const hasRecordPayload = Boolean(button && button.dataset && button.dataset.record);
const isEdit = mode === 'edit' || hasRecordPayload;
form.reset();
form.querySelector('[name="action"]').value = isEdit ? 'update' : 'create';
form.querySelector('[name="id"]').value = '';
const title = modalEl.querySelector('[data-modal-title]');
const submit = modalEl.querySelector('[type="submit"]');
if (hasRecordPayload) {
try {
const data = JSON.parse(atob(button.dataset.record));
Object.entries(data).forEach(([key, value]) => {
const field = form.querySelector(`[name="${key}"]`);
if (!field) return;
field.value = value ?? '';
});
form.querySelector('[name="id"]').value = data.id ?? '';
} catch (error) {
console.error('Failed to parse record payload', error);
}
}
if (title) {
title.textContent = isEdit ? editTitle : createTitle;
}
if (submit) {
submit.textContent = isEdit ? editSubmit : createSubmit;
}
});
}
function setupSidebarToggle() {
const toggleButtons = document.querySelectorAll('[data-sidebar-toggle]');
if (!toggleButtons.length) return;
const storageKey = 'sidebarCollapsed';
if (localStorage.getItem(storageKey) === '1') {
document.body.classList.add('sidebar-collapsed');
}
toggleButtons.forEach((button) => {
button.addEventListener('click', () => {
document.body.classList.toggle('sidebar-collapsed');
const collapsed = document.body.classList.contains('sidebar-collapsed');
localStorage.setItem(storageKey, collapsed ? '1' : '0');
});
});
}
function setupQrModal(id) {
const modalEl = document.getElementById(id);
if (!modalEl) return;
const generator = modalEl.getAttribute('data-generator') || '';
const nameEl = modalEl.querySelector('[data-qr-name]');
const idEl = modalEl.querySelector('[data-qr-id]');
const tokenEl = modalEl.querySelector('[data-qr-token]');
const imageEl = modalEl.querySelector('[data-qr-image]');
const downloadEl = modalEl.querySelector('[data-qr-download]');
modalEl.addEventListener('show.bs.modal', (event) => {
const trigger = event.relatedTarget;
if (!trigger || !trigger.dataset) return;
const token = trigger.dataset.qrToken || '';
const studentId = trigger.dataset.studentId || '';
const studentName = trigger.dataset.studentName || 'Student';
if (nameEl) nameEl.textContent = studentName;
if (idEl) idEl.textContent = `ID: ${studentId || '—'}`;
if (tokenEl) tokenEl.textContent = token;
if (imageEl) {
if (generator && token) {
imageEl.src = `${generator}?token=${encodeURIComponent(token)}&v=${Date.now()}`;
imageEl.alt = `QR code for ${studentName}`;
} else {
imageEl.removeAttribute('src');
imageEl.alt = 'QR code unavailable';
}
}
if (downloadEl) {
const canDownload = Boolean(generator && token);
if (canDownload) {
downloadEl.href = `${generator}?token=${encodeURIComponent(token)}&download=1`;
downloadEl.setAttribute('download', `qr-${studentId || token}.png`);
downloadEl.classList.remove('disabled');
downloadEl.removeAttribute('aria-disabled');
} else {
downloadEl.href = '#';
downloadEl.setAttribute('aria-disabled', 'true');
downloadEl.classList.add('disabled');
}
}
});
}
function setupDeleteModal(id) {
const modalEl = document.getElementById(id);
if (!modalEl) return;
const nameEl = modalEl.querySelector('[data-delete-name]');
const idEl = modalEl.querySelector('[data-delete-id]');
const messageEl = modalEl.querySelector('[data-delete-message]');
const confirmButton = modalEl.querySelector('[data-delete-confirm]');
const hiddenId = modalEl.querySelector('input[name="id"]');
const defaultNameText = nameEl ? nameEl.textContent : 'Record Name';
const defaultIdText = idEl ? idEl.textContent : '';
const defaultMessageText = messageEl ? messageEl.textContent : '';
const defaultConfirmText = confirmButton ? confirmButton.textContent : '';
modalEl.addEventListener('show.bs.modal', (event) => {
const trigger = event.relatedTarget;
if (!trigger || !trigger.dataset) return;
const recordId = trigger.dataset.id || '';
const recordName = trigger.dataset.recordName || trigger.dataset.studentName || 'Record';
const recordRef = trigger.dataset.recordRef || trigger.dataset.studentId || '';
if (hiddenId) hiddenId.value = recordId;
if (nameEl) nameEl.textContent = recordName || defaultNameText;
if (idEl) {
if (recordRef) {
const label = idEl.dataset.label || '';
idEl.textContent = label ? `${label}: ${recordRef}` : recordRef;
} else {
idEl.textContent = defaultIdText;
}
}
if (messageEl) {
const confirmText = trigger.dataset.confirmText || '';
messageEl.textContent = confirmText || defaultMessageText;
}
if (confirmButton) {
const confirmLabel = trigger.dataset.confirmLabel || '';
confirmButton.textContent = confirmLabel || defaultConfirmText;
}
});
modalEl.addEventListener('hidden.bs.modal', () => {
if (hiddenId) hiddenId.value = '';
if (nameEl) nameEl.textContent = defaultNameText;
if (idEl) idEl.textContent = defaultIdText;
if (messageEl) messageEl.textContent = defaultMessageText;
if (confirmButton) confirmButton.textContent = defaultConfirmText;
});
}
})();

223
src/assets/js/scan.js Normal file
View File

@@ -0,0 +1,223 @@
(() => {
document.addEventListener('DOMContentLoaded', () => {
const scanForm = document.querySelector('[data-scan-form]');
if (scanForm) {
const endpoint = scanForm.dataset.endpoint;
const qrInput = scanForm.querySelector('input[name="qr_token"]');
const resultBox = document.querySelector('#scan-result');
const statusBox = document.querySelector('#scan-status');
const submitBtn = scanForm.querySelector('button[type="submit"]');
const setStatus = (message, classes = 'text-white fw-semibold') => {
if (!statusBox) return;
statusBox.textContent = message;
statusBox.className = classes;
};
scanForm.addEventListener('submit', async (event) => {
event.preventDefault();
if (!endpoint) return;
const payload = Object.fromEntries(new FormData(scanForm));
submitBtn.disabled = true;
setStatus('Processing scan…');
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
});
const data = await response.json();
setStatus(
data.message || 'No response message',
data.success ? 'text-success fw-semibold' : 'text-danger fw-semibold'
);
if (data.meta && resultBox) {
resultBox.innerHTML = `
<div class="list-group">
<div class="list-group-item">
<strong>Student:</strong> ${data.meta.student ?? '—'}
</div>
<div class="list-group-item">
<strong>Activity:</strong> ${data.meta.activity ?? '—'}
</div>
<div class="list-group-item">
<strong>Action:</strong> ${data.meta.action ?? '—'}
</div>
<div class="list-group-item">
<strong>Recorded:</strong> ${data.meta.timestamp ?? '—'}
</div>
</div>
`;
}
qrInput.value = '';
qrInput.focus();
} catch (error) {
console.error(error);
setStatus('Unable to process scan. Please try again.', 'text-danger fw-semibold');
} finally {
submitBtn.disabled = false;
}
});
setupCameraScanner(scanForm, qrInput, setStatus);
}
});
function setupCameraScanner(form, qrInput, setStatus) {
const cameraRegion = document.querySelector('#camera-viewer');
const startButton = document.querySelector('[data-scan-start]');
if (!cameraRegion || !startButton) return;
let html5Scanner = null;
let fallbackController = null;
startButton.addEventListener('click', async () => {
if (startButton.dataset.state === 'running') {
await stopScanner();
return;
}
startButton.disabled = true;
setStatus('Initializing camera scanner…');
const started = await startScanner();
startButton.disabled = false;
if (!started) {
startButton.classList.add('disabled');
cameraRegion.innerHTML = '<div class="text-danger text-center small">Camera scan unavailable in this browser.</div>';
setStatus('Camera scan unavailable. Please type QR token manually.', 'text-warning fw-semibold');
}
});
async function startScanner() {
if (window.Html5Qrcode) {
return startHtml5Scanner();
}
if ('BarcodeDetector' in window) {
fallbackController = await startFallbackScanner();
if (fallbackController) {
startButton.dataset.state = 'running';
startButton.textContent = 'Stop Camera Scan';
setStatus('Camera scanner ready (fallback mode).');
return true;
}
}
return false;
}
async function startHtml5Scanner() {
try {
if (!html5Scanner) {
html5Scanner = new Html5Qrcode(cameraRegion.id);
}
cameraRegion.innerHTML = '';
await html5Scanner.start(
{ facingMode: 'environment' },
{ fps: 10, qrbox: 240 },
(decodedText) => {
if (!decodedText) {
return;
}
qrInput.value = decodedText.trim();
form.requestSubmit();
stopScanner();
},
() => {}
);
startButton.dataset.state = 'running';
startButton.textContent = 'Stop Camera Scan';
setStatus('Camera scanner ready (high fidelity).');
return true;
} catch (error) {
console.error(error);
await stopScanner();
setStatus('Camera scanner could not start. Trying fallback…', 'text-warning fw-semibold');
return false;
}
}
async function stopScanner() {
if (html5Scanner) {
await html5Scanner.stop().catch(() => {});
html5Scanner.clear();
html5Scanner = null;
}
if (fallbackController) {
fallbackController.stop();
fallbackController = null;
}
startButton.dataset.state = '';
startButton.textContent = 'Start Camera Scan';
cameraRegion.innerHTML = '<div class="text-center text-muted small">Camera idle</div>';
startButton.classList.remove('disabled');
setStatus('Camera idle. Ready for manual scans.', 'text-white-50 fw-semibold');
}
async function startFallbackScanner() {
let stream;
let detector;
let detectionTimer;
const video = document.createElement('video');
video.className = 'w-100 rounded';
video.setAttribute('playsinline', 'true');
video.muted = true;
cameraRegion.innerHTML = '';
cameraRegion.appendChild(video);
try {
detector = new BarcodeDetector({ formats: ['qr_code'] });
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
video.srcObject = stream;
await video.play();
const scanFrame = async () => {
if (!detector) return;
try {
const codes = await detector.detect(video);
if (codes.length > 0) {
qrInput.value = codes[0].rawValue.trim();
form.requestSubmit();
stop();
return;
}
} catch (error) {
console.error(error);
}
detectionTimer = requestAnimationFrame(scanFrame);
};
detectionTimer = requestAnimationFrame(scanFrame);
} catch (error) {
console.error(error);
stop();
setStatus('Fallback scanner unavailable. Please allow camera access.', 'text-warning fw-semibold');
return null;
}
function stop() {
if (detectionTimer) {
cancelAnimationFrame(detectionTimer);
detectionTimer = null;
}
if (stream) {
stream.getTracks().forEach((track) => track.stop());
stream = null;
}
if (video.srcObject) {
video.pause();
video.srcObject = null;
}
detector = null;
}
return { stop };
}
}
})();

122
src/auth/login.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
if (is_logged_in()) {
redirect('admin/dashboard.php');
}
$appName = config('app.name', 'QR Attendance');
$supportEmail = config('app.support_email', 'support@campus.local');
$supportLink = 'mailto:' . $supportEmail;
$errors = [];
if (is_post()) {
if (!validate_csrf_token($_POST['csrf_token'] ?? null)) {
$errors[] = 'Invalid session token. Please refresh.';
} else {
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
if ($username === '' || $password === '') {
$errors[] = 'Username and password are required.';
} elseif (!attempt_login($username, $password)) {
$errors[] = 'Invalid credentials.';
} else {
add_flash('success', 'Welcome back!');
redirect('admin/dashboard.php');
}
}
}
render_header('Sign in', ['hide_nav' => true]);
?>
<section class="google-auth-shell d-flex flex-column align-items-center justify-content-center gap-4 w-100">
<div class="text-center">
<span class="badge text-bg-light px-3 py-2 text-uppercase fw-semibold">Admin Portal</span>
<p class="text-muted mb-0">Use your institution-issued credentials to continue.</p>
</div>
<div class="google-auth-card card shadow-sm border-0">
<div class="text-center mb-4 google-auth-head">
<div class="google-logo" aria-label="<?= sanitize($appName) ?>">
<span class="qr-logo-icon" aria-hidden="true">
<svg viewBox="0 0 64 64" role="presentation" focusable="false">
<rect x="2" y="2" width="20" height="20" rx="4" fill="#0f5132"></rect>
<rect x="42" y="2" width="20" height="20" rx="4" fill="#1b7f5c"></rect>
<rect x="2" y="42" width="20" height="20" rx="4" fill="#146946"></rect>
<rect x="26" y="26" width="12" height="12" rx="2" fill="#0f5132"></rect>
<rect x="44" y="36" width="12" height="12" rx="2" fill="#1b7f5c"></rect>
<rect x="32" y="48" width="8" height="8" rx="2" fill="#146946"></rect>
<rect x="24" y="8" width="8" height="8" rx="2" fill="#1b7f5c"></rect>
<rect x="8" y="32" width="8" height="8" rx="2" fill="#0f5132"></rect>
</svg>
</span>
<span class="google-logo-text"><?= sanitize($appName) ?></span>
</div>
<h1 class="google-title mb-1">Sign in</h1>
<p class="google-subtitle">Use your administrator account</p>
</div>
<?php if ($errors): ?>
<div class="alert alert-danger mb-4">
<?php foreach ($errors as $error): ?>
<div><?= sanitize($error) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post" novalidate class="google-form">
<input type="hidden" name="csrf_token" value="<?= sanitize(ensure_csrf_token()) ?>">
<div class="google-field">
<input type="text"
name="username"
id="username"
class="google-input"
placeholder=" "
value="<?= sanitize($_POST['username'] ?? '') ?>"
autocomplete="username"
required>
<label for="username" class="google-label">Email or username</label>
</div>
<div class="google-field">
<input type="password"
name="password"
id="password"
class="google-input"
placeholder=" "
autocomplete="current-password"
required
data-password-input>
<label for="password" class="google-label">Password</label>
<button type="button" class="google-toggle" data-password-toggle>Show</button>
</div>
<div class="google-links">
<a href="<?= sanitize($supportLink) ?>">Need help?</a>
</div>
<div class="google-actions">
<button type="button" class="btn btn-link google-alt-action px-0">Create account</button>
<button type="submit" class="btn btn-primary google-primary-action">
Next
</button>
</div>
</form>
<p class="google-footnote text-muted text-center">
This console is managed by <?= sanitize($appName) ?> IT. By continuing, you agree to internal security policies.
</p>
</div>
</section>
<?php
?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const input = document.querySelector('[data-password-input]');
const toggle = document.querySelector('[data-password-toggle]');
if (!input || !toggle) return;
toggle.addEventListener('click', () => {
const isHidden = input.type === 'password';
input.type = isHidden ? 'text' : 'password';
toggle.textContent = isHidden ? 'Hide' : 'Show';
});
});
</script>
<?php
render_footer(['extra_js' => []]);

6
src/auth/logout.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require __DIR__ . '/../includes/bootstrap.php';
logout_user();
add_flash('success', 'You have been signed out.');
redirect('auth/login.php');

22
src/config/config.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
return [
'app' => [
'name' => getenv('APP_NAME') ?: 'QR Attendance System',
'timezone' => getenv('APP_TZ') ?: 'Asia/Manila',
'base_url' => rtrim(getenv('APP_BASE_URL') ?: '', '/'),
],
'db' => [
'host' => getenv('DB_HOST') ?: '127.0.0.1',
'port' => getenv('DB_PORT') ?: '3306',
'name' => getenv('DB_NAME') ?: 'attendance_system',
'user' => getenv('DB_USER') ?: 'root',
'pass' => getenv('DB_PASS') ?: '',
'charset' => 'utf8mb4',
],
'qr' => [
'provider' => strtolower(getenv('QR_PROVIDER') ?: 'auto'),
'goqr_size' => getenv('QR_SIZE') ?: '300x300',
],
];

BIN
src/d4c.zip Normal file

Binary file not shown.

73
src/includes/auth.php Normal file
View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
function current_user(): ?array
{
return $_SESSION['user'] ?? null;
}
function is_logged_in(): bool
{
return current_user() !== null;
}
function require_login(): void
{
if (!is_logged_in()) {
redirect('auth/login.php');
}
}
function require_admin(): void
{
$user = current_user();
if (!$user || $user['role'] !== 'admin') {
http_response_code(403);
echo 'Forbidden';
exit;
}
}
function attempt_login(string $username, string $password): bool
{
$pdo = db();
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND status = 1 LIMIT 1');
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if (!$user) {
return false;
}
if (!password_verify($password, $user['password'])) {
return false;
}
$_SESSION['user'] = [
'id' => $user['id'],
'username' => $user['username'],
'full_name' => $user['full_name'],
'role' => $user['role'],
'email' => $user['email'],
];
return true;
}
function logout_user(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/auth.php';
require_once __DIR__ . '/layout.php';
require_once __DIR__ . '/setup.php';
date_default_timezone_set(config('app.timezone', 'UTC'));
ensure_default_admin();

30
src/includes/db.php Normal file
View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
function db(): PDO
{
static $pdo;
if ($pdo === null) {
$config = config('db');
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
$config['host'],
$config['port'],
$config['name'],
$config['charset']
);
$pdo = new PDO(
$dsn,
$config['user'],
$config['pass'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
}
return $pdo;
}

219
src/includes/helpers.php Normal file
View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
function app_config(): array
{
static $config;
if ($config === null) {
$configPath = __DIR__ . '/../config/config.php';
if (!file_exists($configPath)) {
throw new RuntimeException('Config file missing.');
}
$config = require $configPath;
}
return $config;
}
function config(string $key, mixed $default = null): mixed
{
$config = app_config();
$segments = explode('.', $key);
$value = $config;
foreach ($segments as $segment) {
if (!is_array($value) || !array_key_exists($segment, $value)) {
return $default;
}
$value = $value[$segment];
}
return $value;
}
function url_for(string $path = ''): string
{
$base = resolve_base_url();
$path = '/' . ltrim($path, '/');
if ($base === '' || $base === '/') {
return $path;
}
return rtrim($base, '/') . $path;
}
function asset_url(string $path): string
{
return url_for('assets/' . ltrim($path, '/'));
}
function resolve_base_url(): string
{
static $base = null;
if ($base !== null) {
return $base;
}
$configured = config('app.base_url', '');
if ($configured !== '') {
if (!preg_match('#^https?://#i', $configured)) {
$configured = '/' . ltrim($configured, '/');
}
$base = rtrim($configured, '/') ?: '/';
return $base;
}
$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
$documentRoot = $documentRoot ? rtrim(normalize_path($documentRoot), '/') : '';
$appRoot = rtrim(normalize_path(dirname(__DIR__)), '/');
if ($documentRoot && strpos($appRoot, $documentRoot) === 0) {
$relative = substr($appRoot, strlen($documentRoot));
$relative = trim($relative, '/');
$base = $relative === '' ? '' : '/' . $relative;
} else {
$base = '';
}
return $base;
}
function normalize_path(string $path): string
{
$real = realpath($path);
return str_replace('\\', '/', $real ?: $path);
}
function redirect(string $path): void
{
if (preg_match('#^https?://#i', $path)) {
$target = $path;
} else {
$target = url_for($path);
}
header('Location: ' . $target);
exit;
}
function sanitize(?string $value, int $flags = ENT_QUOTES): string
{
return htmlspecialchars((string) $value, $flags, 'UTF-8');
}
function add_flash(string $type, string $message): void
{
$_SESSION['flash'][$type][] = $message;
}
function get_flashes(): array
{
$messages = $_SESSION['flash'] ?? [];
unset($_SESSION['flash']);
return $messages;
}
function is_post(): bool
{
return strtoupper($_SERVER['REQUEST_METHOD'] ?? '') === 'POST';
}
function random_token(int $bytes = 8): string
{
return bin2hex(random_bytes($bytes));
}
function format_datetime(?string $value, string $format = 'M d, Y h:i A'): string
{
if (!$value) {
return '—';
}
$date = new DateTime($value);
return $date->format($format);
}
function format_date(?string $value, string $format = 'M d, Y'): string
{
if (!$value) {
return '—';
}
$date = new DateTime($value);
return $date->format($format);
}
function selected(mixed $current, mixed $value): string
{
return (string) $current === (string) $value ? 'selected' : '';
}
function checked(bool $condition): string
{
return $condition ? 'checked' : '';
}
function paginate(PDO $pdo, string $countSql, array $params = [], int $perPage = 10): array
{
$page = max(1, (int) ($_GET['page'] ?? 1));
$stmt = $pdo->prepare($countSql);
$stmt->execute($params);
$total = (int) $stmt->fetchColumn();
$pages = max(1, (int) ceil($total / $perPage));
$page = min($page, $pages);
$offset = ($page - 1) * $perPage;
return [
'page' => $page,
'per_page' => $perPage,
'total' => $total,
'pages' => $pages,
'offset' => $offset,
];
}
function render_pagination(array $pagination, string $basePath = '', array $query = []): void
{
if (($pagination['pages'] ?? 1) <= 1) {
return;
}
$base = $basePath ?: parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
?>
<nav aria-label="Pagination" class="mt-4">
<ul class="pagination">
<?php for ($i = 1; $i <= $pagination['pages']; $i++): ?>
<?php
$queryString = http_build_query(array_merge($query, ['page' => $i]));
$href = $base . '?' . $queryString;
?>
<li class="page-item <?= $pagination['page'] === $i ? 'active' : '' ?>">
<a class="page-link" href="<?= sanitize($href) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php
}
function encode_record(array $record): string
{
return base64_encode(json_encode($record, JSON_UNESCAPED_UNICODE));
}
function ensure_csrf_token(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = random_token(16);
}
return $_SESSION['csrf_token'];
}
function validate_csrf_token(?string $token): bool
{
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], (string) $token);
}

171
src/includes/layout.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
function render_header(string $title, array $options = []): void
{
$active = $options['active'] ?? '';
$hideNav = $options['hide_nav'] ?? false;
$extraCss = $options['extra_css'] ?? [];
$appName = config('app.name', 'QR Attendance');
$user = current_user();
$flashes = get_flashes();
$GLOBALS['__layout_hide_nav'] = $hideNav;
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= sanitize($title) ?> | <?= sanitize($appName) ?></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="<?= asset_url('css/style.css') ?>">
<?php foreach ($extraCss as $css): ?>
<link rel="stylesheet" href="<?= sanitize($css) ?>">
<?php endforeach; ?>
</head>
<body>
<?php if ($hideNav): ?>
<main class="auth-shell container py-5">
<?php else: ?>
<div class="app-shell">
<aside class="sidebar bg-white border-end">
<div class="sidebar-brand mb-4">
<div class="sidebar-icon bg-primary-subtle text-primary">QR</div>
<div>
<div class="fw-semibold text-primary">QR Code Attendance</div>
<small class="text-muted">Admin Console</small>
</div>
</div>
<nav class="sidebar-nav flex-grow-1">
<?php foreach (nav_items() as $item): ?>
<?php if (!$item['show']): continue; endif; ?>
<a class="sidebar-link <?= $active === $item['key'] ? 'active' : '' ?>"
href="<?= url_for($item['path']) ?>">
<?= sanitize($item['label']) ?>
</a>
<?php endforeach; ?>
</nav>
<?php if ($user): ?>
<div class="sidebar-user mt-4">
<div>
<div class="fw-semibold"><?= sanitize($user['full_name']) ?></div>
<small class="text-muted text-uppercase"><?= sanitize($user['role']) ?></small>
</div>
<a href="<?= url_for('auth/logout.php') ?>" class="btn btn-sm btn-outline-primary ms-auto">
Logout
</a>
</div>
<?php endif; ?>
</aside>
<div class="content-area flex-grow-1">
<header class="content-topbar bg-white border-bottom d-flex justify-content-between align-items-center px-4 py-3">
<div class="d-flex align-items-center gap-3">
<button class="btn btn-outline-secondary btn-sm sidebar-toggle" type="button" data-sidebar-toggle>
<span class="sidebar-toggle-icon">&#9776;</span>
</button>
<div class="text-muted small">QR Attendance System</div>
<h1 class="h5 mb-0 mb-0"><?= sanitize($title) ?></h1>
</div>
<span class="badge text-bg-light"><?= date('D, M j') ?></span>
</header>
<main class="content-body px-4 py-4">
<?php endif; ?>
<?php foreach ($flashes as $type => $messages): ?>
<?php foreach ($messages as $message): ?>
<div class="alert alert-<?= sanitize($type) ?> alert-dismissible fade show" role="alert">
<?= sanitize($message) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endforeach; ?>
<?php endforeach; ?>
<?php
}
function render_footer(array $options = []): void
{
$extraJs = $options['extra_js'] ?? [];
$hideNav = $GLOBALS['__layout_hide_nav'] ?? false;
?>
<?php if ($hideNav): ?>
</main>
<?php else: ?>
</main>
</div>
</div>
<?php endif; ?>
<?php foreach ($extraJs as $script): ?>
<script src="<?= sanitize($script) ?>"></script>
<?php endforeach; ?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="<?= asset_url('js/admin.js') ?>" defer></script>
<script src="<?= asset_url('js/scan.js') ?>" defer></script>
</body>
</html>
<?php
}
function nav_items(): array
{
$user = current_user();
$isAdmin = $user && $user['role'] === 'admin';
return [
[
'key' => 'dashboard',
'label' => 'Dashboard',
'path' => 'admin/dashboard.php',
'show' => (bool) $user,
],
[
'key' => 'students',
'label' => 'Students',
'path' => 'admin/students.php',
'show' => (bool) $user,
],
[
'key' => 'activities',
'label' => 'Activities',
'path' => 'admin/activities.php',
'show' => (bool) $user,
],
[
'key' => 'courses',
'label' => 'Courses',
'path' => 'admin/courses.php',
'show' => (bool) $user,
],
[
'key' => 'departments',
'label' => 'Departments',
'path' => 'admin/departments.php',
'show' => (bool) $user,
],
[
'key' => 'attendance',
'label' => 'Scan Center',
'path' => 'admin/attendance.php',
'show' => $isAdmin,
],
[
'key' => 'reports',
'label' => 'Reports',
'path' => 'admin/reports.php',
'show' => (bool) $user,
],
[
'key' => 'settings',
'label' => 'Settings',
'path' => 'admin/settings.php',
'show' => $isAdmin,
],
[
'key' => 'users',
'label' => 'Users',
'path' => 'admin/users.php',
'show' => $isAdmin,
],
];
}

View File

@@ -0,0 +1,38 @@
* 1.0.0 build 2010031920
- first public release
- help in readme, install
- cleanup ans separation of QRtools and QRspec
- now TCPDF binding requires minimal changes in TCPDF, having most of job
done in QRtools tcpdfBarcodeArray
- nicer QRtools::timeBenchmark output
- license and copyright notices in files
- indent cleanup - from tab to 4spc, keep it that way please :)
- sf project, repository, wiki
- simple code generator in index.php
* 1.1.0 build 2010032113
- added merge tool wich generate merged version of code
located in phpqrcode.php
- splited qrconst.php from qrlib.php
* 1.1.1 build 2010032405
- patch by Rick Seymour allowing saving PNG and displaying it at the same time
- added version info in VERSION file
- modified merge tool to include version info into generated file
- fixed e-mail in almost all head comments
* 1.1.2 build 2010032722
- full integration with TCPDF thanks to Nicola Asuni, it's author
- fixed bug with alphanumeric encoding detection
* 1.1.3 build 2010081807
- short opening tags replaced with standard ones
* 1.1.4 build 2010100721
- added missing static keyword QRinput::check (found by Luke Brookhart, Onjax LLC)

View File

@@ -0,0 +1,67 @@
== REQUIREMENTS ==
* PHP5
* PHP GD2 extension with JPEG and PNG support
== INSTALLATION ==
If you want to recreate cache by yourself make sure cache directory is
writable and you have permisions to write into it. Also make sure you are
able to read files in it if you have cache option enabled
== CONFIGURATION ==
Feel free to modify config constants in qrconfig.php file. Read about it in
provided comments and project wiki page (links in README file)
== QUICK START ==
Notice: probably you should'nt use all of this in same script :)
<?phpb
//include only that one, rest required files will be included from it
include "qrlib.php"
//write code into file, Error corection lecer is lowest, L (one form: L,M,Q,H)
//each code square will be 4x4 pixels (4x zoom)
//code will have 2 code squares white boundary around
QRcode::png('PHP QR Code :)', 'test.png', 'L', 4, 2);
//same as above but outputs file directly into browser (with appr. header etc.)
//all other settings are default
//WARNING! it should be FIRST and ONLY output generated by script, otherwise
//rest of output will land inside PNG binary, breaking it for sure
QRcode::png('PHP QR Code :)');
//show benchmark
QRtools::timeBenchmark();
//rebuild cache
QRtools::buildCache();
//code generated in text mode - as a binary table
//then displayed out as HTML using Unicode block building chars :)
$tab = $qr->encode('PHP QR Code :)');
QRspec::debug($tab, true);
== TCPDF INTEGRATION ==
Inside bindings/tcpdf you will find slightly modified 2dbarcodes.php.
Instal phpqrcode liblaty inside tcpdf folder, then overwrite (or merge)
2dbarcodes.php
Then use similar as example #50 from TCPDF examples:
<?php
$style = array(
'border' => true,
'padding' => 4,
'fgcolor' => array(0,0,0),
'bgcolor' => false, //array(255,255,255)
);
//code name: QR, specify error correction level after semicolon (L,M,Q,H)
$pdf->write2DBarcode('PHP QR Code :)', 'QR,L', '', '', 30, 30, $style, 'N');

View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser 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
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,45 @@
This is PHP implementation of QR Code 2-D barcode generator. It is pure-php
LGPL-licensed implementation based on C libqrencode by Kentaro Fukuchi.
== LICENSING ==
Copyright (C) 2010 by Dominik Dzienia
This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 3 of the License, or any later version.
This library 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 Lesser General Public License (LICENSE file)
for more details.
You should have received a copy of the GNU Lesser General Public License along
with this library; if not, write to the Free Software Foundation, Inc., 51
Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
== INSTALATION AND USAGE ==
* INSTALL file
* http://sourceforge.net/apps/mediawiki/phpqrcode/index.php?title=Main_Page
== CONTACT ==
Fell free to contact me via e-mail (deltalab at poczta dot fm) or using
folowing project pages:
* http://sourceforge.net/projects/phpqrcode/
* http://phpqrcode.sourceforge.net/
== ACKNOWLEDGMENTS ==
Based on C libqrencode library (ver. 3.1.1)
Copyright (C) 2006-2010 by Kentaro Fukuchi
http://megaui.net/fukuchi/works/qrencode/index.en.html
QR Code is registered trademarks of DENSO WAVE INCORPORATED in JAPAN and other
countries.
Reed-Solomon code encoder is written by Phil Karn, KA9Q.
Copyright (C) 2002, 2003, 2004, 2006 Phil Karn, KA9Q

View File

@@ -0,0 +1,2 @@
1.1.4
2010100721

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
<EFBFBD><EFBFBD>
<EFBFBD> E9<45>u<06><>`<60>"PńC<C584>T!0$

BIN
src/includes/phpqrcode/cache/frame_1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> E]s<>IX<49>;<3B><01>n6<6E><36>`<60>q<EFBFBD><71><EFBFBD>W6<57><36><EFBFBD><04>`<60>%A/3!<21><><EFBFBD><EFBFBD><EFBFBD>!g<><67>̡<EFBFBD>1N) <0B>E<EFBFBD><45>|;<3B><>>6⸏<36>97$<0E><><EFBFBD><EFBFBD>c]kk<6B><6B>w<EFBFBD>1<EFBFBD><31>[<5B>m<EFBFBD>C͜c<CD9C>R<><52><EFBFBD><EFBFBD>><3E><><1A><><EFBFBD>E,<2C>hʼnp<C589>#<1C>xF<1C>yW<79><57>VWG<57><47><EFBFBD>3<EFBFBD><33>+<2B><0F><><EFBFBD>˓<EFBFBD>S<EFBFBD><53><>#<1C>G8b^c^c<><63><11>p<EFBFBD>c&3YQ"<11><1B><><EFBFBD><EFBFBD>v<EFBFBD><76><11><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>k<EFBFBD>9<EFBFBD>܇<EFBFBD>}<7D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <09>Ŀ<EFBFBD>Q<><51>L<EFBFBD>/<2F><><EFBFBD><EFBFBD>

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,2 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD>0E]<5D>օ,2;s<><73>&<26>͚h<14><1D><>O<EFBFBD><4F><EFBFBD><7F><EFBFBD><EFBFBD>1&09OIv@DD<44> <0C>&<26>ىK<D989>X<EFBFBD><58>Fv<46><<11>dq<64>9<><%h<><68> Y<>s !(d<><64><EFBFBD>s;~||b(<28><>Yůg#<23>`<60>K<16><>S<EFBFBD><53><EFBFBD><EFBFBD>Ķ<EFBFBD><C4B6>s<1C>idߍLg:ә<>t<EFBFBD>/gm<67><6D><EFBFBD><EFBFBD>k<EFBFBD>M<>3<EFBFBD>{<7B>4rT<72>Q<EFBFBD><51>e<EFBFBD><65>s<EFBFBD>><3E><>t<EFBFBD>3<EFBFBD><33><EFBFBD>;<12>H<EFBFBD><>t<EFBFBD>3<EFBFBD><EFBFBD>Y<EFBFBD>+og<>h<EFBFBD><68><EFBFBD><EFBFBD>ٽ<EFBFBD>ln<6C><6E>F><3E>i^<5E>#awm;g<>~p<>g<EFBFBD>Ns{6z<36><7A><EFBFBD><EFBFBD><19><><EFBFBD><EFBFBD>p<><70>'

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,3 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD> E<><45><EFBFBD>.<2E>No<4E><37><D19B>iiR<>N2<4E><32>W%<25>x<04>@<40>ڜ<EFBFBD>'<27>
u<EFBFBD>6<EFBFBD><EFBFBD><EFBFBD>.<2E>*S;}<7D><><EFBFBD>à<EFBFBD>T <0B><><EFBFBD>zr<>t<><74>%<25>,<2C><><EFBFBD><EFBFBD><EFBFBD>}<7D>;<3B><><EFBFBD>)<29><><EFBFBD><EFBFBD><EFBFBD>Z<EFBFBD><5A>L<EFBFBD><4C><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><50>$<24><><EFBFBD><1E>q<EFBFBD>g<EFBFBD>L<EFBFBD><4C>dJ<64>;<3B><>w<><77><EFBFBD>.]z#<23><><EFBFBD><><CD9D>Og<4F><67><EFBFBD><EFBFBD>"<22><> <09>B<EFBFBD><17><>}<7D>}<7D>;<3B><>w<><77><1D><>#1Gb<47><62>;<3B><>w<><77><EFBFBD>_<EFBFBD>C+w<>@Df<44><04><><EFBFBD><EFBFBD>u<EFBFBD><75>2<EFBFBD><32><EFBFBD><EFBFBD>N<EFBFBD><4E>9R7|pW<70>k<EFBFBD><6B><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>k<><6B><EFBFBD><07><><1C><><1C><>

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@@ -0,0 +1 @@
x<EFBFBD>͒<EFBFBD>

BIN
src/includes/phpqrcode/cache/frame_2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> E]s<>IX<49>;<3B><01>n6Up<55><13><>в<EFBFBD><D0B2><>< i-eW<65><57><EFBFBD><EFBFBD>)<29><>ŕ<EFBFBD><C595>…H\jvq<76>HL\6<><36><EFBFBD>ЅrI<72><06><4C><DCB9>%<25><18>@<40><><EFBFBD>V<EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>(<28>P4|<7C>Xn<58>gɝ<><15>~]D<><44><EFBFBD><EFBFBD>u1Us S\<5C><16><>,<2C><>2<EFBFBD><1F>N<EFBFBD><4E>?D<>K<EFBFBD><4B>F-:<3A>eJ]p_<70><16><>,<2C>a0<61>`<60><><EFBFBD> X<><16>`<60><> <0C>w,` X<>]<5D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>5 <0B><>Y4{<7B><><EFBFBD><EFBFBD>2<EFBFBD><32><EFBFBD>v<EFBFBD>Js<4A><73><EFBFBD><EFBFBD>9<EFBFBD><39><EFBFBD>)<29>u<EFBFBD>۹<EFBFBD><DBB9><EFBFBD>,<17>]<5D><><EFBFBD><EFBFBD>^_<>7$<24>_<EFBFBD>

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -0,0 +1,3 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD>0 E]{<7B><>.<2E>]{{{<7B><>Z<EFBFBD>Bep<65><06>we@<1F>V<EFBFBD>ERZ3<5A><33>"*2o<32>4<EFBFBD>y<EFBFBD>)i#d<>bdF҅<46><12>I"<22><><14>4<EFBFBD><34>W<17>I<EFBFBD>u<EFBFBD><75>45<34>x<EFBFBD>.Z<>S<EFBFBD>{<7B><><EFBFBD>8<EFBFBD><38><07>k={o.<2E>q<EFBFBD><71><01>[<13><>:帒q<E5B892><71><EFBFBD>y
)t#<23><>N8<4E>dCj<43>-O<>OG}<7D>:/<2F>:s<>z!<21>)^<<3C>e<EFBFBD><65>S<EFBFBD>u<EFBFBD>{<7B> '<27>p<EFBFBD> '<27>=<3D>=<3D>=<3D>'<27>p<EFBFBD> '<27>p<EFBFBD>ߣߣ<DFA3><DFA3><1F>N8<4E><38><EFBFBD><EFBFBD>9<EFBFBD><39><EFBFBD><EFBFBD>pQQ<51>]H<19>pz<70><7A><EFBFBD>G<EFBFBD>^<5E><>Q<EFBFBD><51>I|<7C>߳<EFBFBD>u;9<><39><EFBFBD><EFBFBD><EFBFBD>d;<3B>X$<24><><EFBFBD><13>t<1E><><1B><>dy

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

View File

@@ -0,0 +1,3 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD> E<><45><EFBFBD>fo<>7ћU<D19B>) %M!Δ<><CE94>Yu(<<1E><><17>sK<73><4B>T<EFBFBD><54><EFBFBD>
<EFBFBD>&<26>I<>\i+<2B>Ъ<EFBFBD>(m<><6D>FQ<46><51><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD>v~n1<6E>o<>]s<><73><EFBFBD><EFBFBD><EFBFBD><18>3`<60>_w2<77>ȹ<EFBFBD>lc[<5B><>;<3B><12>c֟ˤ<D69F>N<EFBFBD><4E>4<EFBFBD>p<EFBFBD>

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> E<><45><EFBFBD>MX0;<3B><><EFBFBD>nVP4<50>HSS<19>x<EFBFBD>U3<55>/O<><4F> LiJ4<4A><34><EFBFBD>V<EFBFBD> JC<4A>%<25><>6VR&<16><>D<EFBFBD>B<EFBFBD>HjD<6A><44>J<0E>??<3F><><EFBFBD>Bl<42>cDZ<><C7B1><EFBFBD>'<27>U<EFBFBD><55>X<EFBFBD>U<EFBFBD>ޏ0<DE8F><30>yw<79>į<EFBFBD>j<EFBFBD><6A><EFBFBD><33><C59B><EFBFBD>cj<63><6A><EFBFBD>{<7B><><12>:Gq<>G<1C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><0E>N<EFBFBD>v;<3B><>笓J <0C><><EFBFBD><<3C><><EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>#<23>8<EFBFBD><38>#<23>8<EFBFBD>H'<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Gq<>G<1C><>tr:9<>#<23>8<EFBFBD><38>#<23>8<EFBFBD>ؓh<D893><68><15>N<EFBFBD>t<EFBFBD><74><EFBFBD><EFBFBD>_<EFBFBD><5F>>t<>e<EFBFBD><65>S<EFBFBD><53><EFBFBD><EFBFBD><EFBFBD><EFBFBD>^<5E>\g<><67><EFBFBD>Qe?<3F>vu<><75>o<EFBFBD><6F>;<3B><1A>><3E><>*<2A>wl<77><02>m<>

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -0,0 +1,3 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD> <10><><EFBFBD>s낋<73>]r<>x<13>Y51mM<>BG
<EFBFBD><EFBFBD>*Sx|Ua<35>Z<><5A><EFBFBD>-,<2C>1<EFBFBD><31>H<15>P<EFBFBD>Rj<52><6A>X5<58><35>i<EFBFBD><69><EFBFBD><EFBFBD>G<EFBFBD>>W<><57><EFBFBD>R<EFBFBD><52><EFBFBD>/<2F><>+uT廯<54> <0C>ӯ嗴<D3AF>u<EFBFBD><75><0E><>[S<>a<EFBFBD>[kv<6B><76>5<EFBFBD>+5n<1F><><EFBFBD>J<EFBFBD><4A>%+V<>X<EFBFBD><62>߬u'<27><07><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>SR<53><52><EFBFBD><EFBFBD>tzZ<7A><5A>+<2B>+V<>X<EFBFBD><62>ٟٟٟ<D99F><D99F>+V<>X<EFBFBD>b<EFBFBD><62><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>+V<>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>VI<56><49><EFBFBD><EFBFBD><15>+k<>q<>[<5B><>t<1E><>oVZ<56><5A>voNV<4E>w<1C>}<7D>{<7B>r<ýR<C3BD><52>"<22>R<EFBFBD><52>]<1D>

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -0,0 +1,2 @@
x<EFBFBD><EFBFBD><EFBFBD>A
<EFBFBD> E<><45>օ,t<>7<EFBFBD>7ћU<D19B> E)i7<><37>*~c<><63><EFBFBD><EFBFBD>X<14>EB<45><42><EFBFBD>FC<><43><EFBFBD>6<EFBFBD>:&<26>L,<2C><>Mv.<2E><><EFBFBD><EFBFBD>Kg<4B>ո<EFBFBD>YM<59>><3E><><EFBFBD>><3E><6D>?<3F><>v<><76><EFBFBD>mg?<3F><>ұ<EFBFBD><D2B1><EFBFBD><EFBFBD>η<1D>d<EFBFBD><64>C<><16>U<EFBFBD><55>Ik<49><6B><EFBFBD>E\<5C><>Ms<4D>f<>a<EFBFBD>f<>a><3E>[sӈ9쬩ެ8b<38><k<><6B>7<EFBFBD>}<7D><>k<><6B><EFBFBD><EFBFBD><EFBFBD><EFBFBD>3<EFBFBD>0<EFBFBD> 3<>0<EFBFBD> 3<><33>*r<15><>\<5C>7 f<>a<EFBFBD>f<>a<EFBFBD>fr<15><>\<5C>7 f<>a<EFBFBD>f<>a<EFBFBD>Y<EFBFBD><59><18><> <0C>d<EFBFBD>4<EFBFBD>9k<><6B><EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>X y<>g<EFBFBD><67><EFBFBD>)<1B><>dw<64><6E>U<EFBFBD>><3E><><EFBFBD>]<5D><>Lg<4C><67><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Eo<45> w1

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

@@ -0,0 +1,2 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> <10>a<EFBFBD>޺ <09><><EFBFBD><EFBFBD>@n7+*<2A><><EFBFBD><EFBFBD>4<EFBFBD>!<21>?<3F>J<EFBFBD><4A><EFBFBD> <09><><EFBFBD><EFBFBD>]<5D><1A><>S<EFBFBD><53>Tf)<29><>s<EFBFBD>I<EFBFBD>"<22>Ȕb<C894><62>0<EFBFBD><30>|<7C>"Luٸ<75>,<2C><>E<18>1\6<>*<2A>uQ<75>?<3F>>a<>υ<EFBFBD><CF85><EFBFBD><EFBFBD><EFBFBD>R<EFBFBD>-r<><72><EFBFBD>n.<2E>ꯋ\<5C>T<EFBFBD><54>:<3A>*)|)<29><> ,<2C><> ,<2C><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>x_<78><5F>}:^R<><52>U<6F>u<EFBFBD>~<7E>މX`<60>XЏЏЏЏ<D08F>_`<60>X`<60>XЏЏЏ<D08F>_`<60>X`<60>XЏЏЏЏ<D08F>wb<77>X`<60><16><>PU<><55>)D<><44>"c<>{<7B>z<EFBFBD><7A><EFBFBD>3<EFBFBD><33><EFBFBD><}<7D><><EFBFBD>^?b<>m<EFBFBD><6D><EFBFBD><EFBFBD><EC9E83><EFBFBD><EFBFBD><EFBFBD>a<EFBFBD><61><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.<2E>]
<EFBFBD>{Q6u<07>T,9

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD><EFBFBD>

BIN
src/includes/phpqrcode/cache/frame_3.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> <10>a<EFBFBD>޺ <0B><>

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

View File

@@ -0,0 +1,2 @@
x<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD> <14><>־<EFBFBD><D6BE><EFBFBD><EFBFBD>.<2E> <20>D<EFBFBD>l<EFBFBD>, <0C><>Mz<4D><7A>6<EFBFBD><10>Ç gcJ<63>D;<3B>'.<2E>A<EFBFBD>Iq<49>މ<EFBFBD>I,Ir<49>Y<EFBFBD><59><EFBFBD><EFBFBD>Fk%<25>D<EFBFBD>O<14>y|ED<45>D<EFBFBD><44>(L<>_Y<5F><59>>*ߚ?a<>O<EFBFBD><15>k<7F>L_<4C><[c<><63><EFBFBD><EFBFBD>><3E><63>u<1C>LI<4C><49>%<25>#<23>0<EFBFBD>#<23>0<EFBFBD>#<23><>otѢ<74><D1A2><EFBFBD>}<7D><>4<EFBFBD>f<EFBFBD>v_)<29><>E<EFBFBD>p<EFBFBD><1F><>h5R<35><52>8<EFBFBD>8<EFBFBD>1<EFBFBD>#<23>0<EFBFBD>#<23>0<EFBFBD><30><EFBFBD>i<EFBFBD><69>tZ<74>#<23>0<EFBFBD>#<23>0<EFBFBD>#<23>0<EFBFBD><30><EFBFBD>i<EFBFBD><69>tZ<74>#<23>0<EFBFBD>#<23>0<EFBFBD>#<23>0<EFBFBD><30><EFBFBD>i<EFBFBD><69>tZ<74>l<EFBFBD>0<EFBFBD>#<23>0<EFBFBD><08><>9q"<22><>HܜH<DC9C>Q<EFBFBD><51><1B><>"<22><>L5}-<2D><><59><D7BE><EFBFBD>k<EFBFBD>`<60><>><3E>z<><E9B8B3><EFBFBD>4&<26>p<EFBFBD><70>!<21><><EFBFBD>!<21><>`<60>:5

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

View File

@@ -0,0 +1,14 @@
x<EFBFBD><EFBFBD><EFBFBD>A<0E> <10>a<EFBFBD>޺<EFBFBD><DEBA><EFBFBD><EFBFBD><EFBFBD>@n7+*L++<2B><12><><05><>bb<62>*LC<4C><12><><EFBFBD><EFBFBD>c k<>H<EFBFBD>r<><72>j<EFBFBD><6A><EFBFBD>J5Y<35>i~0<>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>T<EFBFBD>}<7D><>e<EFBFBD>><3E><>5<EFBFBD>b_<62>w<EFBFBD>͟?<3F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><0E>\<5C><>Ra<>i+7<><37>W<EFBFBD><57>\<5C><>wLUN<55>L<EFBFBD><4C>
+<2B><><EFBFBD>
+<2B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>j<EFBFBD><6A>O<4F><7F>kc<6B><63><EFBFBD><EFBFBD><1D>\˩|%<25>o<<3C><>k<EFBFBD><6B>L<EFBFBD>+<2B>+<2B>v<EFBFBD><76><EFBFBD>
+<2B><><EFBFBD>
+<2B><>Š>}<06> <0C>8<><38><EFBFBD>
+<2B><><EFBFBD>
+<2B><><EFBFBD>
+<2B> <0C><19>3<EFBFBD>g<EFBFBD><67><EFBFBD>
+<2B><><EFBFBD>
+<2B><><EFBFBD>
+<2B><>3<EFBFBD>g<EFBFBD><67>@<40><><EFBFBD>
+<2B><><EFBFBD>
+<2B><><EFBFBD>
+<2B><>:R<><52><EFBFBD>X<EFBFBD><58>B<EFBFBD>9<EFBFBD><39>I<EFBFBD>=<>k<EFBFBD><07><>o/Sw<53>ؘ<EFBFBD>ٯ<EFBFBD>`g<><67><EFBFBD><EFBFBD><EFBFBD><1C>r_ٙ<5F>Y<EFBFBD><59>VSY<53><59>zIefnmQoz

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><EFBFBD>0Ў<>u<EFBFBD>A2<41>;Н<><D09D>k<>(<28>g<><67>y<1D>tp9<70><14>$<24><><EFBFBD><EFBFBD>D<7F><44><EFBFBD><EFBFBD>\<5C>e^'t<>-aI<61><49>FM<46>S<EFBFBD>k<EFBFBD><6B>I<EFBFBD>Ť<EFBFBD>:7<><37>|L<>k<EFBFBD>N<EFBFBD>8N7<4E><37><EFBFBD>i}<7D><><EFBFBD><EFBFBD>i,<2C>[W<><57>g<67>Ӵ<><1E><>?3<>1<EFBFBD>i<EFBFBD><69>N<EFBFBD>}}=<3D>OM:4<><34>)S<>L<EFBFBD>2eʔ)S<>L#$<24><>

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

View File

@@ -0,0 +1 @@
x<EFBFBD><EFBFBD><EFBFBD><EFBFBD>

BIN
src/includes/phpqrcode/cache/frame_4.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

View File

@@ -0,0 +1,2 @@
x<EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><EFBFBD>@Ь<><D0AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>@o<>7<1B><>`<60>Qfe<66>䕫PA><3E><><EFBFBD><EFBFBD><EFBFBD><?jjo5WNiz<06><>y<EFBFBD>W<EFBFBD><57><EFBFBD><EFBFBD>&]߅C?<3F>I<>r<EFBFBD>W<EFBFBD><57>^;<3B>8<EFBFBD><38>
<EFBFBD><EFBFBD>s<ð<><C3B0>S{<7B>9^gE<67>}><3E><><]<5D><><EFBFBD><EFBFBD>߳bZ<62>n<EFBFBD><6E>^A<><41>Q}[<5B>9^<5E>]<5D>y<EFBFBD><79>najM܇K̘1cƌ3f̘1<CC98><31><EFBFBD>{<7B>W5}<7D><>{<7B><>7lM<6C><4D><EFBFBD>ޚx<DE9A>I<<1E><>K<EFBFBD><4B><EFBFBD><EFBFBD>αyl3f̘1cƌ3f̘1<CC98><31>ۻٻ={<7B><>αyl3f̘1cƌ3f̘1<CC98><31>ۻٻ={<7B><>αyl3f̘1cƌ3f̘1<CC98><31>ۻٻ={<7B><>αyl3f̘1cƌ3f̘1<CC98><31>ۻٻ={<7B><>αyl3f̘1cƌ3f̘<66><CC98><12><><53>Ӓ7<D392>H<EFBFBD>K޼g\<5C><><EFBFBD>u<EFBFBD><75><EFBFBD>_<EFBFBD><5F>r'4<1F>[<5B><>-<2D>]<5D><>q<EFBFBD><71>L<EFBFBD><4C><38><C69D>Y1q<31><71><EFBFBD><EFBFBD><EFBFBD>!<21><><EFBFBD><EFBFBD>/(%<25>

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Some files were not shown because too many files have changed in this diff Show More