Software y sistemas

Sistema de registro de asistencia con PHP y MySQL

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.

Base de datos y conexión

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.

Diseño del sistema

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>
            &nbsp;|&nbsp;
            <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.

Administración de empleados

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.

Gestión de empleados en sistema de asistencias

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.

Registrar asistencia

Registrar asistencia de empleados – Sistema gratuito con PHP

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: &nbsp;</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.

Reporte de asistencia

Reporte de asistencia por empleado – Software open source para asistencia en PHP y MySQL

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:&nbsp;</label>
            <input required id="start" type="date" name="start" value="<?php echo $start ?>" class="form-control mr-2">
            <label for="end">End:&nbsp;</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.

Poniendo todo junto

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í.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

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

  • 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

  • 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.

  • 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

Entradas recientes

Creador de credenciales web – Aplicación gratuita

Hoy te voy a presentar un creador de credenciales que acabo de programar y que…

1 semana hace

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 semanas hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 semanas hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 semanas hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 semanas hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 semanas hace

Esta web usa cookies.