En este post te mostraré un sistema que acabo de programar en PHP y MySQL, además de usar Bootstrap para framework de diseño. Este software que te presento es totalmente gratuito para descargar, y open source.
El sistema en PHP que he creado se encarga de llevar el registro de asistencia de empleados. Por cada empleado, el sistema guarda si ha asistido o no en determinada fecha.
Un módulo con el que este sistema cuenta es con el de registro y gestión de empleados. El segundo módulo se encarga de tomar la asistencia en una fecha concreta (se puede elegir entre asistencia o falta).
Finalmente el tercer módulo muestra el reporte de asistencia de empleados en donde muestra a partir de un rango de fechas la cantidad de faltas y asistencia que tuvo cada empleado.
Como lo dije, este software es totalmente open source y gratuito. La base de datos que usa es MySQL, con el lenguaje de programación PHP, un poco de JavaScript con Vue y finalmente con Bootstrap para el diseño.
A lo largo de este post te mostraré cómo es que fue creado este sistema así como detallar sus módulos, y te dejaré un enlace de descarga como suelo hacer.
Nota: también puedes ver este sistema funcionando con tarjetas RFID.
Se cuenta con una base de datos que tiene dos tablas: la que tiene el registro de los empleados y la que tiene el registro de asistencia:
CREATE TABLE IF NOT EXISTS employees(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE employee_attendance(
employee_id BIGINT UNSIGNED NOT NULL,
date VARCHAR(10) NOT NULL,
status ENUM('presence', 'absence'),
FOREIGN KEY (employee_id) REFERENCES employees(id) ON UPDATE CASCADE ON DELETE CASCADE
);
Como puedes ver, existen relaciones entre las tablas a través de la columna employee_id
. En el lado de la conexión desde PHP hasta MySQL usamos PDO. La conexión está dentro de las funciones:
<?php
function getDatabase()
{
$password = getVarFromEnvironmentVariables("MYSQL_PASSWORD");
$user = getVarFromEnvironmentVariables("MYSQL_USER");
$dbName = getVarFromEnvironmentVariables("MYSQL_DATABASE_NAME");
$database = new PDO('mysql:host=localhost;dbname=' . $dbName, $user, $password);
$database->query("set names utf8;");
$database->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$database->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
return $database;
}
function getVarFromEnvironmentVariables($key)
{
if (defined("_ENV_CACHE")) {
$vars = _ENV_CACHE;
} else {
$file = "env.php";
if (!file_exists($file)) {
throw new Exception("The environment file ($file) does not exists. Please create it");
}
$vars = parse_ini_file($file);
define("_ENV_CACHE", $vars);
}
if (isset($vars[$key])) {
return $vars[$key];
} else {
throw new Exception("The specified key (" . $key . ") does not exist in the environment file");
}
}
Las credenciales de acceso está en el archivo env.php
(no incluido, el cual debes crear tú). En mi caso se ve así:
; <?php exit; ?>
MYSQL_DATABASE_NAME = "attendance"
MYSQL_USER = "root"
MYSQL_PASSWORD = ""
De este modo vamos a poder incluir la conexión en cualquier lugar que sea necesaria. Por cierto, vamos a evitar inyecciones SQL por lo que este sistema será seguro en ese aspecto.
El sistema es muy simple y se usan varios includes para separar el menú de navegación, el encabezado y el pie. También se separan todas las funciones en un solo archivo. Por ejemplo, así es el encabezado:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Attendance system with PHP - By Parzibyte</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<style>
body {
padding-top: 80px;
}
</style>
</head>
<body>
<main class="container-fluid">
Así es el menú:
<nav class="navbar navbar-expand-md navbar-dark bg-success fixed-top">
<a class="navbar-brand" href="https://parzibyte.me/blog">
<img class="img-fluid" style="max-width: 200px" src="img/parzibyte_logo.png" loading="lazy">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" id="botonMenu"
aria-label="Mostrar u ocultar menú">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="employees.php">Employees <i class="fa fa-users"></i></a>
</li>
<li class="nav-item">
<a class="nav-link" href="attendance_register.php">Register attendance <i class="fa fa-check-double"></i></a>
</li>
<li class="nav-item">
<a class="nav-link" href="attendance_report.php">Attendance report <i class="fa fa-file-alt"></i></a>
</li>
</ul>
</div>
</nav>
Y finalmente así se ve el pie:
</main>
<footer class="px-2 py-2 fixed-bottom bg-dark">
<span class="text-muted">Attendance system written by
<a class="text-white" href="//parzibyte.me/blog">Parzibyte</a>
|
<a target="_blank" class="text-white" href="//parzibyte.me/blog">
View source
</a>
</span>
</footer>
</body>
</html>
También he usado los iconos de FontAwesome.
Pasemos a la administración de empleados. En este caso solo registramos el nombre del empleado pero podemos agregar más campos de ser necesario. Es un CRUD completo, es decir, registrar, editar, eliminar y listar.
Las funciones de PHP que gestionan todas estas operaciones son las siguientes:
<?php
function deleteEmployee($id)
{
$db = getDatabase();
$statement = $db->prepare("DELETE FROM employees WHERE id = ?");
return $statement->execute([$id]);
}
function updateEmployee($name, $id)
{
$db = getDatabase();
$statement = $db->prepare("UPDATE employees SET name = ? WHERE id = ?");
return $statement->execute([$name, $id]);
}
function getEmployeeById($id)
{
$db = getDatabase();
$statement = $db->prepare("SELECT id, name FROM employees WHERE id = ?");
$statement->execute([$id]);
return $statement->fetchObject();
}
function saveEmployee($name)
{
$db = getDatabase();
$statement = $db->prepare("INSERT INTO employees(name) VALUES (?)");
return $statement->execute([$name]);
}
function getEmployees()
{
$db = getDatabase();
$statement = $db->query("SELECT id, name FROM employees");
return $statement->fetchAll();
}
Cada función recibe argumentos dependiendo de lo que realiza. Por ejemplo, la función para obtener empleados no recibe ningún argumento pero regresa un arreglo con todos los empleados. No explicaré todo el código aquí pues se haría muy extenso.
Para el caso del listado de empleados lo hacemos así:
<?php
include_once "header.php";
include_once "nav.php";
include_once "functions.php";
$employees = getEmployees();
?>
<div class="row">
<div class="col-12">
<h1 class="text-center">Employees</h1>
</div>
<div class="col-12">
<a href="employee_add.php" class="btn btn-info mb-2">Add new employee <i class="fa fa-plus"></i></a>
</div>
<div class="col-12">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
<?php foreach ($employees as $employee) { ?>
<tr>
<td>
<?php echo $employee->id ?>
</td>
<td>
<?php echo $employee->name ?>
</td>
<td>
<a class="btn btn-warning" href="employee_edit.php?id=<?php echo $employee->id ?>">
Edit <i class="fa fa-edit"></i>
</a>
</td>
<td>
<a class="btn btn-danger" href="employee_delete.php?id=<?php echo $employee->id ?>">
Delete <i class="fa fa-trash"></i>
</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<?php
include_once "footer.php";
Lo único que hacemos es dibujar una tabla usando un foreach
, recorriendo el arreglo de empleados que nos devuelve la función getEmployees
.
Para el caso del registro de asistencia implementé el framework Vue.js para facilitar ciertas cosas. Se podría decir que este apartado implementa AJAX.
Lo primero que se hace es elegir la fecha en la que se toma la asistencia. Si ya hay datos para la fecha, entonces aparecen. En caso de que no, todos los empleados tienen el estado “desconocido”.
Ahora veamos el algoritmo. Al guardar la asistencia, se eliminan todos los registros de esa fecha. Después, por cada registro nuevo se va guardando el estado. Es decir, se hace un “update” que en realidad es limpiar todo y guardarlo de nuevo.
Por cierto, al inicio cuando cambia una fecha, se cargan todos los datos que ya existan. Todo lo que acabo de mencionar se puede ver en las siguientes funciones:
<?php
function saveAttendanceData($date, $employees)
{
deleteAttendanceDataByDate($date);
$db = getDatabase();
$db->beginTransaction();
$statement = $db->prepare("INSERT INTO employee_attendance(employee_id, date, status) VALUES (?, ?, ?)");
foreach ($employees as $employee) {
$statement->execute([$employee->id, $date, $employee->status]);
}
$db->commit();
return true;
}
function deleteAttendanceDataByDate($date)
{
$db = getDatabase();
$statement = $db->prepare("DELETE FROM employee_attendance WHERE date = ?");
return $statement->execute([$date]);
}
function getAttendanceDataByDate($date)
{
$db = getDatabase();
$statement = $db->prepare("SELECT employee_id, status FROM employee_attendance WHERE date = ?");
$statement->execute([$date]);
return $statement->fetchAll();
}
Aquí hay que prestar atención a la función saveAttendanceData
que simplemente guarda los datos de la asistencia, recibiendo la lista de empleados con su estado, además de la fecha.
Lo interesante es ver el commit
y el beginTransaction
, pues hago esto para que la inserción sea más rápida, ya que se hacen varios insert
dependiendo de la cantidad de datos.
También veamos el código que hace posible esta vista, el cual maneja las peticiones AJAX y usa vue.js:
<?php
include_once "header.php";
include_once "nav.php";
?>
<div class="row" id="app">
<div class="col-12">
<h1 class="text-center">Attendance</h1>
</div>
<div class="col-12">
<div class="form-inline mb-2">
<label for="date">Date: </label>
<input @change="refreshEmployeesList" v-model="date" name="date" id="date" type="date" class="form-control">
<button @click="save" class="btn btn-success ml-2">Save</button>
</div>
</div>
<div class="col-12">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>
Employee
</th>
<th>
Status
</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in employees">
<td>{{employee.name}}</td>
<td>
<select v-model="employee.status" class="form-control">
<option disabled value="unset">--Select--</option>
<option value="presence">Presence</option>
<option value="absence">Absence</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="js/vue.min.js"></script>
<script src="js/vue-toasted.min.js"></script>
<script>
Vue.use(Toasted);
const UNSET_STATUS = "unset";
new Vue({
el: "#app",
data: () => ({
employees: [],
date: "",
}),
async mounted() {
this.date = this.getTodaysDate();
await this.refreshEmployeesList();
},
methods: {
getTodaysDate() {
const date = new Date();
const month = date.getMonth() + 1;
const day = date.getDate();
return `${date.getFullYear()}-${(month < 10 ? '0' : '').concat(month)}-${(day < 10 ? '0' : '').concat(day)}`;
},
async save() {
// We only need id and status, nothing more
let employeesMapped = this.employees.map(employee => {
return {
id: employee.id,
status: employee.status,
}
});
// And we need only where status is set
employeesMapped = employeesMapped.filter(employee => employee.status != UNSET_STATUS);
const payload = {
date: this.date,
employees: employeesMapped,
};
const response = await fetch("./save_attendance_data.php", {
method: "POST",
body: JSON.stringify(payload),
});
this.$toasted.show("Saved", {
position: "top-left",
duration: 1000,
});
},
async refreshEmployeesList() {
// Get all employees
let response = await fetch("./get_employees_ajax.php");
let employees = await response.json();
// Set default status: unset
let employeeDictionary = {};
employees = employees.map((employee, index) => {
employeeDictionary[employee.id] = index;
return {
id: employee.id,
name: employee.name,
status: UNSET_STATUS,
}
});
// Get attendance data, if any
response = await fetch(`./get_attendance_data_ajax.php?date=${this.date}`);
let attendanceData = await response.json();
// Refresh attendance data in each employee, if any
attendanceData.forEach(attendanceDetail => {
let employeeId = attendanceDetail.employee_id;
if (employeeId in employeeDictionary) {
let index = employeeDictionary[employeeId];
employees[index].status = attendanceDetail.status;
}
});
// Let Vue do its magic ;)
this.employees = employees;
}
},
});
</script>
<?php
include_once "footer.php";
Fíjate en la línea 67 que se encarga de guardar la asistencia. En los comentarios se explica el código, y finalmente la petición AJAX es realizada en la línea 81.
También es importante resaltar que para avisar al usuario que la asistencia ha sido guardada he usado una librería para mostrar notificaciones en Vue.js.
El tercer y último módulo se encarga de brindar un reporte de asistencia de empleados, en un período de fechas. Es decir, a partir de un rango de fechas, se muestra un reporte en donde aparece el total de faltas de un empleado, así como el total de asistencias.
La función que se encarga de dar estos datos es la siguiente:
<?php
function getEmployeesWithAttendanceCount($start, $end)
{
$query = "select employees.name,
sum(case when status = 'presence' then 1 else 0 end) as presence_count,
sum(case when status = 'absence' then 1 else 0 end) as absence_count
from employee_attendance
inner join employees on employees.id = employee_attendance.employee_id
where date >= ? and date <= ?
group by employee_id;";
$db = getDatabase();
$statement = $db->prepare($query);
$statement->execute([$start, $end]);
return $statement->fetchAll();
}
En este caso estoy haciendo un doble count, una cosa que no había hecho en toda mi vida. Lo demás es simplemente filtrar por rango de fechas y agrupar.
Y el código que muestra la interfaz dependiendo de la fecha es:
<?php
include_once "header.php";
include_once "nav.php";
include_once "functions.php";
$start = date("Y-m-d");
$end = date("Y-m-d");
if (isset($_GET["start"])) {
$start = $_GET["start"];
}
if (isset($_GET["end"])) {
$end = $_GET["end"];
}
$employees = getEmployeesWithAttendanceCount($start, $end);
?>
<div class="row">
<div class="col-12">
<h1 class="text-center">Attendance report</h1>
</div>
<div class="col-12">
<form action="attendance_report.php" class="form-inline mb-2">
<label for="start">Start: </label>
<input required id="start" type="date" name="start" value="<?php echo $start ?>" class="form-control mr-2">
<label for="end">End: </label>
<input required id="end" type="date" name="end" value="<?php echo $end ?>" class="form-control">
<button class="btn btn-success ml-2">Filter</button>
</form>
</div>
<div class="col-12">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Employee</th>
<th>Presence count</th>
<th>Absence count</th>
</tr>
</thead>
<tbody>
<?php foreach ($employees as $employee) { ?>
<tr>
<td>
<?php echo $employee->name ?>
</td>
<td>
<?php echo $employee->presence_count ?>
</td>
<td>
<?php echo $employee->absence_count ?>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<?php
include_once "footer.php";
Por defecto se muestra el reporte del día actual, pero puede seleccionarse cualquier rango de fechas.
Me parece que ya he explicado las partes más importantes del código fuente de este sistema; aunque recuerda que no es todo el código. Te dejo el código completo en mi Github. Recuerda que debes crear el archivo de entorno y también crear la base de datos.
Hice este sistema porque planeo hacer otro sistema que toma la asistencia usando etiquetas RFID y una tarjeta ESP8266, pero iré paso por paso.
Finalmente te invito a leer más sobre PHP, MySQL o ver más sistemas creados por mí.
El día de hoy te mostraré cómo crear un servidor HTTP (servidor web) en Android…
En este post te voy a enseñar a designar una carpeta para imprimir todos los…
En este artículo te voy a enseñar la guía para imprimir en una impresora térmica…
Hoy te voy a mostrar un ejemplo de programación para agregar un módulo de tasa…
Los usuarios del plugin para impresoras térmicas pueden contratar licencias, y en ocasiones me han…
Hoy voy a enseñarte cómo imprimir el € en una impresora térmica. Vamos a ver…
Esta web usa cookies.
Ver comentarios
Cual seria la estructura y distribucion de las carpetas y archivos.
Modelo (que carpetas y archivos irian aqui)
Vista (que carpetas y archivos irian aqui)
Controlador (que carpetas y archivos irian aqui)
Otros archivos
Hola
Hay alguna manera de integrar un lector de huellas digitales?
Saludos
Que tal, tenia la duda de si este sistema se puede integrar a una base de datos existente, con usuarios y materias ya creados en la misma base. Ya que quisiera registrar las asistencias que ya tenemos en excel.
Saludos
Me pongo en contacto con usted para una asesoria la proxima semana!!
Hola. Todo es posible, solo es cuestión de adaptar el sistema
Buenas, mira la página de registro de asitencia no muestra la información, ni el campo fecha muestra el calendario para poner la fecha, y aparece {{employees}} y no muestra nada.
A ver si me podéis echar una mano
muchas gracias.
Acabo de probar y funciona bien. Debe ser alguna configuración de su lado
Buenas tardes, queria agradecerte por el post es muy interesante, realmente no se mucho de programacion, he descargado tu proyecto y me ha ocurrrido que en la pagina de Attendance report no me lista los usuarios que he creado, al igual en la pagina Attendance, tampoco me lista los usuarios, adicionalmente me sale este error *** Warning: Constants may only evaluate to scalar values in C:\AppServ\www\pruebas\asistencia-php\functions.php on line 118 ***.
Agradezco tu ayuda y atencion
Hola. Gracias por sus comentarios. Puede contactarme en https://parzibyte.me#contacto si necesita ayuda
Saludos!
vamos a probar