Split down files and add question about covid-test
This commit is contained in:
		
							
								
								
									
										293
									
								
								web/index.html
									
									
									
									
									
								
							
							
						
						
									
										293
									
								
								web/index.html
									
									
									
									
									
								
							@ -2,89 +2,10 @@
 | 
			
		||||
<html>
 | 
			
		||||
	<head>
 | 
			
		||||
		<title>FTracker</title>
 | 
			
		||||
		<meta charset="utf-8">
 | 
			
		||||
		<meta name="theme-color" content="#c50e1f">
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 | 
			
		||||
		<style>
 | 
			
		||||
			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>
 | 
			
		||||
		<link href="style.css" rel="stylesheet" type="text/css">
 | 
			
		||||
		<script>
 | 
			
		||||
			// 1st script, prepares values needed for writing document
 | 
			
		||||
			var cbt = {
 | 
			
		||||
@ -114,201 +35,41 @@
 | 
			
		||||
			else
 | 
			
		||||
				document.write('FTracker<br>V1')
 | 
			
		||||
		</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>
 | 
			
		||||
				Full Name:<br>
 | 
			
		||||
				<input type="text" name="name" id="name" placeholder="John Doe" required>
 | 
			
		||||
			</label>
 | 
			
		||||
			<label id="agreelabel">
 | 
			
		||||
			<label class="checkbox">
 | 
			
		||||
				<input type="checkbox" name="agree" id="agree" required>
 | 
			
		||||
				<span><script>
 | 
			
		||||
					document.write(qp ? cbt[qp.action] : '')
 | 
			
		||||
				</script></span>
 | 
			
		||||
			</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">
 | 
			
		||||
		</form>
 | 
			
		||||
		<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>
 | 
			
		||||
		<script src="main.js"></script>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										172
									
								
								web/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								web/main.js
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								web/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								web/style.css
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								web/view.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								web/view.css
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
			
		||||
	<head>
 | 
			
		||||
		<title>FTracker Data</title>
 | 
			
		||||
		<meta charset="utf-8">
 | 
			
		||||
		<meta name="theme-color" content="#c50e1f">
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 | 
			
		||||
		<style>
 | 
			
		||||
			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>
 | 
			
		||||
		<link href="view.css" rel="stylesheet" type="text/css">
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		<header>
 | 
			
		||||
@ -151,283 +31,6 @@
 | 
			
		||||
				</div>
 | 
			
		||||
			</section>
 | 
			
		||||
		</main>
 | 
			
		||||
		<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>
 | 
			
		||||
		<script src="view.js"></script>
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										278
									
								
								web/view.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								web/view.js
									
									
									
									
									
										Normal file
									
								
							@ -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()
 | 
			
		||||
		Reference in New Issue
	
	Block a user