Compare commits
12 Commits
a7921f6d5b
...
main
Author | SHA1 | Date | |
---|---|---|---|
25805f0475 | |||
223c959cd9 | |||
4962218fe3 | |||
7cf3e21efc | |||
a93afcdf97 | |||
0ce071c256 | |||
99d0356b58 | |||
a6072fa465 | |||
c69d9c11ec | |||
297f575d4b | |||
5836c63afc | |||
bc47f8f463 |
@ -19,15 +19,3 @@ function authenticate(): void {
|
||||
http_401_unauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
function authenticate_client(string $client): void {
|
||||
global $CLIENT_CREDENTIALS;
|
||||
$credentials = $CLIENT_CREDENTIALS[$client];
|
||||
if (!array_key_exists('PHP_AUTH_USER', $_SERVER) ||
|
||||
!array_key_exists('PHP_AUTH_PW', $_SERVER) ||
|
||||
!array_key_exists($_SERVER['PHP_AUTH_USER'], $credentials) ||
|
||||
$_SERVER['PHP_AUTH_PW'] !== $credentials[$_SERVER['PHP_AUTH_USER']])
|
||||
{
|
||||
http_401_unauthorized();
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,9 @@
|
||||
<?php
|
||||
global $GITEA_TOKEN;
|
||||
global $CREDENTIALS;
|
||||
global $CLIENT_CREDENTIALS;
|
||||
global $CLIENT_ACCESS;
|
||||
global $COMBINED_ACCESS;
|
||||
|
||||
$GITEA_TOKEN = 'token';
|
||||
|
||||
$CREDENTIALS = [
|
||||
'username' => 'password',
|
||||
];
|
||||
|
||||
$CLIENT_CREDENTIALS = [
|
||||
'name' => [
|
||||
'username' => 'password',
|
||||
],
|
||||
];
|
||||
|
||||
$CLIENT_ACCESS = [
|
||||
'WGX' => [
|
||||
'name' => 'Winzergenossenschaft',
|
||||
'api' => 'https://example.com/elwig/api/v1',
|
||||
],
|
||||
];
|
||||
|
||||
$COMBINED_ACCESS = [
|
||||
'HOLDING' => [
|
||||
'name' => 'Name',
|
||||
'clients' => ['WGX'],
|
||||
],
|
||||
];
|
||||
|
100
www/access.php
100
www/access.php
@ -1,100 +0,0 @@
|
||||
<?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: 404');
|
||||
header('Content-Length: 0');
|
||||
exit();
|
||||
} else if ($_SERVER['PATH_INFO'] !== "/$client[id]/") {
|
||||
header('Status: 308');
|
||||
header("Location: /access/$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-AT">
|
||||
<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>
|
267
www/clients.php
267
www/clients.php
@ -1,267 +0,0 @@
|
||||
<?php
|
||||
require ".php/format.inc";
|
||||
require ".php/auth.inc";
|
||||
require ".php/credentials.inc";
|
||||
global $CLIENT_CREDENTIALS;
|
||||
|
||||
$clients = array_keys($CLIENT_CREDENTIALS);
|
||||
|
||||
$format = get_fmt();
|
||||
|
||||
function get_zip_meta($path): array {
|
||||
$meta = 'null';
|
||||
$fp = fopen($path, "rb");
|
||||
|
||||
$zipHdr1 = fread($fp, 30);
|
||||
if (strlen($zipHdr1) !== 30 || !str_starts_with($zipHdr1, "PK\x03\x04") ||
|
||||
$zipHdr1[8] !== "\x00" || $zipHdr1[9] !== "\x00" || $zipHdr1[26] !== "\x07" || $zipHdr1[27] !== "\x00")
|
||||
{
|
||||
fclose($fp);
|
||||
return array($meta, null, null);
|
||||
}
|
||||
|
||||
$extraFieldLen1 = unpack("v", substr($zipHdr1, 28, 2))[1];
|
||||
$name1 = fread($fp, 7);
|
||||
fseek($fp, $extraFieldLen1, SEEK_CUR);
|
||||
$data1 = fread($fp, unpack("V", substr($zipHdr1, 18, 4))[1]);
|
||||
if ($name1 !== "version" || !str_starts_with($data1, "elwig:")) {
|
||||
fclose($fp);
|
||||
return array($meta, null, null);
|
||||
}
|
||||
$version = (int)substr($data1, 6);
|
||||
|
||||
$zipHdr2 = fread($fp, 30);
|
||||
if (strlen($zipHdr2) !== 30 || !str_starts_with($zipHdr2, "PK\x03\x04") ||
|
||||
$zipHdr2[8] !== "\x00" || $zipHdr2[9] !== "\x00" || $zipHdr2[26] !== "\x09" || $zipHdr2[27] !== "\x00")
|
||||
{
|
||||
fclose($fp);
|
||||
return array($meta, $version, null);
|
||||
}
|
||||
|
||||
$extraFieldLen2 = unpack("v", substr($zipHdr2, 28, 2))[1];
|
||||
$name2 = fread($fp, 9);
|
||||
fseek($fp, $extraFieldLen2, SEEK_CUR);
|
||||
if ($name2 !== "meta.json") {
|
||||
fclose($fp);
|
||||
return array($meta, $version, null);
|
||||
}
|
||||
|
||||
$meta = fread($fp, unpack("V", substr($zipHdr2, 18, 4))[1]);
|
||||
$files = "{";
|
||||
$first = true;
|
||||
while (!feof($fp)) {
|
||||
$zipHdr3 = fread($fp, 30);
|
||||
if (strlen($zipHdr3) !== 30 || !str_starts_with($zipHdr3, "PK\x03\x04"))
|
||||
continue;
|
||||
$compSize = unpack("V", substr($zipHdr3, 18, 4))[1];
|
||||
$uncompSize = unpack("V", substr($zipHdr3, 22, 4))[1];
|
||||
$name = fread($fp, unpack("v", substr($zipHdr3, 26, 2))[1]);
|
||||
$crc = unpack("V", substr($zipHdr3, 14, 4))[1];
|
||||
fseek($fp, $compSize, SEEK_CUR);
|
||||
$hex = substr("00000000" . dechex($crc), -8);
|
||||
if (!$first) $files .= ", ";
|
||||
$files .= "\"$name\": {\"compressed_size\": $compSize, \"uncompressed_size\": $uncompSize, \"crc32\": \"$hex\"}";
|
||||
$first = false;
|
||||
}
|
||||
$files .= "}";
|
||||
|
||||
fclose($fp);
|
||||
return array($meta, $version, $files);
|
||||
}
|
||||
|
||||
$path = $_SERVER['PATH_INFO'];
|
||||
if ($path == '') {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
header('Status: 405');
|
||||
header('Allow: GET');
|
||||
if ($format === 'text') {
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 23');
|
||||
echo "405 Method Not Allowed\n";
|
||||
} else if ($format === 'json') {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Content-Length: 48');
|
||||
echo "{\"errors\": [{\"message\": \"Method not allowed\"}]}\n";
|
||||
} else {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Content-Length: 0');
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
if ($format === 'text') {
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
foreach ($clients as $c)
|
||||
echo "$c\n";
|
||||
} else if ($format === 'json') {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo "{\"data\": [";
|
||||
$first = true;
|
||||
foreach ($clients as $c) {
|
||||
if (!$first) echo ",";
|
||||
echo "\n {\"name\": \"$c\"}";
|
||||
$first = false;
|
||||
}
|
||||
echo "\n]}\n";
|
||||
} else if ($format === 'html') {
|
||||
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-AT">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Mandanten - Elwig</title>
|
||||
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
|
||||
<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"/>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<section>
|
||||
<h3>Mandanten</h3>
|
||||
<p class="center"><a href="/">Startseite</a></p>
|
||||
<table>
|
||||
<thead><tr><th>Name</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($clients as $c) {
|
||||
echo " <tr><td><a href='clients/$c'>$c</a></td></tr>\n";
|
||||
} ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="center"><a href="?format=json">JSON-Format</a></p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
<?php }
|
||||
exit();
|
||||
}
|
||||
|
||||
foreach ($clients as $c) {
|
||||
if ($path !== "/$c" && !str_starts_with($path, "/$c/"))
|
||||
continue;
|
||||
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
|
||||
authenticate_client($c);
|
||||
if ($path === "/$c") {
|
||||
header('Status: 308');
|
||||
header("Location: $c/");
|
||||
header('Content-Length: 23');
|
||||
exit("308 Permanent Redirect\n");
|
||||
} elseif ($path === "/$c/") {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
header("Status: 405");
|
||||
header("Allow: GET");
|
||||
header('Content-Length: 23');
|
||||
exit("405 Method Not Allowed\n");
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
echo "{\"data\": [\n";
|
||||
$first = true;
|
||||
foreach (scandir(".data/clients/$c/") as $file) {
|
||||
if (str_starts_with($file, ".") || str_ends_with($file, ".php") || str_ends_with($file, ".inc")) continue;
|
||||
if (!$first) echo ",\n";
|
||||
$path = ".data/clients/$c/$file";
|
||||
$size = filesize($path);
|
||||
$url = "https://elwig.at/clients/$c/$file";
|
||||
$mod = date(DATE_ATOM, filemtime($path));
|
||||
$cre = date(DATE_ATOM, filectime($path));
|
||||
$datetime = "null";
|
||||
$zwstid = "null";
|
||||
if (str_ends_with($file, ".elwig.zip") && substr_count($file, "_") === 2) {
|
||||
$parts = explode("_", substr($file, 0, -10));
|
||||
$time = str_replace("-", ":", $parts[1]);
|
||||
$dt = DateTime::createFromFormat("Y-m-d H:i:s", "$parts[0] $time");
|
||||
$datetime = '"' . $dt->format(DateTimeInterface::RFC3339) . '"';
|
||||
$zwstid = "\"$parts[2]\"";
|
||||
}
|
||||
list($meta, $version, $files) = get_zip_meta($path);
|
||||
$files ??= "null";
|
||||
$version ??= "null";
|
||||
echo " {\"name\": \"$file\", \"timestamp\": $datetime, \"zwstid\": $zwstid, \"meta\": $meta, \"files\": $files, " .
|
||||
"\"version\": $version, \"url\": \"$url\", \"size\": $size, \"created\": \"$cre\", \"modified\": \"$mod\"}";
|
||||
$first = false;
|
||||
}
|
||||
echo "\n]}\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
$file = substr($path, strlen("/$c/"));
|
||||
$path = ".data/clients/$c/$file";
|
||||
if (!preg_match_all('/[A-Za-z0-9_.-]+/', $file) && !($file === '*' && $_SERVER['REQUEST_METHOD'] === 'DELETE')) {
|
||||
header('Status: 400');
|
||||
header('Content-Length: 16');
|
||||
exit("400 Bad Request\n");
|
||||
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
$size = filesize($path);
|
||||
if ($size === false) {
|
||||
header('Status: 404');
|
||||
header('Content-Length: 14');
|
||||
exit("404 Not Found\n");
|
||||
}
|
||||
$type = mime_content_type($path);
|
||||
header("Content-Type: $type");
|
||||
header("Content-Disposition: attachment; filename=\"$file\"");
|
||||
header("Content-Length: $size");
|
||||
readfile($path);
|
||||
} elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||
$putdata = fopen('php://input', 'r');
|
||||
$fp = fopen($path, 'wb');
|
||||
if ($fp === false) {
|
||||
header("Status: 500");
|
||||
header("Content-Length: 26");
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
while ($data = fread($putdata, 4096))
|
||||
fwrite($fp, $data);
|
||||
fclose($fp);
|
||||
fclose($putdata);
|
||||
header("Status: 201");
|
||||
header('Content-Length: 12');
|
||||
exit("201 Created\n");
|
||||
} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
|
||||
if ($file === '*') {
|
||||
foreach (scandir(".data/clients/$c/") as $f) {
|
||||
if (str_starts_with($f, ".") || str_ends_with($f, ".php") || str_ends_with($f, ".inc")) continue;
|
||||
if (unlink(".data/clients/$c/$f") === false) {
|
||||
header("Status: 500");
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
echo "Deleted $f\n";
|
||||
}
|
||||
} else if (!is_file($path)) {
|
||||
header("Status: 404");
|
||||
header("Content-Length: 14");
|
||||
exit("404 Not Found\n");
|
||||
} else if (unlink($path) === false) {
|
||||
header("Status: 500");
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
exit("200 OK\n");
|
||||
} else {
|
||||
header("Status: 405");
|
||||
header("Allow: GET, PUT, DELETE");
|
||||
header("Content-Length: 23");
|
||||
exit("405 Method Not Allowed\n");
|
||||
}
|
||||
|
||||
exit();
|
||||
}
|
||||
|
||||
header("Status: 404");
|
||||
if ($format === 'text') {
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 14');
|
||||
echo "404 Not Found\n";
|
||||
} else if ($format === 'json') {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Content-Length: 39');
|
||||
echo "{\"errors\": [{\"message\": \"Not found\"}]}\n";
|
||||
} else {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
header('Content-Length: 0');
|
||||
}
|
||||
exit();
|
@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT" prefix="og: https://ogp.me/ns#">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Elwig</title>
|
||||
<title>Elwig - Elektronische Winzergenossenschaftsverwaltung</title>
|
||||
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
|
||||
<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"/>
|
||||
<link rel="stylesheet" href="/res/style.css?v=2025-01-12"/>
|
||||
<link rel="alternate" href="/en/" hreflang="en"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<meta name="theme-color" content="#A040C0"/>
|
||||
<meta name="mobile-web-app-capable" content="yes"/>
|
||||
@ -20,7 +21,7 @@
|
||||
<meta property="og:image:type" content="image/jpeg"/>
|
||||
<meta property="og:image:width" content="1200"/>
|
||||
<meta property="og:image:height" content="630"/>
|
||||
<meta property="og:locale" content="de_AT"/>
|
||||
<meta property="og:locale" content="de"/>
|
||||
<meta property="og:ttl" content="60"/>
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
</head>
|
||||
@ -28,16 +29,18 @@
|
||||
<header>
|
||||
<nav class="index">
|
||||
<div>
|
||||
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
|
||||
<a href="https://elwig.at/de/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
|
||||
</div>
|
||||
<ul>
|
||||
<li><a href="/#">Start</a></li>
|
||||
<li><a href="/#about">Über</a></li>
|
||||
<li><a href="/#clients">Genossenschaften</a></li>
|
||||
<li><a href="#">Start</a></li>
|
||||
<li><a href="#about">Über</a></li>
|
||||
<li><a href="#clients">Genossenschaften</a></li>
|
||||
<li><a href="/files/">Downloads</a></li>
|
||||
<li><a href="https://git.necronda.net/winzer/">Quellcode</a></li>
|
||||
</ul>
|
||||
<div/>
|
||||
<div>
|
||||
<a href="/en/" class="flag"><div/></a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@ -105,9 +108,9 @@
|
||||
<div class="filling"/>
|
||||
<div>
|
||||
<h4>WG Weinland</h4>
|
||||
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Großinzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h6>Zweigstellen:</h6>
|
||||
<div class="branches">Großinzersdorf</div>
|
||||
<div class="branches">Groß-Inzersdorf</div>
|
||||
<p class="link"></p>
|
||||
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
|
||||
</div>
|
149
www/en/index.xhtml
Normal file
149
www/en/index.xhtml
Normal file
@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Elwig - Electronic Management for Vintners' Cooperatives</title>
|
||||
<meta name="description" content="Electronic Management for Vintners' Cooperatives"/>
|
||||
<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?v=2025-01-12"/>
|
||||
<link rel="alternate" href="/de/" hreflang="de"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||
<meta name="theme-color" content="#A040C0"/>
|
||||
<meta name="mobile-web-app-capable" content="yes"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
||||
<meta property="og:site_name" content="Elwig"/>
|
||||
<meta property="og:title" content="Elwig"/>
|
||||
<meta property="og:description" content="Electronic Management for Vintners' Cooperatives"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:url" content="https://elwig.at/"/>
|
||||
<meta property="og:image" content="https://elwig.at/res/images/preview.jpg"/>
|
||||
<meta property="og:image:type" content="image/jpeg"/>
|
||||
<meta property="og:image:width" content="1200"/>
|
||||
<meta property="og:image:height" content="630"/>
|
||||
<meta property="og:locale" content="de"/>
|
||||
<meta property="og:ttl" content="60"/>
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
</head>
|
||||
<body class="header-footer" style="background-color: var(--bg-color);">
|
||||
<header>
|
||||
<nav class="index">
|
||||
<div>
|
||||
<a href="https://elwig.at/en/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
|
||||
</div>
|
||||
<ul>
|
||||
<li><a href="#">Start</a></li>
|
||||
<li><a href="#about">About</a></li>
|
||||
<li><a href="#clients">Cooperatives</a></li>
|
||||
<li><a href="/files/">Downloads</a></li>
|
||||
<li><a href="https://git.necronda.net/winzer/">Source Code</a></li>
|
||||
</ul>
|
||||
<div>
|
||||
<a href="/de/" class="flag"><div/></a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="home background">
|
||||
<a href="#about">
|
||||
<div>
|
||||
<img src="/res/elwig.png" alt="Elwig"/>
|
||||
<div style="max-width: 16em; margin-bottom: 0.5em;">
|
||||
<h1>Elwig</h1>
|
||||
<h2>Electronic Management for Vintners' Cooperatives</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="blur bottom"/>
|
||||
</section>
|
||||
|
||||
<section class="about">
|
||||
<span id="about"/>
|
||||
<h3>Elwig</h3>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="picture-1 background">
|
||||
<div class="blur top"/>
|
||||
<div class="blur bottom"/>
|
||||
</section>
|
||||
|
||||
<section class="clients">
|
||||
<span id="clients"/>
|
||||
<h3>The Cooperatives</h3>
|
||||
<div class="container background">
|
||||
<div>
|
||||
<h4>WG Matzen</h4>
|
||||
<h5><span>Winzergenossenschaft</span> <span>für Matzen und Umgebung</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h6>Branches:</h6>
|
||||
<div class="branches">Matzen</div>
|
||||
<p class="link">
|
||||
<a href="https://winzermatzen.at/">winzermatzen.at</a>
|
||||
</p>
|
||||
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
|
||||
</div>
|
||||
<div class="filling"/>
|
||||
<div>
|
||||
<h4>Winzerkeller im Weinviertel</h4>
|
||||
<h5><span>Winzerkeller im Weinviertel</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h6>Branches:</h6>
|
||||
<div class="branches">Wolkersdorf, Haugsdorf, Sitzendorf</div>
|
||||
<p class="link">
|
||||
<a href="http://www.winzerkeller.eu/">winzerkeller.eu</a>
|
||||
</p>
|
||||
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
|
||||
</div>
|
||||
<div class="filling"/>
|
||||
<div>
|
||||
<h4>WG Weinland</h4>
|
||||
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h6>Branches:</h6>
|
||||
<div class="branches">Groß-Inzersdorf</div>
|
||||
<p class="link"></p>
|
||||
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
|
||||
</div>
|
||||
<div class="filling"/>
|
||||
<div>
|
||||
<h4>WG Baden</h4>
|
||||
<h5><span>Winzergenossenschaft</span> <span>Baden - Bad Vöslau</span><br/><span>reg. Gen.m.b.H.</span></h5>
|
||||
<h6>Zweigstellen:</h6>
|
||||
<div class="branches">Baden</div>
|
||||
<p class="link">
|
||||
<a href="http://www.wg-baden.at/">wg-baden.at</a>
|
||||
</p>
|
||||
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="picture-2 background">
|
||||
<div class="blur top"/>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a>
|
||||
<p>
|
||||
<strong>Imprint</strong><br/>
|
||||
Lorenz Stechauner, Thomas Hilscher<br/>
|
||||
Austria<br/>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Contact</strong><br/>
|
||||
E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/>
|
||||
</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
28
www/files/debian/.update.sh
Executable file
28
www/files/debian/.update.sh
Executable file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
dpkg-scanpackages --arch all pool/ > dists/stable/main/binary-all/Packages
|
||||
cat dists/stable/main/binary-all/Packages | gzip -9 > dists/stable/main/binary-all/Packages.gz
|
||||
|
||||
do_hash() {
|
||||
HASH_NAME=$1
|
||||
HASH_CMD=$2
|
||||
echo "${HASH_NAME}:"
|
||||
for f in $(find dists/stable -type f -not -name '.*' -not -name 'Release'); do
|
||||
echo " $(${HASH_CMD} $f | cut -d" " -f1) $(wc -c $f | sed 's|dists/stable/||')"
|
||||
done
|
||||
}
|
||||
|
||||
cat > dists/stable/Release << EOF
|
||||
Origin: Elwig
|
||||
Suite: stable
|
||||
Codename: stable
|
||||
Version: 1.0
|
||||
Architectures: all
|
||||
Components: main
|
||||
Date: $(date -Ru)
|
||||
EOF
|
||||
do_hash "MD5Sum" "md5sum" >> dists/stable/Release
|
||||
do_hash "SHA1" "sha1sum" >> dists/stable/Release
|
||||
do_hash "SHA256" "sha256sum" >> dists/stable/Release
|
0
www/files/debian/pool/main/.gitkeep
Normal file
0
www/files/debian/pool/main/.gitkeep
Normal file
@ -5,22 +5,40 @@ require "../.php/auth.inc";
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||
authenticate();
|
||||
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
|
||||
$name = substr($_SERVER['PATH_INFO'], 1);
|
||||
if (str_contains($name, "..") || str_contains($name, "/")) {
|
||||
header('Status: 403');
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 14');
|
||||
exit("403 Forbidden\n");
|
||||
} else if (!isset($_SERVER['HTTP_CONTENT_LENGTH'])) {
|
||||
header('Status: 411');
|
||||
header('Content-Length: 20');
|
||||
exit("411 Length Required\n");
|
||||
}
|
||||
|
||||
$upload = fopen("php://input", "r");
|
||||
$fp = fopen($name, "wb+");
|
||||
$fp = fopen("/tmp/upload-$name", "wb+");
|
||||
if (!$upload || !$fp) {
|
||||
fclose($fp);
|
||||
fclose($upload);
|
||||
header('Status: 500');
|
||||
header('Content-Length: 26');
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
|
||||
while ($data = fread($upload, 4096)) fwrite($fp, $data);
|
||||
fclose($fp);
|
||||
fclose($upload);
|
||||
|
||||
if (!rename("/tmp/upload-$name", $name)) {
|
||||
header('Status: 500');
|
||||
header('Content-Length: 26');
|
||||
exit("500 Internal Server Error\n");
|
||||
}
|
||||
|
||||
header('Status: 201');
|
||||
header('Content-Type: text/plain; charset=UTF-8');
|
||||
header('Content-Length: 12');
|
||||
exit("201 Created\n");
|
||||
} else if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
|
||||
@ -120,7 +138,7 @@ if ($format === 'json') {
|
||||
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-AT" prefix="og: https://ogp.me/ns#">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Downloads - Elwig</title>
|
||||
@ -141,7 +159,7 @@ if ($format === 'json') {
|
||||
<meta property="og:image:type" content="image/jpeg"/>
|
||||
<meta property="og:image:width" content="1200"/>
|
||||
<meta property="og:image:height" content="630"/>
|
||||
<meta property="og:locale" content="de_AT"/>
|
||||
<meta property="og:locale" content="de"/>
|
||||
<meta property="og:ttl" content="60"/>
|
||||
<meta name="twitter:card" content="summary_large_image"/>
|
||||
</head>
|
||||
|
@ -1,9 +1,26 @@
|
||||
<?php
|
||||
header("Content-Length: 0");
|
||||
switch ($_SERVER['PATH_INFO']) {
|
||||
case '':
|
||||
case '/':
|
||||
$lang = 'de'; // prefer german
|
||||
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $pref) {
|
||||
$l = substr($pref, 0, 2);
|
||||
if ($l === 'de') {
|
||||
$lang = 'de';
|
||||
break; // force german
|
||||
} else if ($l === 'en') {
|
||||
$lang = 'en'; // use english only, if user specifically asks for it
|
||||
}
|
||||
}
|
||||
}
|
||||
header("Status: 303");
|
||||
header("Location: /$lang/");
|
||||
break;
|
||||
case '/changelog':
|
||||
header("Status: 303");
|
||||
header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md");
|
||||
header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md#changelog");
|
||||
break;
|
||||
case '/vcs':
|
||||
case '/git':
|
||||
@ -13,6 +30,22 @@ switch ($_SERVER['PATH_INFO']) {
|
||||
header("Status: 303");
|
||||
header("Location: https://git.necronda.net/winzer/");
|
||||
break;
|
||||
case '/access':
|
||||
header("Status: 303");
|
||||
header("Location: https://access.elwig.at/");
|
||||
break;
|
||||
case '/clients':
|
||||
header("Status: 303");
|
||||
header("Location: https://sync.elwig.at/");
|
||||
break;
|
||||
default:
|
||||
header("Status: 404");
|
||||
if (str_starts_with($_SERVER['PATH_INFO'], '/access/')) {
|
||||
header("Status: 308");
|
||||
header("Location: https://access.elwig.at/" . substr($_SERVER['PATH_INFO'], 8));
|
||||
} else if (str_starts_with($_SERVER['PATH_INFO'], '/clients/')) {
|
||||
header("Status: 308");
|
||||
header("Location: https://sync.elwig.at/" . substr($_SERVER['PATH_INFO'], 9));
|
||||
} else {
|
||||
header("Status: 404");
|
||||
}
|
||||
}
|
||||
|
@ -1,207 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
function getCredentialsUsername(client) {
|
||||
return window.localStorage.getItem(`${CLIENT}/${client}/username`);
|
||||
}
|
||||
|
||||
function getCredentialsPassword(client) {
|
||||
return window.localStorage.getItem(`${CLIENT}/${client}/password`);
|
||||
}
|
||||
|
||||
function getBasicAuth(client) {
|
||||
return {
|
||||
'Authorization': 'Basic ' + btoa(getCredentialsUsername(client) + ':' + getCredentialsPassword(client)),
|
||||
}
|
||||
}
|
||||
|
||||
async function _get(client, path) {
|
||||
const res = await fetch(`${CLIENTS[client]['api']}${path}`, {
|
||||
method: 'GET',
|
||||
headers: {...getBasicAuth(client)},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new ApiError(res.status, json['message']);
|
||||
return json;
|
||||
}
|
||||
|
||||
async function get(client, path) {
|
||||
return (await _get(client, path))['data'];
|
||||
}
|
||||
|
||||
async function getWineVarieties(client) {
|
||||
return Object.fromEntries((await get(client, '/wine/varieties')).map(item => [item['sortid'], item]));
|
||||
}
|
||||
|
||||
async function getWineQualityLevels(client) {
|
||||
return Object.fromEntries((await get(client, '/wine/quality_levels')).map(item => [item['qualid'], item]));
|
||||
}
|
||||
|
||||
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 load(client) {
|
||||
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.WINE_VARIETIES = await getWineVarieties(client);
|
||||
window.WINE_QUALITY_LEVELS = await getWineQualityLevels(client);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (form) {
|
||||
window.localStorage.removeItem(`${CLIENT}/${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 = `#/${client}/login`;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
//await load();
|
||||
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 = '';
|
||||
|
||||
let client = null;
|
||||
for (const id in CLIENTS) {
|
||||
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
|
||||
client = id;
|
||||
}
|
||||
}
|
||||
|
||||
if (client === null) {
|
||||
window.location.hash = `#/${Object.keys(CLIENTS)[0]}`;
|
||||
return;
|
||||
}
|
||||
nav.children[Object.keys(CLIENTS).indexOf(client)].className = 'active';
|
||||
|
||||
if ((!getCredentialsUsername(client) || !getCredentialsPassword(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="${getCredentialsUsername(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>`;
|
||||
updateOverview(client).then();
|
||||
} else {
|
||||
window.location.hash = `#/${client}`;
|
||||
}
|
||||
}
|
||||
|
||||
function update() {
|
||||
const hash = window.location.hash;
|
||||
let client = null;
|
||||
for (const id in CLIENTS) {
|
||||
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
|
||||
client = id;
|
||||
}
|
||||
}
|
||||
|
||||
if (document.hidden) {
|
||||
// do nothing
|
||||
} else {
|
||||
if (hash === `#/${client}`) {
|
||||
updateOverview(client).then();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
window.localStorage.setItem(`${CLIENT}/${form.client.value}/username`, form.username.value);
|
||||
window.localStorage.setItem(`${CLIENT}/${form.client.value}/password`, form.password.value);
|
||||
load(form.client.value).then(success => {
|
||||
if (success) window.location.hash = `#/${form.client.value}`;
|
||||
});
|
||||
return false;
|
||||
}
|
@ -1,331 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
window.addEventListener('pageshow', update)
|
||||
|
||||
document.addEventListener('visibilitychange', update);
|
||||
|
||||
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();
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
"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.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;
|
||||
}
|
@ -81,11 +81,63 @@ nav li.active a{
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
/**** Index ****/
|
||||
nav a.flag {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
main span[id] {
|
||||
position: relative;
|
||||
top: -8em;
|
||||
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,
|
||||
@ -93,6 +145,22 @@ table a {
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
main section h3 {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/**** Index ****/
|
||||
|
||||
main span[id] {
|
||||
position: relative;
|
||||
top: -8em;
|
||||
}
|
||||
|
||||
main .background {
|
||||
background-image: var(--img);
|
||||
background-repeat: no-repeat;
|
||||
@ -204,15 +272,6 @@ main section p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main section h3 {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
main .about {
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
@ -331,29 +390,6 @@ table .unit {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
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 (min-width: 1921px) {
|
||||
main .picture-1 {
|
||||
height: calc(36em + 4rem);
|
||||
@ -374,16 +410,6 @@ footer a {
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
body.header-footer {
|
||||
min-height: calc(100vh + 20em);
|
||||
padding-bottom: 20em;
|
||||
@ -422,197 +448,3 @@ footer a {
|
||||
font-size: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
/**** 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
# robots.txt for elwig.at
|
||||
User-Agent: *
|
||||
Disallow: /clients/
|
||||
Sitemap: https://elwig.at/sitemap.xml
|
||||
|
@ -1,10 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://elwig.at/</loc>
|
||||
<loc>https://elwig.at/de/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://elwig.at/en/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://elwig.at/files/</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
|
Reference in New Issue
Block a user