main[ADD]attendance_flask:Codigos QR para asistencias
This commit is contained in:
parent
6c9b5169d2
commit
cbd3e19d7a
28
get_query.py
Normal file
28
get_query.py
Normal file
@ -0,0 +1,28 @@
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
import requests
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('database.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
conn = get_db_connection()
|
||||
data = conn.execute('SELECT user, datein, dateout, ip_branch, id FROM hits WHERE dateout != ? AND dateinspe = ? ', ('', '')).fetchall()
|
||||
|
||||
for d in data:
|
||||
params ={
|
||||
'employee_id': d[0],
|
||||
'check_in': d[1],
|
||||
'check_out': d[2],
|
||||
'ip_branch': d[3],
|
||||
}
|
||||
response = requests.post('http://137.184.126.24:8080/hr_attendance_extended/public/attendance/', json = params).json()
|
||||
if response["message"]:
|
||||
time = datetime.now(pytz.timezone('America/Bogota')).strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn.execute("UPDATE hits SET dateinspe = ? WHERE id = ?", (time, d[4]))
|
||||
conn.commit()
|
||||
else:
|
||||
continue
|
||||
128
main.py
128
main.py
@ -2,106 +2,164 @@
|
||||
|
||||
import uuid
|
||||
import sqlite3
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, render_template, current_app
|
||||
import pytz
|
||||
import os
|
||||
from flask import Flask, request, render_template
|
||||
from flask_uuid import FlaskUUID
|
||||
from flask_qrcode import QRcode
|
||||
from flask_socketio import SocketIO, emit
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
# WIP: use sockets to broadcast when a hit to the expected uuid has been
|
||||
# received, so that connected clients (the Chromium public browser) can
|
||||
# refresh the qr code and update the list of users
|
||||
# https://flask-socketio.readthedocs.io
|
||||
import requests
|
||||
|
||||
app = Flask(__name__)
|
||||
FlaskUUID(app)
|
||||
#QRcode(app)
|
||||
qrcode = QRcode(app)
|
||||
app.config['SECRET_KEY'] = '53a8373e7ae652cd38beba15454b1dc4'
|
||||
#app = ProxyFix(app, x_for=1, x_host=1)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
socketio = SocketIO(app, async_mode=None)
|
||||
|
||||
|
||||
def get_db_connection():
|
||||
conn = sqlite3.connect('database.db')
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def hash_string(string):
|
||||
salt = 'xpwQDFQ7gvcQ--rgO7l9yA'
|
||||
return hashlib.md5(salt.encode() + string.encode()).hexdigest()
|
||||
|
||||
|
||||
def check_hash(string, hashed_string):
|
||||
salt = 'xpwQDFQ7gvcQ--rgO7l9yA'
|
||||
return hashed_string == hashlib.md5(salt.encode() + string.encode()).hexdigest()
|
||||
|
||||
def generate_next_url():
|
||||
|
||||
def type_string(string):
|
||||
return hashlib.md5(string.encode()).hexdigest()
|
||||
|
||||
|
||||
def check_type(string):
|
||||
if string == '02181ec55d150f6230547dbbf313e4f8':
|
||||
type_check = 'CheckIn'
|
||||
elif string == '231d5cc583e69d5b6c5bdced40dcc27c':
|
||||
type_check = 'CheckOut'
|
||||
else:
|
||||
type_check = 'mistake'
|
||||
return type_check
|
||||
|
||||
|
||||
def generate_next_url(type, ip_branch):
|
||||
next_uuid = str(uuid.uuid1())
|
||||
url = request.url_root + str(next_uuid) + '/' + hash_string(next_uuid)
|
||||
url = str(next_uuid) + '/' + hash_string(next_uuid) + \
|
||||
'/' + type_string(type) + '/' + (ip_branch)
|
||||
message_bytes = url.encode('ascii')
|
||||
base64_bytes = base64.b64encode(message_bytes)
|
||||
url = base64_bytes.decode('ascii')
|
||||
return url
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def show_qr_and_list():
|
||||
# TODO: reject direct connections to server; allow access only via proxy
|
||||
get_list = requests.get('http://137.184.126.24:8080/hr_attendance_extended/public/attendance/').json()
|
||||
list_ips = get_list["list_ips"]
|
||||
ip_branch = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
if ip_branch in list_ips:
|
||||
access = True
|
||||
else:
|
||||
access = False
|
||||
return render_template("template.html", ip_branch=ip_branch, access=access)
|
||||
|
||||
return render_template("template.html")
|
||||
|
||||
@app.route('/scan')
|
||||
def show_qr_reader():
|
||||
return current_app.send_static_file('scan.html')
|
||||
|
||||
@app.route('/<uuid:id>/<hashed>')
|
||||
def catch_uuids(id, hashed):
|
||||
@app.route('/<uuid:id>/<hashed>/<type>/<ip_branch>')
|
||||
def catch_uuids(id, hashed, type, ip_branch):
|
||||
user = request.headers.get('Remote-User')
|
||||
# TODO: Check directly with Authelia using https://auth.agofer.net/api/verify
|
||||
time = datetime.now().strftime("%A %Y-%m-%d %H:%M:%S")
|
||||
time = datetime.now(pytz.timezone('America/Bogota')
|
||||
).strftime("%Y-%m-%d %H:%M:%S")
|
||||
type_check = check_type(str(type))
|
||||
error = None
|
||||
data = []
|
||||
conn = get_db_connection()
|
||||
existing = conn.execute(
|
||||
'SELECT * FROM hits WHERE uuid = ?', (str(id),)).fetchone()
|
||||
existing1 = conn.execute(
|
||||
'SELECT * FROM hits WHERE uuid1 = ?', (str(id),)).fetchone()
|
||||
existing2 = conn.execute(
|
||||
'SELECT * FROM hits WHERE uuid2 = ?', (str(id),)).fetchone()
|
||||
if not user:
|
||||
error = 'NO_USERNAME'
|
||||
elif existing:
|
||||
elif existing1:
|
||||
error = 'ALREADY_USED'
|
||||
elif existing2:
|
||||
error = 'ALREADY_USED'
|
||||
elif not check_hash(str(id), str(hashed)):
|
||||
error = 'DIFFERENT_NODE'
|
||||
else:
|
||||
conn.execute("INSERT INTO hits (uuid, user) VALUES (?, ?)", (str(id), user))
|
||||
if type_check == 'mistake':
|
||||
error = 'DIFFERENT_TYPE'
|
||||
else:
|
||||
exit = conn.execute(
|
||||
'SELECT * FROM hits WHERE user = ? AND dateout = ?', (user, '')).fetchone()
|
||||
if type_check == 'CheckIn':
|
||||
if exit:
|
||||
error = 'MISSING_EXIT'
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO hits (user, uuid1, datein) VALUES (?, ?, ?)", (user, str(id), time))
|
||||
conn.commit()
|
||||
#socketio.emit('qr_used', {'data': (time, user, str(id))})
|
||||
url = generate_next_url()
|
||||
url = generate_next_url(type_check, str(ip_branch))
|
||||
qr = qrcode(url)
|
||||
socketio.emit('qr_used', {'data': (qr, url, user, time)})
|
||||
|
||||
socketio.emit('qr_used', {'data': (
|
||||
qr, url, user, time, type_check,)})
|
||||
elif type_check == 'CheckOut':
|
||||
if exit:
|
||||
conn.execute(
|
||||
"UPDATE hits SET dateout = ?, uuid2 = ?, ip_branch = ? WHERE user = ? AND dateout = ?", (time, str(id), str(ip_branch), user, ''))
|
||||
conn.commit()
|
||||
url = generate_next_url(type_check, str(ip_branch))
|
||||
qr = qrcode(url)
|
||||
os.system('python get_query.py')
|
||||
socketio.emit('qr_used', {'data': (
|
||||
qr, url, user, time, type_check)})
|
||||
else:
|
||||
error = 'MISSING_ENTER'
|
||||
data = conn.execute(
|
||||
'SELECT * FROM hits WHERE user = ? ORDER BY id DESC LIMIT 10', (user,)
|
||||
).fetchall()
|
||||
'SELECT * FROM hits WHERE user = ? ORDER BY id DESC LIMIT 10', (user,)).fetchall()
|
||||
conn.close()
|
||||
return render_template('thanks.html', user=user, time=time, error=error, type=type_check, hits=data)
|
||||
|
||||
return render_template('thanks.html', user=user, time=time, error=error, hits=data)
|
||||
|
||||
@socketio.on('message')
|
||||
def handle_message(data):
|
||||
print('received message: ' + data)
|
||||
|
||||
|
||||
@socketio.on('connection')
|
||||
def handle_initial_connection(json_data):
|
||||
url = generate_next_url()
|
||||
ip_branch = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
|
||||
url = generate_next_url('CheckIn', ip_branch)
|
||||
url2 = generate_next_url('CheckOut', ip_branch)
|
||||
qr = qrcode(url)
|
||||
qr2 = qrcode(url2)
|
||||
conn = get_db_connection()
|
||||
last_entries = conn.execute('SELECT user, created FROM hits ORDER BY id DESC LIMIT 10')
|
||||
last_entries = conn.execute(
|
||||
'SELECT user, datein FROM hits WHERE datein != ? ORDER BY id DESC LIMIT 10', ('',)).fetchall()
|
||||
last_entries2 = conn.execute(
|
||||
'SELECT user, dateout FROM hits WHERE dateout != ? ORDER BY id DESC LIMIT 10', ('',)).fetchall()
|
||||
array_entries = []
|
||||
array_entries2 = []
|
||||
for entry in last_entries:
|
||||
array_entries.append((entry[0], entry[1]))
|
||||
for entry2 in last_entries2:
|
||||
array_entries2.append((entry2[0], entry2[1]))
|
||||
conn.close()
|
||||
emit('initial_qr', {'data': (qr, url, array_entries)})
|
||||
#socketio.emit('initial_qr', {'data': (qr,)})
|
||||
emit('initial_qr', {
|
||||
'data': (qr, url, array_entries, qr2, url2, array_entries2)})
|
||||
print('received json: ' + str(json_data))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#app.run(host='0.0.0.0')
|
||||
socketio.run(app, host='0.0.0.0')
|
||||
|
||||
10
schema.sql
10
schema.sql
@ -2,8 +2,12 @@ DROP TABLE IF EXISTS hits;
|
||||
|
||||
CREATE TABLE hits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
uuid TEXT NOT NULL,
|
||||
user TEXT NOT NULL
|
||||
user TEXT NOT NULL,
|
||||
uuid1 TEXT NOT NULL,
|
||||
uuid2 TEXT NOT NULL DEFAULT '',
|
||||
datein TEXT NOT NULL DEFAULT '' ,
|
||||
dateout TEXT NOT NULL DEFAULT '',
|
||||
ip_branch TEXT NOT NULL DEFAULT '',
|
||||
dateinspe TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Instascan</title>
|
||||
<!-- script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script -->
|
||||
<!-- script src="https://rawgit.com/schmich/instascan-builds/master/instascan.min.js"></script -->
|
||||
<script src="/static/instascan.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video style="outline:none; width:90%;" id="preview"></video>
|
||||
<script type="text/javascript">
|
||||
let options = {
|
||||
video: document.getElementById('preview'),
|
||||
mirror: false,
|
||||
};
|
||||
let scanner = new Instascan.Scanner(options);
|
||||
scanner.addListener('scan', function (content) {
|
||||
window.location.replace(content);
|
||||
});
|
||||
Instascan.Camera.getCameras().then(function (cameras) {
|
||||
if (cameras.length > 1) {
|
||||
scanner.start(cameras[1]);
|
||||
} else if (cameras.length > 0) {
|
||||
scanner.start(cameras[0]);
|
||||
} else {
|
||||
console.error('No cameras found.');
|
||||
}
|
||||
}).catch(function (e) {
|
||||
console.error(e);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,60 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<script src="/static/instascan.min.js"></script>
|
||||
<script src="//code.jquery.com/jquery-1.12.4.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"
|
||||
integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA=="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
$(document).ready(function() {
|
||||
$(document).ready(function () {
|
||||
var socket = io();
|
||||
socket.on('connect', function() {
|
||||
socket.emit('connection', {data: 'Connected to server'});
|
||||
socket.on('connect', function () {
|
||||
socket.emit('connection', { data: 'Connected to server' });
|
||||
});
|
||||
socket.on('initial_qr', function(msg, cb) {
|
||||
socket.on('initial_qr', function (msg, cb) {
|
||||
$('#qrcode').prop('src', msg.data[0]);
|
||||
$('#qrcode2').prop('src', msg.data[3]);
|
||||
$('#url').prop('href', msg.data[1]).text(msg.data[1]);
|
||||
$('#url2').prop('href', msg.data[4]).text(msg.data[4]);
|
||||
$('#log').empty();
|
||||
msg.data[2].forEach(function(item) {
|
||||
msg.data[2].forEach(function (item) {
|
||||
$('#log').append('<li>' + item[0] + ': ' + item[1] + '</li>');
|
||||
}
|
||||
);
|
||||
$('#log2').empty();
|
||||
msg.data[5].forEach(function (item) {
|
||||
$('#log2').append('<li>' + item[0] + ': ' + item[1] + '</li>');
|
||||
}
|
||||
);
|
||||
if (cb)
|
||||
cb();
|
||||
});
|
||||
socket.on('qr_used', function(msg, cb) {
|
||||
/*
|
||||
location.reload(true);
|
||||
*/
|
||||
$('#qrcode').prop('src', msg.data[0]).fadeTo('fast', 0.05, function(){$(this).delay(100).fadeTo('slow', 1)});
|
||||
socket.on('qr_used', function (msg, cb) {
|
||||
if (msg.data[4] == 'CheckIn') {
|
||||
$('#qrcode').prop('src', msg.data[0]).fadeTo(3000, 0.05, function () { $(this).delay(100).fadeTo('slow', 1) });
|
||||
$('#url').prop('href', msg.data[1]).text(msg.data[1]);
|
||||
//$('#log').fadeIn(500, function() { $(this).prepend('<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>') });
|
||||
var new_entry = '<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>';
|
||||
$(new_entry).hide().prependTo('#log').fadeIn('slow');
|
||||
$('#log').fadeIn(500, function() { $(this).prepend() });
|
||||
$('#log li:gt(9)' ).remove();
|
||||
$('#log').fadeIn(500, function () { $(this).prepend() });
|
||||
$('#log li:gt(9)').remove();
|
||||
}
|
||||
if (msg.data[4] == 'CheckOut') {
|
||||
$('#qrcode2').prop('src', msg.data[0]).fadeTo(3000, 0.05, function () { $(this).delay(100).fadeTo('slow', 1) });
|
||||
$('#url2').prop('href', msg.data[1]).text(msg.data[1]);
|
||||
var new_entry = '<li>' + msg.data[2] + ': ' + msg.data[3] + '</li>';
|
||||
$(new_entry).hide().prependTo('#log2').fadeIn('slow');
|
||||
$('#log2').fadeIn(500, function () { $(this).prepend() });
|
||||
$('#log2 li:gt(9)').remove();
|
||||
}
|
||||
if (cb)
|
||||
cb();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<title>Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>QR Code:</h1>
|
||||
{#
|
||||
<p>For URL <a href="{{ request.url_root + next_uuid }}">{{ request.url_root + next_uuid }}</a></p>
|
||||
<img id="qrcode" src="{{ qrcode(request.url_root + next_uuid, box_size=12, border=5) }}">
|
||||
#}
|
||||
<p>For URL <a id="url" href="/">__loading...__</a></p>
|
||||
<img id="qrcode" src="/static/loading.gif">
|
||||
<h1>Last 10 users:</h1>
|
||||
<div><ul id="log"></ul></div>
|
||||
{#
|
||||
<ul>
|
||||
{% for hit in hits %}
|
||||
<li>{{ hit['created'] }}, {{ hit['user'] }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
#}
|
||||
</body>
|
||||
</html>
|
||||
<title>Códigos QR</title>
|
||||
<style>
|
||||
#contenedor {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#contenedor>div {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.block {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.list {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#refuse {
|
||||
font-size: 24pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body style="text-align: center;">
|
||||
{% if access == true %}
|
||||
<h1>Códigos QR</h1>
|
||||
<p> IP: {{ ip_branch }}</p>
|
||||
<div id="contenedor">
|
||||
<div>
|
||||
<h1>Entrada</h1>
|
||||
<img id="qrcode" src="/static/loading.gif">
|
||||
<h1>Últimos 10 usuarios:</h1>
|
||||
<div class="block">
|
||||
<ul class="list" id="log"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Salida</h1>
|
||||
<img id="qrcode2" src="/static/loading.gif">
|
||||
<h1>Últimos 10 usuarios:</h1>
|
||||
<div class="block">
|
||||
<ul class="list" id="log2"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="refuse">
|
||||
<video style="width:100%; height:100%;" id="preview"></video>
|
||||
<script type="text/javascript">
|
||||
let options = {
|
||||
video: document.getElementById('preview'),
|
||||
mirror: false,
|
||||
};
|
||||
let scanner = new Instascan.Scanner(options);
|
||||
scanner.addListener('scan', function (content) {
|
||||
const decodedData = atob(content)
|
||||
window.location.replace(decodedData);
|
||||
});
|
||||
|
||||
Instascan.Camera.getCameras().then(function (cameras) {
|
||||
if (cameras.length > 3) {
|
||||
scanner.start(cameras[3]);
|
||||
} else if (cameras.length > 2) {
|
||||
scanner.start(cameras[2]);
|
||||
} else if (cameras.length > 1) {
|
||||
scanner.start(cameras[1]);
|
||||
} else if (cameras.length > 0) {
|
||||
scanner.start(cameras[0]);
|
||||
} else {
|
||||
console.error('No cameras found.');
|
||||
}
|
||||
}).catch(function (e) {
|
||||
console.error(e);
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
{% endif %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@ -1,29 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Example</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
|
||||
<title>Registro</title>
|
||||
<style>
|
||||
.list {
|
||||
text-align: left;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 24pt;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 40pt;
|
||||
}
|
||||
|
||||
.contenedor {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
padding: 2% 8%;
|
||||
font-size: 34pt;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
background-color: #060674;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<body>
|
||||
<div id="contenedor">
|
||||
<br /> <br />
|
||||
{% if error == 'NO_USERNAME' %}
|
||||
<h1>Error</h1>
|
||||
<p>No username received. <a href='https://auth.agofer.net/'>Login here</a>.</p>
|
||||
<p>No se encontro el usuario.<br /><br /><a href='https://auth.agofer.net/'>Inicia sesión AQUI</a>.</p>
|
||||
{% elif error == 'ALREADY_USED' %}
|
||||
<h1>Error</h1>
|
||||
<p>Code has been used already.</p>
|
||||
<p>El enlace ya ha sido utilizado.<br /><br /><a href='https://attendance.agofer.net'>inténteta de nuevo
|
||||
AQUI</a>.</p>
|
||||
{% elif error == 'DIFFERENT_NODE' %}
|
||||
<h1>Error</h1>
|
||||
<p>Code was not generated by this system.</p>
|
||||
<p>El código no fue generado por este sistema.<br /><br /><a href='https://attendance.agofer.net'>inténteta
|
||||
de nuevo AQUI</a>.</p>
|
||||
{% elif error == 'DIFFERENT_TYPE' %}
|
||||
<h1>Error</h1>
|
||||
<p>No se encontró el tipo de entrada.<br /><br /><a href='https://attendance.agofer.net'>inténteta de nuevo
|
||||
AQUI</a>.</p>
|
||||
{% elif error == 'MISSING_EXIT' %}
|
||||
<h1>!Olvidaste la Salida!</h1>
|
||||
<p>Para registrar la entrada es necesario terminar la jornada de trabajo anterior.<br /><br /><a
|
||||
href='https://attendance.agofer.net'>Escanear Salida AQUI</a>.</p>
|
||||
{% elif error == 'MISSING_ENTER' %}
|
||||
<h1>!Olvidaste la Entrada!</h1>
|
||||
<p>Para registrar la salida es necesario iniciar la jornada laboral.<br /><br /><a
|
||||
href='https://attendance.agofer.net'>Escanear entrada AQUI</a>.</p>
|
||||
{% else %}
|
||||
<h1>Thanks</h1>
|
||||
<p><strong>At {{ time }}</strong>,</p>
|
||||
<p>Registered entrance or exit for user {{ user }} .</p>
|
||||
<h2>Last 10 registers for {{ user }}:</h2>
|
||||
<ul>
|
||||
<span class="fa-stack fa-lg">
|
||||
<i class="fa fa-circle fa-stack-2x text-success" style="color:green;"></i>
|
||||
<i class="fa fa-check fa-stack-1x fa-inverse"></i>
|
||||
</span>
|
||||
<span class="header"><strong>Registro Guardado</strong></span>
|
||||
<br />
|
||||
<p><strong>{{ time }}</strong></p>
|
||||
<p>Registrado {{ type }} para el usuario {{ user }} .</p>
|
||||
<br />
|
||||
<span><strong>Últimos 10 registros para {{ user }}:</strong></span>
|
||||
<br />
|
||||
<ul class="list">
|
||||
{% if type == 'CheckOut' %}
|
||||
{% for hit in hits %}
|
||||
<li>{{ hit['created'] }}</li>
|
||||
<li>{{ hit['dateout'] }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for hit in hits %}
|
||||
<li>{{ hit['datein'] }}</li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
<br /> <br />
|
||||
</div>
|
||||
<button onclick="window.close();">Cerrar</button>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user