Initial commit
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | .idea | ||||||
|  | credentials.* | ||||||
|  | !*.sample.* | ||||||
							
								
								
									
										6
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  |  | ||||||
|  | # Elwig Access Frontend | ||||||
|  |  | ||||||
|  | The web frontend for accessing Elwig online. | ||||||
|  |  | ||||||
|  | https://access.elwig.at/ | ||||||
							
								
								
									
										18
									
								
								www/.php/credentials.sample.inc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								www/.php/credentials.sample.inc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | <?php | ||||||
|  | global $CLIENT_ACCESS; | ||||||
|  | global $COMBINED_ACCESS; | ||||||
|  |  | ||||||
|  | $CLIENT_ACCESS = [ | ||||||
|  |     'WGX' => [ | ||||||
|  |         'name' => 'Winzergenossenschaft Ort', | ||||||
|  |         'short' => 'Ort', | ||||||
|  |         'api' => 'https://example.com/elwig/api/v1', | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | $COMBINED_ACCESS = [ | ||||||
|  |     'HOLDING' => [ | ||||||
|  |         'name' => 'Name', | ||||||
|  |         'clients' => ['WGX'], | ||||||
|  |     ], | ||||||
|  | ]; | ||||||
							
								
								
									
										
											BIN
										
									
								
								www/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								www/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 229 KiB | 
							
								
								
									
										100
									
								
								www/index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								www/index.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | <?php | ||||||
|  | require ".php/credentials.inc"; | ||||||
|  | global $CLIENT_ACCESS; | ||||||
|  | global $COMBINED_ACCESS; | ||||||
|  |  | ||||||
|  | $info = explode('/', $_SERVER['PATH_INFO']); | ||||||
|  | $client = null; | ||||||
|  | foreach ($CLIENT_ACCESS as $name => $data) { | ||||||
|  |     if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) { | ||||||
|  |         $client = $data; | ||||||
|  |         $client['id'] = $name; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | foreach ($COMBINED_ACCESS as $name => $data) { | ||||||
|  |     if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) { | ||||||
|  |         $client = $data; | ||||||
|  |         $client['id'] = $name; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | if ($client === null) { | ||||||
|  |     header('Status: 200'); | ||||||
|  |     header('Content-Length: 0'); | ||||||
|  |     exit(); | ||||||
|  | } else if ($_SERVER['PATH_INFO'] !== "/$client[id]/") { | ||||||
|  |     header('Status: 308'); | ||||||
|  |     header("Location: /$client[id]/"); | ||||||
|  |     exit(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | if ($client['api']) { | ||||||
|  |     $data = "window.ELWIG_API = \"$client[api]\";"; | ||||||
|  | } else { | ||||||
|  |     $data = "window.CLIENTS = {"; | ||||||
|  |     $first = true; | ||||||
|  |     foreach ($client['clients'] as $id) { | ||||||
|  |         $c = $CLIENT_ACCESS[$id]; | ||||||
|  |         if (!$first) $data .= ", "; | ||||||
|  |         $data .= "\"$id\": {\"name\": \"$c[name]\", \"short\": \"$c[short]\", \"api\": \"$c[api]\"}"; | ||||||
|  |         $first = false; | ||||||
|  |     } | ||||||
|  |     $data .= "};"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | header('Content-Type: application/xhtml+xml; charset=UTF-8'); | ||||||
|  | echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; ?> | ||||||
|  | <html xmlns="http://www.w3.org/1999/xhtml" lang="de"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"/> | ||||||
|  |     <title><?php echo $client['name']; ?> - Elwig</title> | ||||||
|  |     <link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/> | ||||||
|  |     <link rel="stylesheet" href="/res/style.css"/> | ||||||
|  |     <meta name="viewport" content="width=device-width,initial-scale=1.0"/> | ||||||
|  |     <script>window.CLIENT = "<?php echo $info[1]; ?>"; <?php echo $data; ?></script> | ||||||
|  |     <script src="/res/access.js" type="application/javascript"/> | ||||||
|  |     <script src="<?php echo $client['api'] ? "/res/access-single.js" : "/res/access-multiple.js"; ?>" type="application/javascript"/> | ||||||
|  | </head> | ||||||
|  | <body class="header-footer"> | ||||||
|  | <header> | ||||||
|  |     <nav> | ||||||
|  |         <div> | ||||||
|  |             <a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a> | ||||||
|  |         </div> | ||||||
|  |         <ul> | ||||||
|  |             <?php if (isset($client['clients'])) { | ||||||
|  |                 foreach ($client['clients'] as $id) { | ||||||
|  |                     $c = $CLIENT_ACCESS[$id]; | ||||||
|  |                     echo "        <li><a href=\"#/$id\">$c[short]</a></li>\n"; | ||||||
|  |                 } | ||||||
|  |             } else { ?> | ||||||
|  |                 <li><a href="#/">Übersicht</a></li> | ||||||
|  |                 <li><a href="#/mitglied">Mitglied</a></li> | ||||||
|  |                 <li><a href="#/lieferungen">Lieferungen</a></li> | ||||||
|  |                 <li><a href="#/anmeldungen">Anmeldungen</a></li> | ||||||
|  |             <?php } ?> | ||||||
|  |         </ul> | ||||||
|  |         <div> | ||||||
|  |             <?php if (!isset($client['clients'])) { ?> | ||||||
|  |                 <a href="#/mitglied" id="user"> | ||||||
|  |                     <div id="usertext"></div> | ||||||
|  |                     <img src="/res/avatar.png" alt="Avatar" style="border-radius: 50%; height: 2.25em; margin: 0.5em;"/> | ||||||
|  |                 </a> | ||||||
|  |             <?php } ?> | ||||||
|  |         </div> | ||||||
|  |     </nav> | ||||||
|  | </header> | ||||||
|  | <main id="access"/> | ||||||
|  | <footer> | ||||||
|  |     <a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a> | ||||||
|  |     <p> | ||||||
|  |         <strong>Impressum</strong><br/> | ||||||
|  |         Lorenz Stechauner, Thomas Hilscher<br/> | ||||||
|  |         Österreich (Austria)<br/> | ||||||
|  |     </p> | ||||||
|  |     <p> | ||||||
|  |         <strong>Kontakt</strong><br/> | ||||||
|  |         E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/> | ||||||
|  |     </p> | ||||||
|  | </footer> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										189
									
								
								www/res/access-multiple.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								www/res/access-multiple.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | window.CLIENT = window.CLIENT || null; | ||||||
|  | window.CLIENTS = window.CLIENTS || {}; | ||||||
|  |  | ||||||
|  | function getStoredUsername(client) { | ||||||
|  |     return window.localStorage.getItem(`${CLIENT}/${client}/username`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getStoredToken(client) { | ||||||
|  |     return window.localStorage.getItem(`${CLIENT}/${client}/token`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getAuthorizationHeader(client) { | ||||||
|  |     return { | ||||||
|  |         'Authorization': 'Bearer ' + window.localStorage.getItem(`${CLIENT}/${client}/token`), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function authenticate(client, username, password) { | ||||||
|  |     const res = await fetch(`${CLIENTS[client]['api']}/auth`, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: {'Authorization': 'Basic ' + btoa(username + ':' + password)}, | ||||||
|  |     }); | ||||||
|  |     const json = await res.json(); | ||||||
|  |     if (!res.ok) throw new ApiError(res.status, json['message']); | ||||||
|  |     return json['token']; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function get(client, path) { | ||||||
|  |     const res = await fetch(`${CLIENTS[client]['api']}${path}`, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: {...getAuthorizationHeader(client)}, | ||||||
|  |     }); | ||||||
|  |     const json = await res.json(); | ||||||
|  |     if (!res.ok) throw new ApiError(res.status, json['message']); | ||||||
|  |     return json; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getDeliverySchedules(client, filters, limit, offset) { | ||||||
|  |     const query = []; | ||||||
|  |     if (!!filters) query.push(`filters=${filters.join(',')}`); | ||||||
|  |     if (!!limit) query.push(`limit=${limit}`); | ||||||
|  |     if (!!offset) query.push(`offset=${offset}`); | ||||||
|  |     return await get(client, `/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function init() { | ||||||
|  |     render(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateOverview(client) { | ||||||
|  |     const [schedules] = await Promise.all([getDeliverySchedules(client, [`year=${getCurrentLastSeason()}`])]); | ||||||
|  |     const rows = []; | ||||||
|  |     const days = groupBy(schedules['data'], 'date'); | ||||||
|  |     const now = new Date(); | ||||||
|  |     for (const [dateString, day] of Object.entries(days)) { | ||||||
|  |         const date = new Date(dateString); | ||||||
|  |         const row = document.createElement('div'); | ||||||
|  |         row.className = 'day'; | ||||||
|  |         if (now.getFullYear() === date.getFullYear() && now.getMonth() === date.getMonth() && now.getDate() === date.getDate()) | ||||||
|  |             row.classList.add('today'); | ||||||
|  |         row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`; | ||||||
|  |         const container = document.createElement('div'); | ||||||
|  |         container.className = 'schedule-container'; | ||||||
|  |         for (const schedule of day) { | ||||||
|  |             const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null; | ||||||
|  |             const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null; | ||||||
|  |             const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei'; | ||||||
|  |             const link = document.createElement('a'); | ||||||
|  |             if (schedule.is_cancelled) link.className = 'cancelled'; | ||||||
|  |             link.innerHTML += `<div><span style="font-size: 0.75em; display: block;">${escapeHTML(schedule.branch.name)}</span><span>${escapeHTML(schedule.description)}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`; | ||||||
|  |             if (schedule.delivered_weight > 0) { | ||||||
|  |                 link.innerHTML += ` | ||||||
|  |                     <span> | ||||||
|  |                       <span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> / | ||||||
|  |                       <span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span> | ||||||
|  |                       (<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>) | ||||||
|  |                     </span>`; | ||||||
|  |             } else if (schedule.max_weight !== null) { | ||||||
|  |                 link.innerHTML += ` | ||||||
|  |                     <span> | ||||||
|  |                       <span>${fmtInt(schedule.announced_weight)} kg</span> / | ||||||
|  |                       <span class="min-kg">${fmtInt(schedule.max_weight)} kg</span> | ||||||
|  |                       (<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>) | ||||||
|  |                     </span>`; | ||||||
|  |             } else { | ||||||
|  |                 link.innerHTML += `<span><span>${fmtInt(schedule.announced_weight)} kg</span></span>`; | ||||||
|  |             } | ||||||
|  |             container.append(link); | ||||||
|  |         } | ||||||
|  |         row.appendChild(container); | ||||||
|  |         rows.push(row); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const main = document.getElementsByTagName('main')[0]; | ||||||
|  |     main.replaceChildren(main.firstElementChild, ...rows); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function render() { | ||||||
|  |     const hash = window.location.hash; | ||||||
|  |     const main = document.getElementById("access"); | ||||||
|  |     const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0]; | ||||||
|  |     for (const li of nav.children) li.className = ''; | ||||||
|  |  | ||||||
|  |     const client = Object.keys(CLIENTS).find(id => hash.startsWith(`#/${id}/`) || hash === `#/${id}`); | ||||||
|  |     if (client === undefined) { | ||||||
|  |         window.location.hash = `#/${Object.keys(CLIENTS).find(id => !!getStoredUsername(id) && !!getStoredToken(id)) || Object.keys(CLIENTS)[0]}`; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     nav.children[Object.keys(CLIENTS).indexOf(client)].className = 'active'; | ||||||
|  |  | ||||||
|  |     if ((!getStoredUsername(client) || !getStoredToken(client)) && window.location.hash !== `#/${client}/login`) { | ||||||
|  |         window.location.hash = `#/${client}/login`; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (hash === `#/${client}/login`) { | ||||||
|  |         main.className = 'login'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <form onsubmit="return actionLogin(this);"> | ||||||
|  |               <h1>Anmelden</h1> | ||||||
|  |               <input type="text" name="username" placeholder="Benutzername" value="${getStoredUsername(client) ?? ''}"/> | ||||||
|  |               <input type="password" name="password" placeholder="Kennwort"/> | ||||||
|  |               <input type="hidden" name="client" value="${client}"/> | ||||||
|  |               <button type="submit">Anmelden</button> | ||||||
|  |             </form>`; | ||||||
|  |     } else if (hash === `#/${client}`) { | ||||||
|  |         main.className = 'overview'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <h1>${CLIENTS[client].name}</h1>`; | ||||||
|  |         update(); | ||||||
|  |     } else { | ||||||
|  |         window.location.hash = `#/${client}`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function update() { | ||||||
|  |     const hash = window.location.hash; | ||||||
|  |     const client = Object.keys(CLIENTS).find(id => hash.startsWith(`#/${id}/`) || hash === `#/${id}`); | ||||||
|  |     if (document.hidden || client === undefined) { | ||||||
|  |         // do nothing | ||||||
|  |     } else { | ||||||
|  |         if (hash === `#/${client}`) { | ||||||
|  |             updateOverview(client) | ||||||
|  |                 .then() | ||||||
|  |                 .catch(e => { | ||||||
|  |                     if (e instanceof ApiError && e.statusCode === 401) { | ||||||
|  |                         window.localStorage.removeItem(`${CLIENT}/${client}/token`); | ||||||
|  |                         window.location.hash = `#/${client}/login`; | ||||||
|  |                     } else { | ||||||
|  |                         alert(e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? `Unbekannter Fehler: ${e.message}`); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | document.addEventListener('DOMContentLoaded', async () => { | ||||||
|  |     await init(); | ||||||
|  |     setInterval(update, 60_000); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | window.addEventListener('hashchange', render); | ||||||
|  |  | ||||||
|  | window.addEventListener('pageshow', update) | ||||||
|  |  | ||||||
|  | document.addEventListener('visibilitychange', update); | ||||||
|  |  | ||||||
|  | function actionLogin(form) { | ||||||
|  |     const elements = form.getElementsByClassName('error'); | ||||||
|  |     for (const e of elements) form.removeChild(e); | ||||||
|  |  | ||||||
|  |     const client = form['client'].value; | ||||||
|  |     window.localStorage.setItem(`${CLIENT}/${client}/username`, form['username'].value); | ||||||
|  |  | ||||||
|  |     authenticate(client, form['username'].value, form['password'].value) | ||||||
|  |         .then(token => { | ||||||
|  |             window.localStorage.setItem(`${CLIENT}/${client}/token`, token); | ||||||
|  |             window.location.hash = `#/${client}`; | ||||||
|  |         }).catch(e => { | ||||||
|  |             const error = document.createElement('div'); | ||||||
|  |             error.className = 'error'; | ||||||
|  |             error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? `Unbekannter Fehler\n(${e.message})`; | ||||||
|  |             form.insertBefore(error, form.lastChild.previousSibling); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | } | ||||||
							
								
								
									
										330
									
								
								www/res/access-single.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								www/res/access-single.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | window.CLIENT = window.CLIENT || null; | ||||||
|  | window.ELWIG_API = window.ELWIG_API || null; | ||||||
|  |  | ||||||
|  | function getCredentialsUsername() { | ||||||
|  |     return window.localStorage.getItem(`${CLIENT}/username`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCredentialsPassword() { | ||||||
|  |     return window.localStorage.getItem(`${CLIENT}/password`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getBasicAuth() { | ||||||
|  |     return { | ||||||
|  |         'Authorization': 'Basic ' + btoa(getCredentialsUsername() + ':' + getCredentialsPassword()), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function _get(path) { | ||||||
|  |     const res = await fetch(`${ELWIG_API}${path}`, { | ||||||
|  |         method: 'GET', | ||||||
|  |         headers: {...getBasicAuth()}, | ||||||
|  |     }); | ||||||
|  |     const json = await res.json(); | ||||||
|  |     if (!res.ok) throw new ApiError(res.status, json['message']); | ||||||
|  |     return json; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function get(path) { | ||||||
|  |     return (await _get(path))['data']; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getMember(mgnr) { | ||||||
|  |     return await get(`/members/${mgnr}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getWineVarieties() { | ||||||
|  |     return Object.fromEntries((await get('/wine/varieties')).map(item => [item['sortid'], item])); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getWineQualityLevels() { | ||||||
|  |     return Object.fromEntries((await get('/wine/quality_levels')).map(item => [item['qualid'], item])); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getWineAttributes() { | ||||||
|  |     return Object.fromEntries((await get('/wine/attributes')).map(item => [item['attrid'], item])); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getWineCultivations() { | ||||||
|  |     return Object.fromEntries((await get('/wine/cultivations')).map(item => [item['cultid'], item])); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getModifiers() { | ||||||
|  |     const list = await get('/modifiers'); | ||||||
|  |     const dict = {}; | ||||||
|  |     for (const item of list) { | ||||||
|  |         if (!dict[item['year']]) dict[item['year']] = {}; | ||||||
|  |         dict[item['year']][item['modid']] = item; | ||||||
|  |     } | ||||||
|  |     return dict; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getDeliveries(filters, limit, offset) { | ||||||
|  |     const query = ['sort=reverse']; | ||||||
|  |     if (!!filters) query.push(`filters=${filters.join(',')}`); | ||||||
|  |     if (!!limit) query.push(`limit=${limit}`); | ||||||
|  |     if (!!offset) query.push(`offset=${offset}`); | ||||||
|  |     return await _get(`/deliveries${!!query ? '?' : ''}${query.join('&')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getDeliveryStats(filters, detail) { | ||||||
|  |     const query = []; | ||||||
|  |     if (!!filters) query.push(`filters=${filters.join(',')}`); | ||||||
|  |     if (!!detail) query.push(`detail=${detail}`); | ||||||
|  |     return await _get(`/deliveries/stat${!!query ? '?' : ''}${query.join('&')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getDeliverySchedules(filters, limit, offset) { | ||||||
|  |     const query = []; | ||||||
|  |     if (!!filters) query.push(`filters=${filters.join(',')}`); | ||||||
|  |     if (!!limit) query.push(`limit=${limit}`); | ||||||
|  |     if (!!offset) query.push(`offset=${offset}`); | ||||||
|  |     return await _get(`/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getDeliveryAnnouncements(filters, limit, offset) { | ||||||
|  |     const query = []; | ||||||
|  |     if (!!filters) query.push(`filters=${filters.join(',')}`); | ||||||
|  |     if (!!limit) query.push(`limit=${limit}`); | ||||||
|  |     if (!!offset) query.push(`offset=${offset}`); | ||||||
|  |     return await _get(`/delivery_announcements${!!query ? '?' : ''}${query.join('&')}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function load() { | ||||||
|  |     const main = document.getElementById("access"); | ||||||
|  |     const form = main.getElementsByTagName("form")[0]; | ||||||
|  |     if (form) { | ||||||
|  |         const elements = form.getElementsByClassName('error'); | ||||||
|  |         for (const e of elements) form.removeChild(e); | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |         window.MEMBER = await getMember(getCredentialsUsername()); | ||||||
|  |         const txt = document.getElementById('usertext'); | ||||||
|  |         txt.innerHTML = `${MEMBER.prefix ?? ''} ${MEMBER.given_name ?? ''} ${MEMBER.middle_names ?? ''} ${MEMBER.name ?? ''} ${MEMBER.suffix ?? ''}<br/><div>MgNr. ${MEMBER.mgnr}</div>`; | ||||||
|  |         window.WINE_VARIETIES = await getWineVarieties(); | ||||||
|  |         window.WINE_QUALITY_LEVELS = await getWineQualityLevels(); | ||||||
|  |         window.WINE_ATTRIBUTES = await getWineAttributes(); | ||||||
|  |         window.WINE_CULTIVATIONS = await getWineCultivations(); | ||||||
|  |         window.MODIFIERS = await getModifiers(); | ||||||
|  |         return true; | ||||||
|  |     } catch (e) { | ||||||
|  |         if (form) { | ||||||
|  |             window.localStorage.removeItem(`${CLIENT}/password`); | ||||||
|  |             const error = document.createElement('div'); | ||||||
|  |             error.className = 'error'; | ||||||
|  |             error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? 'Unbekannter Fehler'; | ||||||
|  |             form.insertBefore(error, form.lastChild.previousSibling); | ||||||
|  |         } else { | ||||||
|  |             window.location.hash = '#/login'; | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function init() { | ||||||
|  |     if (!getCredentialsUsername() || !getCredentialsPassword()) { | ||||||
|  |         window.location.hash = '#/login'; | ||||||
|  |         render(); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     await load(); | ||||||
|  |     render(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateOverview() { | ||||||
|  |     const [schedules] = await Promise.all([getDeliverySchedules([`year=${getCurrentLastSeason()}`])]); | ||||||
|  |  | ||||||
|  |     const main = document.getElementsByTagName('main')[0]; | ||||||
|  |     const days = groupBy(schedules.data, 'date'); | ||||||
|  |     for (const [dateString, day] of Object.entries(days)) { | ||||||
|  |         const row = document.createElement('div'); | ||||||
|  |         row.className = 'day'; | ||||||
|  |         const date = new Date(dateString); | ||||||
|  |         row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`; | ||||||
|  |         const container = document.createElement('div'); | ||||||
|  |         container.className = 'schedule-container'; | ||||||
|  |         for (const schedule of day) { | ||||||
|  |             const now = new Date(); | ||||||
|  |             const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null; | ||||||
|  |             const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null; | ||||||
|  |             const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei'; | ||||||
|  |             const link = document.createElement('a'); | ||||||
|  |             link.href = `#/anmelden/${schedule.year}/${schedule.dsnr}` | ||||||
|  |             link.innerHTML += `<div><span>${schedule.description}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`; | ||||||
|  |             if (schedule.delivered_weight > 0) { | ||||||
|  |                 link.innerHTML += ` | ||||||
|  |                     <span> | ||||||
|  |                       <span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> / | ||||||
|  |                       <span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span> | ||||||
|  |                       (<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>) | ||||||
|  |                     </span>`; | ||||||
|  |             } else { | ||||||
|  |                 link.innerHTML += ` | ||||||
|  |                     <span> | ||||||
|  |                       <span>${fmtInt(schedule.announced_weight)} kg</span> / | ||||||
|  |                       <span class="min-kg">${fmtInt(schedule.max_weight)} kg</span> | ||||||
|  |                       (<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>) | ||||||
|  |                     </span>`; | ||||||
|  |             } | ||||||
|  |             container.append(link); | ||||||
|  |         } | ||||||
|  |         row.appendChild(container); | ||||||
|  |         main.appendChild(row); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateDeliveries(year) { | ||||||
|  |     const filters = [`mgnr=${MEMBER.mgnr}`, `year=${year}`]; | ||||||
|  |     const [deliveries, stat] = await Promise.all([getDeliveries(filters), getDeliveryStats(filters)]); | ||||||
|  |  | ||||||
|  |     const tbody = document.getElementById('delivery-list'); | ||||||
|  |     tbody.innerHTML = ''; | ||||||
|  |     for (const delivery of deliveries.data) { | ||||||
|  |         const tr = document.createElement('tr'); | ||||||
|  |         tr.style.background = '#C0C0C0'; | ||||||
|  |         tr.innerHTML = ` | ||||||
|  |             <th colspan="2">${delivery.lsnr}</th> | ||||||
|  |             <td>${fmtDate(new Date(delivery.date))}</td> | ||||||
|  |             <td>${delivery.time.substring(0, 5)}</td> | ||||||
|  |             <td colspan="2">${delivery.branch.name}</td> | ||||||
|  |             <td colspan="7">${delivery.comment ?? ''}</td>`; | ||||||
|  |         tbody.appendChild(tr); | ||||||
|  |         for (const part of delivery.parts) { | ||||||
|  |             const tr = document.createElement('tr'); | ||||||
|  |             const defaultQualityLevel = getDefaultQualityLevel(part.gradation.kmw); | ||||||
|  |             tr.innerHTML = ` | ||||||
|  |                 <th>${part.dpnr}</th> | ||||||
|  |                 <td colspan="2">${WINE_VARIETIES[part.variety.sortid]?.name ?? ''}</td> | ||||||
|  |                 <td colspan="2" style="font-weight: bold;">${WINE_CULTIVATIONS[part.cultivation?.cultid]?.name ?? ''}</td> | ||||||
|  |                 <td colspan="2" style="font-weight: bold;">${WINE_ATTRIBUTES[part.attribute?.attrid]?.name ?? ''}</td> | ||||||
|  |                 <td class="${defaultQualityLevel['qualid'] != part.quality_level.qualid ? 'abgewertet' : ''}">${WINE_QUALITY_LEVELS[part.quality_level.qualid]?.name ?? ''}</td> | ||||||
|  |                 <td class="center">${fmtOe(part.gradation.oe)}</td> | ||||||
|  |                 <td class="center">${fmtKmw(part.gradation.kmw)}</td> | ||||||
|  |                 <td class="number">${fmtInt(part.weight)}</td> | ||||||
|  |                 <td>${part.modifiers.map(m => MODIFIERS[delivery.year][m.modid]).sort(m => m.ordering).map(m => m.name).join(' / ')}</td> | ||||||
|  |                 <td>${part.comment ?? ''}</td>`; | ||||||
|  |             tbody.appendChild(tr); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const element = document.getElementById('delivery-stat'); | ||||||
|  |     element.innerText = `(Teil-)Lieferungen: ${fmtInt(stat.data.total.count)} (${fmtInt(stat.data.total.parts)}), Gewicht: ${fmtInt(stat.data.total.weight.sum)} kg`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function render() { | ||||||
|  |     const hash = window.location.hash; | ||||||
|  |     const main = document.getElementById("access"); | ||||||
|  |     const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0]; | ||||||
|  |     for (const li of nav.children) li.className = ''; | ||||||
|  |     if (hash === '#/login') { | ||||||
|  |         main.className = 'login'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <form onsubmit="return actionLogin(this);"> | ||||||
|  |               <h1>Anmelden</h1> | ||||||
|  |               <input type="text" name="username" placeholder="Mitgliedsnummer" value="${getCredentialsUsername() ?? ''}"/> | ||||||
|  |               <input type="password" name="password" placeholder="Kennwort"/> | ||||||
|  |               <button type="submit">Anmelden</button> | ||||||
|  |             </form>`; | ||||||
|  |     } else if (hash === '#/') { | ||||||
|  |         nav.children[0].className = 'active'; | ||||||
|  |         main.className = 'overview'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <h1>Übersicht</h1>`; | ||||||
|  |         updateOverview().then(); | ||||||
|  |     } else if (hash === '#/mitglied') { | ||||||
|  |         nav.children[1].className = 'active'; | ||||||
|  |         main.className = 'member'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <h1>Mitglied</h1> | ||||||
|  |             <button onclick="actionLogout()">Abmelden</button> | ||||||
|  |             <pre>${JSON.stringify(MEMBER, null, 2)}</pre>`; | ||||||
|  |     } else if (hash === '#/lieferungen') { | ||||||
|  |         nav.children[2].className = 'active'; | ||||||
|  |         main.className = 'deliveries'; | ||||||
|  |         main.innerHTML = ` | ||||||
|  |             <h1>Lieferungen</h1> | ||||||
|  |             <form> | ||||||
|  |               <div> | ||||||
|  |                 <label for="season">Saison:</label> | ||||||
|  |                 <input name="season" type="number" min="1900" max="9999" | ||||||
|  |                        value="${getCurrentLastSeason()}" onchange="updateDeliveries(this.value).then()"/> | ||||||
|  |               </div> | ||||||
|  |               <div id="delivery-stat"/> | ||||||
|  |             </form> | ||||||
|  |             <table style="width: 100%;"> | ||||||
|  |               <colgroup> | ||||||
|  |                 <col style="width:  50px;"/> | ||||||
|  |                 <col style="width:  70px;"/> | ||||||
|  |                 <col style="width: 100px;"/> | ||||||
|  |                 <col style="width:  60px;"/> | ||||||
|  |                 <col style="width:  50px;"/> | ||||||
|  |                 <col style="width:  70px;"/> | ||||||
|  |                 <col style="width:  50px;"/> | ||||||
|  |                 <col style="width: 120px;"/> | ||||||
|  |                 <col style="width:  50px;"/> | ||||||
|  |                 <col style="width:  50px;"/> | ||||||
|  |                 <col style="width:  60px;"/> | ||||||
|  |                 <col style="min-width: 80px;"/> | ||||||
|  |               </colgroup> | ||||||
|  |               <thead> | ||||||
|  |                 <tr> | ||||||
|  |                   <th rowspan="2"></th> | ||||||
|  |                   <th rowspan="2" colspan="2">Sorte</th> | ||||||
|  |                   <th rowspan="2" colspan="2">Bewirt.</th> | ||||||
|  |                   <th rowspan="2" colspan="2">Attribut</th> | ||||||
|  |                   <th rowspan="2">Qualitätsstufe</th> | ||||||
|  |                   <th colspan="2" class="center">Gradation</th> | ||||||
|  |                   <th class="center">Gewicht</th> | ||||||
|  |                   <th rowspan="2">Zu-/Abschläge</th> | ||||||
|  |                   <th></th> | ||||||
|  |                 </tr> | ||||||
|  |                 <tr> | ||||||
|  |                   <th class="unit">[°Oe]</th> | ||||||
|  |                   <th class="unit">[°KMW]</th> | ||||||
|  |                   <th class="unit">[kg]</th> | ||||||
|  |                 </tr> | ||||||
|  |               </thead> | ||||||
|  |               <tbody id="delivery-list"></tbody> | ||||||
|  |             </table>`; | ||||||
|  |         updateDeliveries(getCurrentLastSeason()).then(); | ||||||
|  |     } else if (hash === '#/anmeldungen') { | ||||||
|  |         nav.children[3].className = 'active'; | ||||||
|  |         main.className = 'announcements'; | ||||||
|  |         main.innerHTML = '<h1>Anmeldungen</h1>'; | ||||||
|  |     } else if (hash.startsWith('#/anmelden/')) { | ||||||
|  |         nav.children[3].className = 'active'; | ||||||
|  |         main.className = 'announce'; | ||||||
|  |         main.innerHTML = '<h1>Anmelden</h1>'; | ||||||
|  |     } else { | ||||||
|  |         window.location.hash = `#/`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | document.addEventListener('DOMContentLoaded', async () => { | ||||||
|  |     await init(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | window.addEventListener('hashchange', () => { | ||||||
|  |     if ((!getCredentialsUsername() || !getCredentialsPassword()) && window.location.hash !== '#/login') { | ||||||
|  |         window.location.hash = '#/login'; | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     render(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | function actionLogin(form) { | ||||||
|  |     window.localStorage.setItem(`${CLIENT}/username`, form.username.value); | ||||||
|  |     window.localStorage.setItem(`${CLIENT}/password`, form.password.value); | ||||||
|  |     load().then(success => { | ||||||
|  |         if (success) window.location.hash = '#/'; | ||||||
|  |     }); | ||||||
|  |     return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function actionLogout() { | ||||||
|  |     window.localStorage.removeItem(`${CLIENT}/username`); | ||||||
|  |     window.localStorage.removeItem(`${CLIENT}/password`); | ||||||
|  |     window.location.reload(); | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								www/res/access.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								www/res/access.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const ERROR_MESSAGES = { | ||||||
|  |     401: 'Ungültige Anmeldedaten', | ||||||
|  |     'NetworkError when attempting to fetch resource.': 'Netzwerkfehler', | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class ApiError extends Error { | ||||||
|  |     constructor(statusCode, message) { | ||||||
|  |         super(statusCode + ' - ' + message); | ||||||
|  |         this.statusCode = statusCode; | ||||||
|  |         this.name = 'ApiError'; | ||||||
|  |         this.localizedMessage = ERROR_MESSAGES[statusCode]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function escapeHTML(str) { | ||||||
|  |     const p = document.createElement("p"); | ||||||
|  |     p.appendChild(document.createTextNode(str)); | ||||||
|  |     return p.innerHTML; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtDate(date) { | ||||||
|  |     return date.toLocaleDateString('de-AT', { | ||||||
|  |         day: '2-digit', | ||||||
|  |         month: '2-digit', | ||||||
|  |         year: 'numeric', | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtDateTime(date) { | ||||||
|  |     return date.toLocaleDateString('de-AT', { | ||||||
|  |         day: '2-digit', | ||||||
|  |         month: '2-digit', | ||||||
|  |         hour: '2-digit', | ||||||
|  |         minute: '2-digit', | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtDateWeekday(date) { | ||||||
|  |     return date.toLocaleDateString('de-AT', { | ||||||
|  |         weekday: 'long', | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtOe(oe) { | ||||||
|  |     return oe.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtKmw(kmw) { | ||||||
|  |     return kmw.toLocaleString('de-AT', {minimumFractionDigits: 1, maximumFractionDigits: 1}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function fmtInt(num) { | ||||||
|  |     return num.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function groupBy(list, key) { | ||||||
|  |     return list.reduce((groups, value) => { | ||||||
|  |         (groups[value[key]] = groups[value[key]] ?? []).push(value); | ||||||
|  |         return groups; | ||||||
|  |     }, {}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getCurrentLastSeason() { | ||||||
|  |     const date = new Date(); | ||||||
|  |     return date.getFullYear() - ((date.getMonth() + 1) <= 6 ? 1 : 0); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getDefaultQualityLevel(kmw) { | ||||||
|  |     const list = Object.values(WINE_QUALITY_LEVELS).filter(q => !q['is_predicate']).sort((a, b) => a['min_kmw'] - b['min_kmw']); | ||||||
|  |     let last = list[0]; | ||||||
|  |     for (const q of list) { | ||||||
|  |         if (q['min_kmw'] > kmw) { | ||||||
|  |             return last; | ||||||
|  |         } | ||||||
|  |         last = q; | ||||||
|  |     } | ||||||
|  |     return last; | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								www/res/avatar.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								www/res/avatar.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								www/res/elwig.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								www/res/elwig.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 66 KiB | 
							
								
								
									
										340
									
								
								www/res/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								www/res/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,340 @@ | |||||||
|  |  | ||||||
|  | /**** Shared ****/ | ||||||
|  |  | ||||||
|  | * { | ||||||
|  |     box-sizing: border-box; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :root { | ||||||
|  |     font-family: 'Arial', sans-serif; | ||||||
|  |     scroll-behavior: smooth; | ||||||
|  |     --main-color: #A040C0; | ||||||
|  |     --light-color: #D098E0; | ||||||
|  |     --accent-color: #C0F080; | ||||||
|  |     --bg-color: #EBEFE7; | ||||||
|  |     --blur-color: #60804020; | ||||||
|  |     --blur: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body { | ||||||
|  |     margin: 0; | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body.header-footer { | ||||||
|  |     min-height: calc(100vh + 8em); | ||||||
|  |     padding: 3em 0 8em 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | header { | ||||||
|  |     position: fixed; | ||||||
|  |     top: 0; | ||||||
|  |     width: 100%; | ||||||
|  |     height: 3em; | ||||||
|  |     box-shadow: 0 0 0.5em #00000060; | ||||||
|  |     background-color: #FFFFFF; | ||||||
|  |     z-index: 10; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | header img { | ||||||
|  |     height: 2.5em; | ||||||
|  |     margin: 0.25em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav { | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav > *:first-child, | ||||||
|  | nav > *:last-child { | ||||||
|  |     flex: 100px 1 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav > *:last-child { | ||||||
|  |     text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav ul { | ||||||
|  |     display: flex; | ||||||
|  |     list-style-type: none; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  |     justify-content: center; | ||||||
|  |     align-items: center; | ||||||
|  |     height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav li { | ||||||
|  |     flex: 100px 1 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav li a { | ||||||
|  |     text-decoration: none; | ||||||
|  |     padding: 1em 2em; | ||||||
|  |     color: #808080; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav li a:hover, | ||||||
|  | nav li.active a{ | ||||||
|  |     color: var(--main-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav a.flag { | ||||||
|  |     text-decoration: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav a.flag div { | ||||||
|  |     display: inline-block; | ||||||
|  |     font-size: 1.25em; | ||||||
|  |     width: 1em; | ||||||
|  |     margin: 0.75rem 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav a.flag[href='/de/'] div::before, | ||||||
|  | nav a.flag[href='/en/']:hover div::before, | ||||||
|  | nav a.flag[href='/en/']:focus div::before { | ||||||
|  |     content: '\1F1EC\1F1E7';  /* GB */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | nav a.flag[href='/en/'] div::before, | ||||||
|  | nav a.flag[href='/de/']:hover div::before, | ||||||
|  | nav a.flag[href='/de/']:focus div::before { | ||||||
|  |     content: '\1F1E6\1F1F9';  /* AT */ | ||||||
|  | } | ||||||
|  |  | ||||||
|  | footer { | ||||||
|  |     height: 8em; | ||||||
|  |     padding: 1em; | ||||||
|  |     width: 100%; | ||||||
|  |     box-shadow: 0 0 0.5rem #00000060; | ||||||
|  |     background-color: #404040; | ||||||
|  |     color: #FFFFFF; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: center; | ||||||
|  |     gap: 2em; | ||||||
|  |     position: absolute; | ||||||
|  |     bottom: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | footer img { | ||||||
|  |     height: 4em; | ||||||
|  |     width: 4em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | footer a { | ||||||
|  |     align-self: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media screen and (max-width: 800px) { | ||||||
|  |     footer { | ||||||
|  |         flex-direction: column; | ||||||
|  |         height: unset; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 1em; | ||||||
|  |         padding-top: 3em; | ||||||
|  |     } | ||||||
|  |     footer > *:not(:first-child) { | ||||||
|  |         width: 300px; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | p a, | ||||||
|  | table a { | ||||||
|  |     color: var(--main-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /**** Access ****/ | ||||||
|  |  | ||||||
|  | main#access { | ||||||
|  |     width: calc(100% - 2em); | ||||||
|  |     max-width: 1200px; | ||||||
|  |     margin: 4em auto 4em auto; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     box-shadow: 0 0 0.5em #00000060; | ||||||
|  |     padding: 1em 2em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access h1 { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access.login { | ||||||
|  |     max-width: 500px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access.login h1 { | ||||||
|  |     margin: 1rem 0 1rem 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access.login form { | ||||||
|  |     text-align: center; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     align-items: stretch; | ||||||
|  |     gap: 1em; | ||||||
|  |     margin: 1em 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access.login form input, | ||||||
|  | main#access.login form button { | ||||||
|  |     font-size: 1em; | ||||||
|  |     border: 1px solid #C0C0C0; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     padding: 0.25em 0.5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main#access.login form .error { | ||||||
|  |     font-size: 1em; | ||||||
|  |     color: #C00000; | ||||||
|  |     border: 1px solid #C00000; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     padding: 0.25em 0.5em; | ||||||
|  |     text-align: center; | ||||||
|  |     font-weight: bold; | ||||||
|  |     margin: 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #user { | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: end; | ||||||
|  |     align-items: center; | ||||||
|  |     text-decoration: none; | ||||||
|  |     color: inherit; | ||||||
|  |     position: absolute; | ||||||
|  |     right: 0; | ||||||
|  |     height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #user:hover { | ||||||
|  |     color: var(--main-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #usertext { | ||||||
|  |     font-size: 0.875em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #usertext > div { | ||||||
|  |     font-size: 0.75em; | ||||||
|  |     opacity: 0.5; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main .number { | ||||||
|  |     text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.deliveries form { | ||||||
|  |     margin: 1em; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     align-content: center; | ||||||
|  |     gap: 1em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .abgewertet { | ||||||
|  |     color: #C00000; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .day { | ||||||
|  |     border-radius: 4px; | ||||||
|  |     padding-left: 1.5em; | ||||||
|  |     background: #E0E0E0; | ||||||
|  |     margin: 1em 0; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 2em; | ||||||
|  |     height: 10em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .day.today { | ||||||
|  |     background-color: var(--light-color); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .schedule-container { | ||||||
|  |     background: #00000020; | ||||||
|  |     border-radius: 4px; | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |     flex-grow: 1; | ||||||
|  |     height: 100%; | ||||||
|  |     justify-content: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .schedule-container a { | ||||||
|  |     border-radius: 4px; | ||||||
|  |     color: unset; | ||||||
|  |     text-decoration: none; | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     align-items: center; | ||||||
|  |     padding: 0 1em; | ||||||
|  |     transition: background-color 0.125s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .schedule-container a.cancelled > div { | ||||||
|  |     color: #A00000; | ||||||
|  |     text-decoration: line-through; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .schedule-container a.cancelled > span { | ||||||
|  |     opacity: 0.25; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | main.overview .schedule-container a:hover { | ||||||
|  |     background-color: #00000020; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .min-percent { | ||||||
|  |     min-width: 2.625em; | ||||||
|  |     display: inline-block; | ||||||
|  |     text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .min-kg { | ||||||
|  |     min-width: 5em; | ||||||
|  |     display: inline-block; | ||||||
|  |     text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media screen and (max-width: 1000px) { | ||||||
|  |     main#access { | ||||||
|  |         padding: 1em 0.5em; | ||||||
|  |         margin-left: 0; | ||||||
|  |         margin-right: 0; | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     main.overview .day { | ||||||
|  |         flex-direction: column; | ||||||
|  |         height: unset; | ||||||
|  |         padding-left: 0; | ||||||
|  |         gap: 0.5em; | ||||||
|  |         padding-top: 0.5em; | ||||||
|  |     } | ||||||
|  |     main.overview .day > div:first-child { | ||||||
|  |         text-align: center; | ||||||
|  |     } | ||||||
|  |     main.overview .schedule-container { | ||||||
|  |         height: unset; | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     main.overview .schedule-container a { | ||||||
|  |         padding: 1em; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media screen and (max-width: 650px) { | ||||||
|  |     main.overview .schedule-container a { | ||||||
|  |         flex-direction: column; | ||||||
|  |     } | ||||||
|  |     main.overview .schedule-container a > *:first-child { | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     main.overview .schedule-container a > *:last-child { | ||||||
|  |         width: 100%; | ||||||
|  |         text-align: right; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								www/robots.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								www/robots.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | # robots.txt for access.elwig.at | ||||||
|  | User-Agent: * | ||||||
|  | Disallow: / | ||||||
		Reference in New Issue
	
	Block a user