Split down files and add question about covid-test
This commit is contained in:
parent
4e26bbd6f2
commit
73241c4116
|
@ -77,6 +77,7 @@ def post_arrival():
|
||||||
now = datetime.utcnow().isoformat() + 'Z'
|
now = datetime.utcnow().isoformat() + 'Z'
|
||||||
db.insert({
|
db.insert({
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'tested': data.get('tested') or False,
|
||||||
'room': data['room'],
|
'room': data['room'],
|
||||||
'arrival': data.get('arrival') or now,
|
'arrival': data.get('arrival') or now,
|
||||||
'departure': None
|
'departure': None
|
||||||
|
|
293
web/index.html
293
web/index.html
|
@ -2,89 +2,10 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>FTracker</title>
|
<title>FTracker</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
<meta name="theme-color" content="#c50e1f">
|
<meta name="theme-color" content="#c50e1f">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<style>
|
<link href="style.css" rel="stylesheet" type="text/css">
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
height: 100%;
|
|
||||||
background: #ddd;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #eee;
|
|
||||||
background: #c50e1f;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
form {
|
|
||||||
padding: 16px;
|
|
||||||
max-width: 512px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
label#agreelabel {
|
|
||||||
height: 32px;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
label span {
|
|
||||||
width: calc(100% - 50px);
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
border: none;
|
|
||||||
padding: 16px;
|
|
||||||
margin: 4px 0;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
input[type=text] {
|
|
||||||
color: #000;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
}
|
|
||||||
input[type=submit], input[type=button] {
|
|
||||||
background: #c50e1f;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
input[type=checkbox] {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
float: left;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.request {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
background: #ddd;
|
|
||||||
top: 16px;
|
|
||||||
left: 16px;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
box-shadow: 0 1px 4px 0;
|
|
||||||
}
|
|
||||||
input[type=datetime-local] {
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
<script>
|
||||||
// 1st script, prepares values needed for writing document
|
// 1st script, prepares values needed for writing document
|
||||||
var cbt = {
|
var cbt = {
|
||||||
|
@ -114,201 +35,41 @@
|
||||||
else
|
else
|
||||||
document.write('FTracker<br>V1')
|
document.write('FTracker<br>V1')
|
||||||
</script></h1>
|
</script></h1>
|
||||||
<form id="mainform">
|
<div id="startpage">
|
||||||
|
This is a web app to track which people
|
||||||
|
were in the same rooms at which times in order to backtrace
|
||||||
|
potential viral infections.<br><br>
|
||||||
|
If you've reached this page that either means your're
|
||||||
|
testing things or something has gone quite wrong with the\
|
||||||
|
URL.<br>
|
||||||
|
In the former case: Yay it works! In the latter you should
|
||||||
|
probably contact an admin or a dev nearby :(<br><br>
|
||||||
|
Here are a few links for testing:<br>
|
||||||
|
<a href="/view">View Data</a>,
|
||||||
|
<a href="/QRgen">Door Sign Generator</a>,
|
||||||
|
<a href="/?arrival=42">Test Arrival</a>,
|
||||||
|
<a href="/?departure=42">Test Departure</a><br><br>
|
||||||
|
© 2020 made by <a target="_blank" href="mailto:o.winkels@fasttube.de">Oskar</a> \
|
||||||
|
for <a target="_blank" href="//fasttube.de">FaSTTUBe</a>.<br>
|
||||||
|
For source code & licensing see <a href="//git.fasttube.de/FaSTTUBe/ftracker">git repo</a>
|
||||||
|
</div>
|
||||||
|
<form id="mainform" style="display: none">
|
||||||
<label>
|
<label>
|
||||||
Full Name:<br>
|
Full Name:<br>
|
||||||
<input type="text" name="name" id="name" placeholder="John Doe" required>
|
<input type="text" name="name" id="name" placeholder="John Doe" required>
|
||||||
</label>
|
</label>
|
||||||
<label id="agreelabel">
|
<label class="checkbox">
|
||||||
<input type="checkbox" name="agree" id="agree" required>
|
<input type="checkbox" name="agree" id="agree" required>
|
||||||
<span><script>
|
<span><script>
|
||||||
document.write(qp ? cbt[qp.action] : '')
|
document.write(qp ? cbt[qp.action] : '')
|
||||||
</script></span>
|
</script></span>
|
||||||
</label>
|
</label>
|
||||||
|
<script>
|
||||||
|
if (qp && qp.action == 'arrival')
|
||||||
|
document.write('<label class="checkbox"><input type="checkbox" name="tested" id="tested"><span>I have been tested negative for COVID in the last 24 hours</span></label>')
|
||||||
|
</script>
|
||||||
<input type="submit">
|
<input type="submit">
|
||||||
</form>
|
</form>
|
||||||
<script>
|
<script src="main.js"></script>
|
||||||
var mform = document.getElementById('mainform')
|
|
||||||
if (qp == null) {
|
|
||||||
mform.innerHTML = 'This is a web app to track which people\
|
|
||||||
were in the same rooms at which times in order to backtrace\
|
|
||||||
potential viral infections.<br><br>\
|
|
||||||
If you\'ve reached this page that either means your\'re\
|
|
||||||
testing things or something has gone quite wrong with the\
|
|
||||||
URL.<br>\
|
|
||||||
In the former case: Yay it works! In the latter you should\
|
|
||||||
probably contact an admin or a dev nearby :(<br><br>\
|
|
||||||
Here are a few links for testing:<br>\
|
|
||||||
<a href="/view">View Data</a>, \
|
|
||||||
<a href="/QRgen">Door Sign Generator</a>, \
|
|
||||||
<a href="/?arrival=42">Test Arrival</a>, \
|
|
||||||
<a href="/?departure=42">Test Departure</a><br><br>\
|
|
||||||
© 2020 made by <a target="_blank" href="mailto:o.winkels@fasttube.de">Oskar</a> \
|
|
||||||
for <a target="_blank" href="//fasttube.de">FaSTTUBe</a>.<br> \
|
|
||||||
For source code & licensing see <a href="//git.fasttube.de/FaSTTUBe/ftracker">git repo</a>'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefill the name field if it was successfully entered before
|
|
||||||
var savedName = localStorage.getItem('name')
|
|
||||||
if (savedName && qp)
|
|
||||||
document.getElementById('name').value = savedName
|
|
||||||
|
|
||||||
// 2nd script, server API communication
|
|
||||||
var name, agreed
|
|
||||||
mform.onsubmit = function(e) {
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
name = e.srcElement[0].value
|
|
||||||
agreed = e.srcElement[1].checked
|
|
||||||
|
|
||||||
sendMainData()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMainData() {
|
|
||||||
|
|
||||||
// POST JSON. See docs/API.md
|
|
||||||
var payload = (qp.action == 'arrival') ?
|
|
||||||
{
|
|
||||||
'room': qp.room,
|
|
||||||
'name': name,
|
|
||||||
'agreetoguidelines': agreed
|
|
||||||
} :
|
|
||||||
{
|
|
||||||
'name': name,
|
|
||||||
'cleanedworkspace': agreed
|
|
||||||
}
|
|
||||||
|
|
||||||
post("/" + qp.action, payload)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function post(url, payload) {
|
|
||||||
|
|
||||||
console.log("Sending payload:", payload)
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
}).then(res => {
|
|
||||||
handleResponse(res)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleResponse(res) {
|
|
||||||
|
|
||||||
console.log("Request complete! response:", res)
|
|
||||||
|
|
||||||
if (Math.floor(res.status / 100) == 2) {
|
|
||||||
|
|
||||||
// Success
|
|
||||||
mform = document.getElementById('mainform')
|
|
||||||
mform.innerHTML = "<h2>Done. Thanks!</h2>"
|
|
||||||
localStorage.setItem('name', name)
|
|
||||||
|
|
||||||
} else if (res.status == 409) {
|
|
||||||
|
|
||||||
// Conflict, more data requested
|
|
||||||
handleRequest(res)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Any other generic error
|
|
||||||
res.text().then(function (text) {
|
|
||||||
alert(text)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRequestSubmit(e, json) {
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
var input = e.srcElement[0].value
|
|
||||||
var iso = new Date(input).toISOString()
|
|
||||||
|
|
||||||
// POST JSON. See docs/API.md
|
|
||||||
var payload = (json.request == 'arrival') ?
|
|
||||||
{
|
|
||||||
'room': qp.room,
|
|
||||||
'name': name,
|
|
||||||
'arrival': iso,
|
|
||||||
'agreetoguidelines': agreed
|
|
||||||
} :
|
|
||||||
{
|
|
||||||
'name': name,
|
|
||||||
'departure': iso,
|
|
||||||
'cleanedworkspace': agreed
|
|
||||||
}
|
|
||||||
|
|
||||||
return post("/" + json.request, payload)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function localISOTimeMinutes(date) {
|
|
||||||
|
|
||||||
var tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
|
||||||
var localISOTime = (new Date(date - tzoffset)).toISOString().slice(0, -1);
|
|
||||||
|
|
||||||
return localISOTime.split(':').slice(0,2).join(':')
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRequest(res) {
|
|
||||||
|
|
||||||
var reqt = {
|
|
||||||
'arrival': 'You probably forgot to sign in when you arrived. Please enter your arrival time now:',
|
|
||||||
'departure': 'You probably forgot to sign out when you left. Please enter your departure time now:'
|
|
||||||
}
|
|
||||||
|
|
||||||
mform.innerHTML = "<h2>Processing Request...</h2>"
|
|
||||||
|
|
||||||
res.json().then(function (json) {
|
|
||||||
|
|
||||||
var aInfo = ''
|
|
||||||
var minD = ''
|
|
||||||
var doubleT = ''
|
|
||||||
if (json.request == 'departure') {
|
|
||||||
var d = new Date(json.arrival.time)
|
|
||||||
var dInfo = d.toString('en-GB').split(' ').slice(0,5).join(' ')
|
|
||||||
aInfo = `Your last arrival was on <b>${dInfo}</b> in room <b>${json.arrival.room}</b>.`
|
|
||||||
minD = `min="${localISOTimeMinutes(d)}"`
|
|
||||||
if (new Date() - d < 3 * 60 * 1000) {
|
|
||||||
doubleT = '<b style="color:red">Your last sign in was less than 3 minutes ago. You might be accidentally trying to sign in twice. If you don\'t intend to log 2 arrivals within the last 3 minutes, please abort below.</b>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = localISOTimeMinutes(new Date())
|
|
||||||
|
|
||||||
document.body.innerHTML +=
|
|
||||||
`<div class="request">
|
|
||||||
<h1>${json.request} missing!</h1>
|
|
||||||
<form id="reqform">
|
|
||||||
<label>
|
|
||||||
${reqt[json.request]}
|
|
||||||
<input type="datetime-local" id="datetime" ${minD} max="${now}" required>
|
|
||||||
${aInfo}
|
|
||||||
</label>
|
|
||||||
${doubleT}
|
|
||||||
<input type="submit">
|
|
||||||
<input type="button" value="Abort" onclick="document.body.innerHTML='<h1>Aborted</h1><form>Nothing was logged.<br>You can close this tab/window now.</form>'">
|
|
||||||
</form>
|
|
||||||
</div>`
|
|
||||||
|
|
||||||
rform = document.getElementById('reqform')
|
|
||||||
rform.onsubmit = async function(e) {
|
|
||||||
await handleRequestSubmit(e, json)
|
|
||||||
document.querySelector('.request').remove()
|
|
||||||
setTimeout(sendMainData, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
var spage = document.getElementById('startpage')
|
||||||
|
var mform = document.getElementById('mainform')
|
||||||
|
|
||||||
|
if (qp) {
|
||||||
|
spage.style.display = 'none'
|
||||||
|
mform.style.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefill the name field if it was successfully entered before
|
||||||
|
var savedName = localStorage.getItem('name')
|
||||||
|
if (savedName && qp)
|
||||||
|
document.getElementById('name').value = savedName
|
||||||
|
|
||||||
|
// 2nd script, server API communication
|
||||||
|
var name, agreed, tested
|
||||||
|
mform.onsubmit = function(e) {
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
name = e.srcElement[0].value
|
||||||
|
agreed = e.srcElement[1].checked
|
||||||
|
if (e.srcElement.length > 2)
|
||||||
|
tested = e.srcElement[2].checked
|
||||||
|
|
||||||
|
sendMainData()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMainData() {
|
||||||
|
|
||||||
|
// POST JSON. See docs/API.md
|
||||||
|
var payload = (qp.action == 'arrival') ?
|
||||||
|
{
|
||||||
|
'room': qp.room,
|
||||||
|
'name': name,
|
||||||
|
'agreetoguidelines': agreed,
|
||||||
|
'tested': tested
|
||||||
|
} :
|
||||||
|
{
|
||||||
|
'name': name,
|
||||||
|
'cleanedworkspace': agreed
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/" + qp.action, payload)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(url, payload) {
|
||||||
|
|
||||||
|
console.log("Sending payload:", payload)
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
}).then(res => {
|
||||||
|
handleResponse(res)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResponse(res) {
|
||||||
|
|
||||||
|
console.log("Request complete! response:", res)
|
||||||
|
|
||||||
|
if (Math.floor(res.status / 100) == 2) {
|
||||||
|
|
||||||
|
// Success
|
||||||
|
mform = document.getElementById('mainform')
|
||||||
|
mform.innerHTML = "<h2>Done. Thanks!</h2>"
|
||||||
|
localStorage.setItem('name', name)
|
||||||
|
|
||||||
|
} else if (res.status == 409) {
|
||||||
|
|
||||||
|
// Conflict, more data requested
|
||||||
|
handleRequest(res)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Any other generic error
|
||||||
|
res.text().then(function (text) {
|
||||||
|
alert(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequestSubmit(e, json) {
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
var input = e.srcElement[0].value
|
||||||
|
var iso = new Date(input).toISOString()
|
||||||
|
|
||||||
|
// POST JSON. See docs/API.md
|
||||||
|
var payload = (json.request == 'arrival') ?
|
||||||
|
{
|
||||||
|
'room': qp.room,
|
||||||
|
'name': name,
|
||||||
|
'arrival': iso,
|
||||||
|
'agreetoguidelines': agreed
|
||||||
|
} :
|
||||||
|
{
|
||||||
|
'name': name,
|
||||||
|
'departure': iso,
|
||||||
|
'cleanedworkspace': agreed
|
||||||
|
}
|
||||||
|
|
||||||
|
return post("/" + json.request, payload)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function localISOTimeMinutes(date) {
|
||||||
|
|
||||||
|
var tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
||||||
|
var localISOTime = (new Date(date - tzoffset)).toISOString().slice(0, -1);
|
||||||
|
|
||||||
|
return localISOTime.split(':').slice(0,2).join(':')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequest(res) {
|
||||||
|
|
||||||
|
var reqt = {
|
||||||
|
'arrival': 'You probably forgot to sign in when you arrived. Please enter your arrival time now:',
|
||||||
|
'departure': 'You probably forgot to sign out when you left. Please enter your departure time now:'
|
||||||
|
}
|
||||||
|
|
||||||
|
mform.innerHTML = "<h2>Processing Request...</h2>"
|
||||||
|
|
||||||
|
res.json().then(function (json) {
|
||||||
|
|
||||||
|
var aInfo = ''
|
||||||
|
var minD = ''
|
||||||
|
var doubleT = ''
|
||||||
|
if (json.request == 'departure') {
|
||||||
|
var d = new Date(json.arrival.time)
|
||||||
|
var dInfo = d.toString('en-GB').split(' ').slice(0,5).join(' ')
|
||||||
|
aInfo = `Your last arrival was on <b>${dInfo}</b> in room <b>${json.arrival.room}</b>.`
|
||||||
|
minD = `min="${localISOTimeMinutes(d)}"`
|
||||||
|
if (new Date() - d < 3 * 60 * 1000) {
|
||||||
|
doubleT = '<b style="color:red">Your last sign in was less than 3 minutes ago. You might be accidentally trying to sign in twice. If you don\'t intend to log 2 arrivals within the last 3 minutes, please abort below.</b>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = localISOTimeMinutes(new Date())
|
||||||
|
|
||||||
|
document.body.innerHTML +=
|
||||||
|
`<div class="request">
|
||||||
|
<h1>${json.request} missing!</h1>
|
||||||
|
<form id="reqform">
|
||||||
|
<label>
|
||||||
|
${reqt[json.request]}
|
||||||
|
<input type="datetime-local" id="datetime" ${minD} max="${now}" required>
|
||||||
|
${aInfo}
|
||||||
|
</label>
|
||||||
|
${doubleT}
|
||||||
|
<input type="submit">
|
||||||
|
<input type="button" value="Abort" onclick="document.body.innerHTML='<h1>Aborted</h1><form>Nothing was logged.<br>You can close this tab/window now.</form>'">
|
||||||
|
</form>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
rform = document.getElementById('reqform')
|
||||||
|
rform.onsubmit = async function(e) {
|
||||||
|
await handleRequestSubmit(e, json)
|
||||||
|
document.querySelector('.request').remove()
|
||||||
|
setTimeout(sendMainData, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
background: #ddd;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #eee;
|
||||||
|
background: #c50e1f;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
form, #startpage {
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 512px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
label.checkbox {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
label span {
|
||||||
|
width: calc(100% - 50px);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
border: none;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
input[type=text] {
|
||||||
|
color: #000;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
}
|
||||||
|
input[type=submit], input[type=button] {
|
||||||
|
background: #c50e1f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
input[type=checkbox] {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
float: left;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.request {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
background: #ddd;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
box-shadow: 0 1px 4px 0;
|
||||||
|
}
|
||||||
|
input[type=datetime-local] {
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
height: calc(38px - 16px);
|
||||||
|
padding: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
main > section {
|
||||||
|
height: calc(100% - 32px);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
main > .viewheader {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
main > section.names, #nameheader {
|
||||||
|
width: 199px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
main > section.times, #timeheader {
|
||||||
|
width: calc(100% - 200px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.scroll {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
.row, .row #timelabels {
|
||||||
|
position: relative;
|
||||||
|
height: 32px;
|
||||||
|
background-size: 60px 100%;
|
||||||
|
}
|
||||||
|
.names .row:nth-child(even) { background: #ddd; }
|
||||||
|
.names .row:nth-child(odd) { background: #eee; }
|
||||||
|
.times .row:nth-child(even) {
|
||||||
|
background-image: linear-gradient(to right, #bbb 1px, #ddd 1px);
|
||||||
|
}
|
||||||
|
.times .row:nth-child(odd) {
|
||||||
|
background-image: linear-gradient(to right, #bbb 1px, #eee 1px);
|
||||||
|
}
|
||||||
|
.row span {
|
||||||
|
height: 18px;
|
||||||
|
padding: 7px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.times span, #timeheader span {
|
||||||
|
position: absolute;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
.times span {
|
||||||
|
background: #c50e1f;
|
||||||
|
color: #ddd;
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-text-stroke: .4px #c50e1f;
|
||||||
|
}
|
||||||
|
.times span.implausible {
|
||||||
|
background: linear-gradient(to right, #c50e1f, rgba(197,14,31,0.2) 1000px);
|
||||||
|
}
|
||||||
|
.viewheader.row {
|
||||||
|
height: 30px;
|
||||||
|
background: #ddd !important;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
}
|
||||||
|
.viewheader span {
|
||||||
|
background: none !important;
|
||||||
|
color: #000 !important;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
#credprompt {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 95%;
|
||||||
|
max-width: 320px;
|
||||||
|
margin: auto;
|
||||||
|
background: #ddd;
|
||||||
|
box-shadow: 0 0 0 10000px rgba(0,0,0,.75);
|
||||||
|
}
|
||||||
|
#credprompt h1 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #eee;
|
||||||
|
background: #c50e1f;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#credprompt input {
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
margin: 16px auto;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#credprompt input[type=text],
|
||||||
|
#credprompt input[type=password] {
|
||||||
|
color: #000;
|
||||||
|
width: calc(100% - 64px);
|
||||||
|
}
|
||||||
|
#credprompt input[type=submit] {
|
||||||
|
background: #c50e1f;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
width: calc(100% - 32px);
|
||||||
|
}
|
403
web/view.html
403
web/view.html
|
@ -2,130 +2,10 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>FTracker Data</title>
|
<title>FTracker Data</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
<meta name="theme-color" content="#c50e1f">
|
<meta name="theme-color" content="#c50e1f">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<style>
|
<link href="view.css" rel="stylesheet" type="text/css">
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
height: 100%;
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
header {
|
|
||||||
height: calc(38px - 16px);
|
|
||||||
padding: 8px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
main {
|
|
||||||
height: calc(100% - 38px);
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
main > section {
|
|
||||||
height: calc(100% - 32px);
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
main > .viewheader {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
main > section.names, #nameheader {
|
|
||||||
width: 199px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-right: 1px solid gray;
|
|
||||||
font-weight: bold;
|
|
||||||
text-transform: capitalize;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
main > section.times, #timeheader {
|
|
||||||
width: calc(100% - 200px);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.scroll {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
.row, .row #timelabels {
|
|
||||||
position: relative;
|
|
||||||
height: 32px;
|
|
||||||
background-size: 60px 100%;
|
|
||||||
}
|
|
||||||
.names .row:nth-child(even) { background: #ddd; }
|
|
||||||
.names .row:nth-child(odd) { background: #eee; }
|
|
||||||
.times .row:nth-child(even) {
|
|
||||||
background-image: linear-gradient(to right, #bbb 1px, #ddd 1px);
|
|
||||||
}
|
|
||||||
.times .row:nth-child(odd) {
|
|
||||||
background-image: linear-gradient(to right, #bbb 1px, #eee 1px);
|
|
||||||
}
|
|
||||||
.row span {
|
|
||||||
padding: 7px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.times span, #timeheader span {
|
|
||||||
position: absolute;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
.times span {
|
|
||||||
background: #c50e1f;
|
|
||||||
color: #ddd;
|
|
||||||
font-weight: bold;
|
|
||||||
-webkit-text-stroke: .4px #c50e1f;
|
|
||||||
}
|
|
||||||
.times span.implausible {
|
|
||||||
background: linear-gradient(to right, #c50e1f, rgba(197,14,31,0.2) 1000px);
|
|
||||||
}
|
|
||||||
.viewheader.row {
|
|
||||||
height: 30px;
|
|
||||||
background: #ddd !important;
|
|
||||||
border-top: 1px solid gray;
|
|
||||||
border-bottom: 1px solid gray;
|
|
||||||
}
|
|
||||||
.viewheader span {
|
|
||||||
background: none !important;
|
|
||||||
color: #000 !important;
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
#credprompt {
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 95%;
|
|
||||||
max-width: 320px;
|
|
||||||
margin: auto;
|
|
||||||
background: #ddd;
|
|
||||||
box-shadow: 0 0 0 10000px rgba(0,0,0,.75);
|
|
||||||
}
|
|
||||||
#credprompt h1 {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #eee;
|
|
||||||
background: #c50e1f;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#credprompt input {
|
|
||||||
display: block;
|
|
||||||
border: none;
|
|
||||||
margin: 16px auto;
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
#credprompt input[type=text],
|
|
||||||
#credprompt input[type=password] {
|
|
||||||
color: #000;
|
|
||||||
width: calc(100% - 64px);
|
|
||||||
}
|
|
||||||
#credprompt input[type=submit] {
|
|
||||||
background: #c50e1f;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff;
|
|
||||||
width: calc(100% - 32px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
|
@ -151,283 +31,6 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script src="view.js"></script>
|
||||||
var data = null;
|
|
||||||
|
|
||||||
var names = document.querySelector('main #names')
|
|
||||||
var times = document.querySelector('main #times')
|
|
||||||
|
|
||||||
function exportCSV() {
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
alert('No data found.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var startInput = document.querySelector('input#start')
|
|
||||||
var endInput = document.querySelector('input#end')
|
|
||||||
var roomInput = document.querySelector('input#room')
|
|
||||||
|
|
||||||
var startDate = new Date(startInput.value)
|
|
||||||
var endDate = new Date(endInput.value)
|
|
||||||
var roomRE = new RegExp(roomInput.value || '.*')
|
|
||||||
|
|
||||||
csv = '"ftracker-export",'
|
|
||||||
days = []
|
|
||||||
|
|
||||||
var tc = new Date(startDate.getTime())
|
|
||||||
tc.setHours(1,0,0,0)
|
|
||||||
while (tc < endDate) {
|
|
||||||
var isodate = tc.toISOString().split('T')[0]
|
|
||||||
csv += ('"' + isodate + '",')
|
|
||||||
days.push(isodate)
|
|
||||||
tc.setDate(tc.getDate() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
csv = csv.replace(/,$/, '')
|
|
||||||
|
|
||||||
csv += '\n'
|
|
||||||
|
|
||||||
for (var [name, list] of Object.entries(data)) {
|
|
||||||
|
|
||||||
csv += '"' + name + '"'
|
|
||||||
|
|
||||||
for (day of days) {
|
|
||||||
|
|
||||||
csv += ',"'
|
|
||||||
|
|
||||||
daytexts = []
|
|
||||||
|
|
||||||
for (entry of list) {
|
|
||||||
|
|
||||||
if (entry.room.match(roomRE) == null)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var arrD = new Date(entry.arrival)
|
|
||||||
var depD = entry.departure ? new Date(entry.departure) : new Date()
|
|
||||||
|
|
||||||
if (depD < startDate || arrD > endDate)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var [arrDay, arrT] = localISOTimeMinutes(arrD).split('T')
|
|
||||||
var [depDay, depT] = localISOTimeMinutes(depD).split('T')
|
|
||||||
|
|
||||||
if ((arrDay == day) && (depDay == day)) {
|
|
||||||
daytexts.push(arrT + '-' + depT + ' (' + entry.room + ')')
|
|
||||||
} else if (arrDay == day) {
|
|
||||||
daytexts.push(arrT + '-... (' + entry.room + ')')
|
|
||||||
} else if (depDay == day) {
|
|
||||||
daytexts.push('...-' + depT + ' (' + entry.room + ')')
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
csv += daytexts.join('\n')
|
|
||||||
|
|
||||||
csv += '"'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
csv += '\n'
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var element = document.createElement('a');
|
|
||||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csv));
|
|
||||||
element.setAttribute('download', 'ftracker-export.csv');
|
|
||||||
element.style.display = 'none';
|
|
||||||
document.body.appendChild(element);
|
|
||||||
element.click();
|
|
||||||
document.body.removeChild(element);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderData() {
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
alert('No data found.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
names = document.querySelector('main #names')
|
|
||||||
times = document.querySelector('main #times')
|
|
||||||
|
|
||||||
names.innerHTML = ''
|
|
||||||
times.innerHTML = ''
|
|
||||||
|
|
||||||
var startInput = document.querySelector('input#start')
|
|
||||||
var endInput = document.querySelector('input#end')
|
|
||||||
var roomInput = document.querySelector('input#room')
|
|
||||||
|
|
||||||
var startDate = new Date(startInput.value)
|
|
||||||
var endDate = new Date(endInput.value)
|
|
||||||
var roomRE = new RegExp(roomInput.value || '.*')
|
|
||||||
|
|
||||||
// Clear everything below hours because that would lead to
|
|
||||||
// misalignments with the day grid
|
|
||||||
startDate.setMinutes(0,0,0)
|
|
||||||
|
|
||||||
var tc = new Date(startDate.getTime())
|
|
||||||
var content = ''
|
|
||||||
while (tc < endDate) {
|
|
||||||
var h = tc.getHours()
|
|
||||||
var t = (h == 0) ?
|
|
||||||
'<b>'+tc.getDate()+'.'+(tc.getMonth()+1)+'.</b>' :
|
|
||||||
h+':00'
|
|
||||||
var left = ((tc - startDate) / (1000 * 60))
|
|
||||||
content += '<span style="left:'+left+'px;">'+t+'</span>'
|
|
||||||
tc.setTime(tc.getTime() + (60*60*1000));
|
|
||||||
}
|
|
||||||
var timeheader = document.getElementById('timelabels')
|
|
||||||
timeheader.innerHTML = content
|
|
||||||
|
|
||||||
var viewwidth = ((endDate - startDate) / (1000 * 60))
|
|
||||||
timeheader.style.width = viewwidth + 'px'
|
|
||||||
times.style.width = viewwidth + 'px'
|
|
||||||
|
|
||||||
var rowCount = 0
|
|
||||||
|
|
||||||
for (var [name, list] of Object.entries(data)) {
|
|
||||||
|
|
||||||
var row = document.createElement('div')
|
|
||||||
row.classList.add('row')
|
|
||||||
|
|
||||||
var rowHasBlock = false
|
|
||||||
|
|
||||||
for (entry of list) {
|
|
||||||
|
|
||||||
if (entry.room.match(roomRE) == null)
|
|
||||||
continue
|
|
||||||
|
|
||||||
var arrD = new Date(entry.arrival)
|
|
||||||
var depD = entry.departure ? new Date(entry.departure) : new Date()
|
|
||||||
|
|
||||||
if (depD < startDate || arrD > endDate)
|
|
||||||
continue
|
|
||||||
|
|
||||||
rowHasBlock = true
|
|
||||||
|
|
||||||
// Minutes since start date / beginning
|
|
||||||
var arr = (arrD - startDate) / (1000 * 60)
|
|
||||||
var dep = (depD - startDate) / (1000 * 60)
|
|
||||||
var dur = dep - arr
|
|
||||||
|
|
||||||
var block = document.createElement('span')
|
|
||||||
block.innerHTML = entry.room
|
|
||||||
block.style.left = arr + 'px' // 1px/min
|
|
||||||
block.style.width = Math.max(0,(dur-14)) + 'px' // 1px/min
|
|
||||||
|
|
||||||
if (dur > 60 * 24)
|
|
||||||
block.classList.add('implausible')
|
|
||||||
|
|
||||||
row.appendChild(block)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowHasBlock) {
|
|
||||||
var vname = name.replace(/-/g, ' ')
|
|
||||||
names.innerHTML += '<div class="row"><span>'+vname+'</span></div>'
|
|
||||||
times.appendChild(row)
|
|
||||||
rowCount += 12
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//var viewheight = rowCount * 32;
|
|
||||||
//times.style.height = viewheight + 'px'
|
|
||||||
|
|
||||||
var tw = document.querySelector('main .scroll')
|
|
||||||
tw.scrollLeft = tw.scrollWidth
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveData(rdata) {
|
|
||||||
|
|
||||||
data = rdata.reduce((acc, entry) => {
|
|
||||||
var name = entry.name
|
|
||||||
delete entry.name
|
|
||||||
acc[name] = [...acc[name] || [], entry];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
console.log(data)
|
|
||||||
renderData()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitCredentials() {
|
|
||||||
var cp = document.querySelector('#credprompt')
|
|
||||||
var user = cp.querySelector('#user').value
|
|
||||||
var pass = cp.querySelector('#pass').value
|
|
||||||
cp.remove()
|
|
||||||
var auth = btoa(user + ":" + pass)
|
|
||||||
localStorage.setItem('dataauth', auth)
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadData() {
|
|
||||||
|
|
||||||
var auth = localStorage.getItem('dataauth')
|
|
||||||
if (auth == null) {
|
|
||||||
var prompt = document.createElement('div')
|
|
||||||
prompt.id = 'credprompt'
|
|
||||||
prompt.innerHTML = '<h1>Credentials Required</h1>\
|
|
||||||
<input type="text" id="user" placeholder="username" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
|
||||||
<input type="password" id="pass" placeholder="password" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
|
||||||
<input type="submit" onclick="submitCredentials()">'
|
|
||||||
document.body.appendChild(prompt)
|
|
||||||
document.querySelector('#credprompt #user').focus()
|
|
||||||
return // Abort load, wait for submit
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Headers()
|
|
||||||
headers.append('Authorization', 'Basic ' + auth)
|
|
||||||
|
|
||||||
var fetchopts = {
|
|
||||||
method: 'GET',
|
|
||||||
headers: headers
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch('/data', fetchopts)
|
|
||||||
.then(res => {
|
|
||||||
if (Math.floor(res.status / 100) == 2)
|
|
||||||
return res.json()
|
|
||||||
else
|
|
||||||
localStorage.removeItem('dataauth')
|
|
||||||
res.text().then(function (text) {
|
|
||||||
alert(text)
|
|
||||||
location.reload()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(rdata => saveData(rdata))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function localISOTimeMinutes(date) {
|
|
||||||
|
|
||||||
var tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
|
||||||
var localISOTime = (new Date(date - tzoffset)).toISOString().slice(0, -1);
|
|
||||||
|
|
||||||
return localISOTime.split(':').slice(0,2).join(':')
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = new Date()
|
|
||||||
var startDate = new Date()
|
|
||||||
startDate.setDate(now.getDate() - (4*7))
|
|
||||||
startDate.setHours(0,0,0,0)
|
|
||||||
document.querySelector('input#start').value = localISOTimeMinutes(startDate)
|
|
||||||
document.querySelector('input#end').value = localISOTimeMinutes(now)
|
|
||||||
|
|
||||||
var scrollbox = document.querySelector('.scroll')
|
|
||||||
var timehead = document.querySelector('#timeheader')
|
|
||||||
var namebox = document.querySelector('section.names')
|
|
||||||
scrollbox.onscroll = function() {
|
|
||||||
timehead.scrollLeft = scrollbox.scrollLeft
|
|
||||||
namebox.scrollTop = scrollbox.scrollTop
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData()
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,278 @@
|
||||||
|
var data = null;
|
||||||
|
|
||||||
|
var names = document.querySelector('main #names')
|
||||||
|
var times = document.querySelector('main #times')
|
||||||
|
|
||||||
|
function exportCSV() {
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
alert('No data found.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var startInput = document.querySelector('input#start')
|
||||||
|
var endInput = document.querySelector('input#end')
|
||||||
|
var roomInput = document.querySelector('input#room')
|
||||||
|
|
||||||
|
var startDate = new Date(startInput.value)
|
||||||
|
var endDate = new Date(endInput.value)
|
||||||
|
var roomRE = new RegExp(roomInput.value || '.*')
|
||||||
|
|
||||||
|
csv = '"ftracker-export",'
|
||||||
|
days = []
|
||||||
|
|
||||||
|
var tc = new Date(startDate.getTime())
|
||||||
|
tc.setHours(1,0,0,0)
|
||||||
|
while (tc < endDate) {
|
||||||
|
var isodate = tc.toISOString().split('T')[0]
|
||||||
|
csv += ('"' + isodate + '",')
|
||||||
|
days.push(isodate)
|
||||||
|
tc.setDate(tc.getDate() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
csv = csv.replace(/,$/, '')
|
||||||
|
|
||||||
|
csv += '\n'
|
||||||
|
|
||||||
|
for (var [name, list] of Object.entries(data)) {
|
||||||
|
|
||||||
|
csv += '"' + name + '"'
|
||||||
|
|
||||||
|
for (day of days) {
|
||||||
|
|
||||||
|
csv += ',"'
|
||||||
|
|
||||||
|
daytexts = []
|
||||||
|
|
||||||
|
for (entry of list) {
|
||||||
|
|
||||||
|
if (entry.room.match(roomRE) == null)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var arrD = new Date(entry.arrival)
|
||||||
|
var depD = entry.departure ? new Date(entry.departure) : new Date()
|
||||||
|
|
||||||
|
if (depD < startDate || arrD > endDate)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var [arrDay, arrT] = localISOTimeMinutes(arrD).split('T')
|
||||||
|
var [depDay, depT] = localISOTimeMinutes(depD).split('T')
|
||||||
|
|
||||||
|
if ((arrDay == day) && (depDay == day)) {
|
||||||
|
daytexts.push(arrT + '-' + depT + ' (' + entry.room + ')')
|
||||||
|
} else if (arrDay == day) {
|
||||||
|
daytexts.push(arrT + '-... (' + entry.room + ')')
|
||||||
|
} else if (depDay == day) {
|
||||||
|
daytexts.push('...-' + depT + ' (' + entry.room + ')')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
csv += daytexts.join('\n')
|
||||||
|
|
||||||
|
csv += '"'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
csv += '\n'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var element = document.createElement('a');
|
||||||
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csv));
|
||||||
|
element.setAttribute('download', 'ftracker-export.csv');
|
||||||
|
element.style.display = 'none';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderData() {
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
alert('No data found.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
names = document.querySelector('main #names')
|
||||||
|
times = document.querySelector('main #times')
|
||||||
|
|
||||||
|
names.innerHTML = ''
|
||||||
|
times.innerHTML = ''
|
||||||
|
|
||||||
|
var startInput = document.querySelector('input#start')
|
||||||
|
var endInput = document.querySelector('input#end')
|
||||||
|
var roomInput = document.querySelector('input#room')
|
||||||
|
|
||||||
|
var startDate = new Date(startInput.value)
|
||||||
|
var endDate = new Date(endInput.value)
|
||||||
|
var roomRE = new RegExp(roomInput.value || '.*')
|
||||||
|
|
||||||
|
// Clear everything below hours because that would lead to
|
||||||
|
// misalignments with the day grid
|
||||||
|
startDate.setMinutes(0,0,0)
|
||||||
|
|
||||||
|
var tc = new Date(startDate.getTime())
|
||||||
|
var content = ''
|
||||||
|
while (tc < endDate) {
|
||||||
|
var h = tc.getHours()
|
||||||
|
var t = (h == 0) ?
|
||||||
|
'<b>'+tc.getDate()+'.'+(tc.getMonth()+1)+'.</b>' :
|
||||||
|
h+':00'
|
||||||
|
var left = ((tc - startDate) / (1000 * 60))
|
||||||
|
content += '<span style="left:'+left+'px;">'+t+'</span>'
|
||||||
|
tc.setTime(tc.getTime() + (60*60*1000));
|
||||||
|
}
|
||||||
|
var timeheader = document.getElementById('timelabels')
|
||||||
|
timeheader.innerHTML = content
|
||||||
|
|
||||||
|
var viewwidth = ((endDate - startDate) / (1000 * 60))
|
||||||
|
timeheader.style.width = viewwidth + 'px'
|
||||||
|
times.style.width = viewwidth + 'px'
|
||||||
|
|
||||||
|
var rowCount = 0
|
||||||
|
|
||||||
|
for (var [name, list] of Object.entries(data)) {
|
||||||
|
|
||||||
|
var row = document.createElement('div')
|
||||||
|
row.classList.add('row')
|
||||||
|
|
||||||
|
var rowHasBlock = false
|
||||||
|
|
||||||
|
for (entry of list) {
|
||||||
|
|
||||||
|
if (entry.room.match(roomRE) == null)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var arrD = new Date(entry.arrival)
|
||||||
|
var depD = entry.departure ? new Date(entry.departure) : new Date()
|
||||||
|
|
||||||
|
if (depD < startDate || arrD > endDate)
|
||||||
|
continue
|
||||||
|
|
||||||
|
rowHasBlock = true
|
||||||
|
|
||||||
|
// Minutes since start date / beginning
|
||||||
|
var arr = (arrD - startDate) / (1000 * 60)
|
||||||
|
var dep = (depD - startDate) / (1000 * 60)
|
||||||
|
var dur = dep - arr
|
||||||
|
|
||||||
|
var block = document.createElement('span')
|
||||||
|
block.innerHTML = entry.room
|
||||||
|
block.style.left = arr + 'px' // 1px/min
|
||||||
|
block.style.width = Math.max(0,(dur-14)) + 'px' // 1px/min
|
||||||
|
if (entry.tested)
|
||||||
|
block.style.background = '#080'
|
||||||
|
|
||||||
|
if (dur > 60 * 24)
|
||||||
|
block.classList.add('implausible')
|
||||||
|
|
||||||
|
row.appendChild(block)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowHasBlock) {
|
||||||
|
var vname = name.replace(/-/g, ' ')
|
||||||
|
names.innerHTML += '<div class="row"><span>'+vname+'</span></div>'
|
||||||
|
times.appendChild(row)
|
||||||
|
rowCount += 12
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//var viewheight = rowCount * 32;
|
||||||
|
//times.style.height = viewheight + 'px'
|
||||||
|
|
||||||
|
var tw = document.querySelector('main .scroll')
|
||||||
|
tw.scrollLeft = tw.scrollWidth
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveData(rdata) {
|
||||||
|
|
||||||
|
data = rdata.reduce((acc, entry) => {
|
||||||
|
var name = entry.name
|
||||||
|
delete entry.name
|
||||||
|
acc[name] = [...acc[name] || [], entry];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
console.log(data)
|
||||||
|
renderData()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitCredentials() {
|
||||||
|
var cp = document.querySelector('#credprompt')
|
||||||
|
var user = cp.querySelector('#user').value
|
||||||
|
var pass = cp.querySelector('#pass').value
|
||||||
|
cp.remove()
|
||||||
|
var auth = btoa(user + ":" + pass)
|
||||||
|
localStorage.setItem('dataauth', auth)
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
|
||||||
|
var auth = localStorage.getItem('dataauth')
|
||||||
|
if (auth == null) {
|
||||||
|
var prompt = document.createElement('div')
|
||||||
|
prompt.id = 'credprompt'
|
||||||
|
prompt.innerHTML = '<h1>Credentials Required</h1>\
|
||||||
|
<input type="text" id="user" placeholder="username" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
||||||
|
<input type="password" id="pass" placeholder="password" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
||||||
|
<input type="submit" onclick="submitCredentials()">'
|
||||||
|
document.body.appendChild(prompt)
|
||||||
|
document.querySelector('#credprompt #user').focus()
|
||||||
|
return // Abort load, wait for submit
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Headers()
|
||||||
|
headers.append('Authorization', 'Basic ' + auth)
|
||||||
|
|
||||||
|
var fetchopts = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: headers
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/data', fetchopts)
|
||||||
|
.then(res => {
|
||||||
|
if (Math.floor(res.status / 100) == 2)
|
||||||
|
return res.json()
|
||||||
|
else
|
||||||
|
localStorage.removeItem('dataauth')
|
||||||
|
res.text().then(function (text) {
|
||||||
|
alert(text)
|
||||||
|
location.reload()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(rdata => saveData(rdata))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function localISOTimeMinutes(date) {
|
||||||
|
|
||||||
|
var tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
||||||
|
var localISOTime = (new Date(date - tzoffset)).toISOString().slice(0, -1);
|
||||||
|
|
||||||
|
return localISOTime.split(':').slice(0,2).join(':')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = new Date()
|
||||||
|
var startDate = new Date()
|
||||||
|
startDate.setDate(now.getDate() - (4*7))
|
||||||
|
startDate.setHours(0,0,0,0)
|
||||||
|
document.querySelector('input#start').value = localISOTimeMinutes(startDate)
|
||||||
|
document.querySelector('input#end').value = localISOTimeMinutes(now)
|
||||||
|
|
||||||
|
var scrollbox = document.querySelector('.scroll')
|
||||||
|
var timehead = document.querySelector('#timeheader')
|
||||||
|
var namebox = document.querySelector('section.names')
|
||||||
|
scrollbox.onscroll = function() {
|
||||||
|
timehead.scrollLeft = scrollbox.scrollLeft
|
||||||
|
namebox.scrollTop = scrollbox.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData()
|
Loading…
Reference in New Issue