Compare commits

..

No commits in common. "ec5a69f3e6abb34197a4076bedaa3143d97fdaad" and "f4cdd0ae283d8afe5e577304c62a10bd45a89ac1" have entirely different histories.

53 changed files with 1067 additions and 2458 deletions

View File

@ -1,6 +0,0 @@
target/
db.sqlite
.history/
frontend/node_modules/*
/static/
/data-ergo/

View File

@ -11,62 +11,59 @@ env:
jobs:
test:
runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
container: rust:latest
steps:
- uses: actions/checkout@v3
- name: Run Test DB Script
run: ./test_db.sh
- name: Setup Environment
run: |
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
- name: Checkout
uses: actions/checkout@v3
- name: Run Test DB Script
run: ./test_db.sh
- name: Build
run: |
cargo build
cd frontend && npm install && npm run build
- name: Run Tests
run: cargo test --verbose
deploy-staging:
runs-on: ubuntu-latest
container: rust:latest
needs: [test]
if: github.ref == 'refs/heads/staging'
steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev
# Handling NodeSource GPG key
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then
rm /etc/apt/keyrings/nodesource.gpg
fi
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key
# Adding NodeSource repository
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# Installing Node.js and npm
apt-get update
apt-get install nodejs -y
apt-get install npm -y
- name: Set up cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-debug-
- name: Build
run: |
cargo build
cd frontend && npm install && npm run build
- name: Frontend tests
run: cd frontend && npx playwright install && npx playwright test --workers 1
- name: Backend tests
run: cargo test --verbose
#- uses: actions/upload-artifact@v3
# if: always()
# with:
# name: playwright-report
# path: frontend/playwright-report/
# retention-days: 30
deploy-staging:
runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
needs: [test]
if: github.ref == 'refs/heads/staging'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Run Test DB Script
run: ./test_db.sh
- name: Set up cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release-
- name: Build
run: |
cargo build --release --target $CARGO_TARGET
@ -75,20 +72,20 @@ jobs:
- name: Deploy to Staging
run: |
mkdir -p ~/.ssh
mkdir ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/rot-updating
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing-staging/
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm /home/philipp/rowing-staging/db.sqlite && cp /home/philipp/rowing/db.sqlite /home/philipp/rowing-staging/db.sqlite && mkdir -p /home/philipp/rowing-staging/svelte/build && mkdir -p /home/philipp/rowing-staging/data-ergo/thirty && mkdir -p /home/philipp/rowing-staging/data-ergo/dozen && sqlite3 /home/philipp/rowing-staging/db.sqlite < /home/philipp/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing-staging/rot-updating /home/philipp/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
@ -97,48 +94,41 @@ jobs:
deploy-main:
runs-on: ubuntu-latest
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240215
container: rust:latest
needs: [test]
if: github.ref == 'refs/heads/main'
steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq pkg-config sshpass musl musl-tools sqlite3 curl gnupg libssl-dev && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
- name: Checkout
uses: actions/checkout@v3
- name: Run Test DB Script
run: ./test_db.sh
- name: Set up cargo cache
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-release-
- name: Build
run: |
cargo build --release --target $CARGO_TARGET
strip target/$CARGO_TARGET/release/rot
cd frontend && npm install && npm run build
- name: Deploy to production
- name: Deploy to Main
run: |
mkdir -p ~/.ssh
mkdir ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/philipp/rowing/rot-updating
scp -r static $SSH_USER@$SSH_HOST:/home/philipp/rowing/
scp -r templates $SSH_USER@$SSH_HOST:/home/philipp/rowing/
scp -r svelte $SSH_USER@$SSH_HOST:/home/philipp/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/philipp/rowing/svelte/build && mkdir -p /home/philipp/rowing/data-ergo/thirty && mkdir -p /home/philipp/rowing/data-ergo/dozen'
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating
scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/
scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/
scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
ssh $SSH_USER@$SSH_HOST 'mv /home/philipp/rowing/rot-updating /home/philipp/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

543
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ rest = []
rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
log = "0.4"
env_logger = "0.11"
env_logger = "0.10"
sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]}

View File

@ -1,25 +0,0 @@
# This dockerfile is used as basis for the CI jobs.
# Process to renew it:
# 0. Login to gitea docker registry: `docker login git.hofer.link`
# 1. Build the image `docker build .`
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
FROM rust:1.76
RUN apt-get update && apt-get install -y sqlite3
# nodejs
RUN apt-get install -y curl && \
curl -sL https://deb.nodesource.com/setup_21.x | bash - && \
apt-get install -y nodejs
# playwright
RUN npx playwright install --with-deps
# deployment
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev
# TEMPORARY act workaround (otherwise gitea cache is not working)
RUN apt-get install -y zstd

View File

@ -1,18 +1,15 @@
# Build
## Frontend
1. `cd frontend`
2. `npm install`
3. `npm run (watch/build)`
# Frontend Process
´cd frontend´
´npm install´
´npm run (watch/build)´
# Notes / Bugfixes
## Frontend
- [] support esc to close sidebar
- [] reload page -> don't throw input away!
# Run
## Backend
1. `cargo r`
# Test
# Nice to have
## Frontend
- `npx playwright test --workers 1 --project firefox`
- Nice UI: `--ui`
- Generate tests: `npx playwright codegen`
## Backend (Unit + Integration)
`cargo t`
- [] my trips for cox

5
frontend/.gitignore vendored
View File

@ -1,6 +1 @@
package-lock.json
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@ -6,7 +6,6 @@ export interface choiceMap {
}
let choiceObjects: choiceMap = {};
let boat_in_ottensheim = true;
document.addEventListener('DOMContentLoaded', function() {
changeTheme();
@ -107,7 +106,6 @@ interface ChoiceBoatEvent extends Event{
amount_seats: number,
owner: number,
default_destination: string,
boat_in_ottensheim: boolean,
}
};
}
@ -125,8 +123,6 @@ function selectBoatChange() {
boatSelect.addEventListener('addItem', function(e) {
const event = e as ChoiceBoatEvent;
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
const amount_seats = event.detail.customProperties.amount_seats;
setMaxAmountRowers("newrower", amount_seats);
@ -278,7 +274,6 @@ interface ChoiceEvent extends Event{
is_cox: boolean,
steers: boolean,
cox_on_boat: boolean,
is_racing: boolean,
}
};
}
@ -325,16 +320,6 @@ function initNewChoice(select: HTMLInputElement) {
},
callbackOnInit: function() {
this._currentState.items.forEach(function(obj){
if (boat_in_ottensheim && obj.customProperties) {
if (obj.customProperties.is_racing) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
var new_option = new Option(obj.label, obj.value);
if (obj.customProperties.cox_on_boat){
new_option.selected = true;
}
coxSelect.add(new_option);
}
}
if (obj.customProperties && obj.customProperties.is_cox){
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
var new_option = new Option(obj.label, obj.value);
@ -361,14 +346,6 @@ function initNewChoice(select: HTMLInputElement) {
const user_id = event.detail.value;
const name = event.detail.label;
if (boat_in_ottensheim && event.detail.customProperties.is_racing) {
if (event.detail.customProperties.is_racing) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
if (coxSelect){
coxSelect.add(new Option(name, user_id));
}
}
}
if (event.detail.customProperties.is_cox) {
const coxSelect = <HTMLSelectElement>document.querySelector('#shipmaster-' + select.id + 'js');
if (coxSelect){
@ -484,6 +461,10 @@ function filterAction(activeFilter: string) {
filterCoxs();
break;
}
case 'filter-months': {
filterMonths(activeFilter)
break;
}
}
}
@ -501,6 +482,28 @@ function filterCoxs() {
});
}
function filterMonths(action: string) {
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const monthToggle = <HTMLButtonElement>document.querySelector('button[data-action="' + action + '"]');
if(monthToggle) {
const currentMonth = monthToggle.dataset.month;
if(currentMonth) {
const index = months.indexOf(currentMonth);
if (index > -1) { // only splice array when item is found
months.splice(index, 1); // 2nd parameter means remove one item only
}
Array.prototype.forEach.call(months, (month: HTMLElement) => {
const notThisMonth = document.querySelectorAll('div[data-month="' + month + '"]');
Array.prototype.forEach.call(notThisMonth, (notThisMonth: HTMLElement) => {
notThisMonth.classList.toggle('hidden');
});
});
}
}
}
function initSearch() {
const input = <HTMLInputElement>document.querySelector('#filter-js');

View File

@ -9,9 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"@playwright/test": "^1.40.1",
"@types/d3": "^7.4.1",
"@types/node": "^20.11.4",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"sass": "^1.60.0",

View File

@ -1,77 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 180000,
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
//{
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
//},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
//{
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
//},
/* Test against branded browsers. */
//{
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
//},
//{
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
//},
],
/* Run your local dev server before starting the tests */
webServer: {
timeout: 15 * 60 * 1000,
command: 'cd .. && ./test_db.sh && cargo r',
},
});

View File

@ -1,116 +0,0 @@
import { test, expect, Page } from '@playwright/test';
test('cox can create and delete trip', async ({ page }) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await page.locator('.relative').first().click();
await page.locator('#sidebar #planned_starting_time').click();
await page.locator('#sidebar #planned_starting_time').fill('18:00');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.getByRole('spinbutton').fill('5');
await page.getByRole('button', { name: 'Erstellen', exact: true }).click();
await expect(page.locator('body')).toContainText('18:00 Uhr (cox) Details');
await page.goto('http://localhost:8000/planned');
await page.getByRole('link', { name: 'Details' }).click();
await page.getByRole('link', { name: 'Termin löschen' }).click();
await expect(page.locator('body')).toContainText('Erfolgreich gelöscht!');
});
// TODO: group -> cox can create trips
// TODO: cox can help/register at trips/events
test.describe('cox can edit trips', () => {
let sharedPage: Page;
test.beforeEach(async ({ browser }) => {
const page = await browser.newPage();
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.getByRole('link', { name: 'Geplante Ausfahrten' }).click();
await page.locator('.relative').first().click();
await page.locator('#sidebar #planned_starting_time').click();
await page.locator('#sidebar #planned_starting_time').fill('18:00');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.locator('#sidebar #planned_starting_time').press('Tab');
await page.getByRole('spinbutton').fill('5');
await page.getByRole('button', { name: 'Erstellen', exact: true }).click();
sharedPage = page;
});
test('edit remarks', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.locator('#sidebar #notes').click();
await sharedPage.locator('#sidebar #notes').fill('Meine Anmerkung');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Meine Anmerkung');
await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click();
});
test('add and remove guest', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.locator('#sidebar #user_note').click();
await sharedPage.locator('#sidebar #user_note').fill('Mein Gast');
await sharedPage.getByRole('button', { name: 'Gast hinzufügen' }).click();
await expect(sharedPage.locator('body')).toContainText('Erfolgreich angemeldet!');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 4');
await expect(sharedPage.locator('#sidebar')).toContainText('Mein Gast (Gast) Abmelden');
await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).not.toBeVisible();
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
await expect(sharedPage.locator('body')).toContainText('Erfolgreich abgemeldet!');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await expect(sharedPage.locator('#sidebar')).toContainText('Keine Ruderer angemeldet');
await expect(sharedPage.getByRole('link', { name: 'Termin löschen' })).toBeVisible();
await sharedPage.getByRole('button', { name: 'Ausfahrt erstellen schließen' }).click();
});
test('change amount rower', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await sharedPage.getByRole('spinbutton').click();
await sharedPage.getByRole('spinbutton').fill('3');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
});
test('call off trip', async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await expect(sharedPage.locator('#sidebar')).toContainText('Freie Plätze: 5');
await sharedPage.getByRole('spinbutton').click();
await sharedPage.getByRole('spinbutton').fill('0');
await sharedPage.getByRole('button', { name: 'Speichern' }).click();
await expect(sharedPage.locator('body')).toContainText('Ausfahrt erfolgreich aktualisiert.');
await expect(sharedPage.locator('body')).toContainText('(Absage cox )');
});
test.afterEach(async () => {
await sharedPage.goto('http://localhost:8000/planned');
await sharedPage.getByRole('link', { name: 'Details' }).click();
await sharedPage.getByRole('link', { name: 'Termin löschen' }).click();
await sharedPage.close();
});
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
});

View File

@ -1,69 +0,0 @@
import { test, expect } from '@playwright/test';
test('Cox can start and cancel trip', async ({ page }, testInfo) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox2');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.goto('http://localhost:8000/');
await page.getByRole('link', { name: 'Ausfahrt eintragen' }).click();
if (testInfo.project.name.includes('Mobile')) { // No left boat selector on mobile views
await page.getByText('Kaputtes Boot :-( (7 x)').nth(1).click();
await page.getByRole('option', { name: 'Joe' }).click();
} else{
await page.getByText('Joe', { exact: true }).click();
}
await page.getByPlaceholder('Ruderer auswählen').click();
await page.getByRole('option', { name: 'rower2' }).click();
await page.getByRole('option', { name: 'cox2' }).click();
await expect(page.getByRole('listbox')).toContainText('Nur 2 Ruderer können hinzugefügt werden');
await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox');
await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox');
await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt');
await expect(page.locator('body')).toContainText('Joe');
await page.getByRole('link', { name: 'Joe' }).click();
page.once('dialog', dialog => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
});
test('Cox can start and finish trip', async ({ page }, testInfo) => {
await page.goto('http://localhost:8000/auth');
await page.getByPlaceholder('Name').click();
await page.getByPlaceholder('Name').fill('cox2');
await page.getByPlaceholder('Name').press('Tab');
await page.getByPlaceholder('Passwort').fill('cox');
await page.getByPlaceholder('Passwort').press('Enter');
await page.goto('http://localhost:8000/');
await page.getByRole('link', { name: 'Ausfahrt eintragen' }).click();
if (testInfo.project.name.includes('Mobile')) { // No left boat selector on mobile views
await page.getByText('Kaputtes Boot :-( (7 x)').nth(1).click();
await page.getByRole('option', { name: 'Joe' }).click();
} else{
await page.getByText('Joe', { exact: true }).click();
}
await page.getByPlaceholder('Ruderer auswählen').click();
await page.getByRole('option', { name: 'rower2' }).click();
await page.getByRole('option', { name: 'cox2' }).click();
await expect(page.getByRole('listbox')).toContainText('Nur 2 Ruderer können hinzugefügt werden');
await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox');
await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox');
await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt');
await expect(page.locator('body')).toContainText('Joe');
await page.goto('http://localhost:8000/log');
await page.waitForTimeout(60000);
await page.locator('div:nth-child(2) > .border-0').click();
await page.getByRole('combobox', { name: 'Destination' }).click();
await page.getByRole('combobox', { name: 'Destination' }).fill('Ottensheim');
await page.getByRole('button', { name: 'Ausfahrt beenden' }).click();
await expect(page.locator('body')).toContainText('Ausfahrt korrekt eingetragen');
});

View File

@ -15,12 +15,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"nickname" text,
"notes" text,
"phone" text,
"address" text,
"family_id" INTEGER REFERENCES family(id)
);
CREATE TABLE IF NOT EXISTS "family" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
"address" text
);
CREATE TABLE IF NOT EXISTS "role" (

View File

@ -1,73 +0,0 @@
# Wordpress auth
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function rot_auth( $user, $username, $password ){
// Make sure a username and password are present for us to work with
if($username == '' || $password == '') return;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the cURL session and get the response
$response = curl_exec($ch);
// Check for cURL errors
if(curl_errno($ch)){
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
}
// Close the cURL session
curl_close($ch);
if (strpos($response, 'SUCC') !== false) {
$user = get_user_by('login', $username);
if (!$user) {
// User does not exist, create a new one
$userdata = array(
'user_email' => $username,
'user_login' => $username,
'first_name' => $username,
'last_name' => ''
);
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Load the new user info
$user = new WP_User($new_user_id);
// Set role based on username
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
$user->set_role('administrator');
} else {
$user->set_role('editor');
}
} else {
// Handle error in user creation
return $new_user_id;
}
} else {
}
} else {
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
}
return $user;
}
// Comment this line if you wish to fall back on WordPress authentication
// Useful for times when the external service is offline
remove_action('authenticate', 'wp_authenticate_username_password', 20);
add_filter( 'authenticate', 'rot_auth', 10, 3 );
```

10
package-lock.json generated Normal file
View File

@ -0,0 +1,10 @@
{
"name": "rot",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rot"
}
}
}

View File

@ -4,7 +4,7 @@ Description=Rot
[Service]
User=root
Group=root
WorkingDirectory=/home/philipp/rowing
WorkingDirectory=/home/k004373/rowing
Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001"

View File

@ -4,12 +4,12 @@ Description=Rot Staging
[Service]
User=root
Group=root
WorkingDirectory=/home/philipp/rowing-staging
WorkingDirectory=/home/k004373/rowing-staging
Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=7999"
Environment="ROCKET_LOG=info"
ExecStart=/home/philipp/rowing-staging/rot
ExecStart=/home/k004373/rowing-staging/rot
[Install]
WantedBy=multi-user.target

View File

@ -2,32 +2,18 @@ INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('planned_event');
INSERT INTO "role" (name) VALUES ('Rennrudern');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,6);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
INSERT INTO "user" (name) VALUES('new');
INSERT INTO "user_role" (user_id, role_id) VALUES(5,5);
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(6,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(7,5);
INSERT INTO "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);

1
shame.txt Normal file
View File

@ -0,0 +1 @@
2023-06-06: Phil Baillon um 19:10 für 18 Uhr Fahrt abgemeldet

View File

@ -1,5 +1,3 @@
#![allow(clippy::blocks_in_conditions)]
pub mod model;
#[cfg(feature = "rowing-tera")]

View File

@ -1,5 +1,3 @@
#![allow(clippy::blocks_in_conditions)]
use std::str::FromStr;
#[cfg(feature = "rest")]

View File

@ -185,7 +185,7 @@ ORDER BY amount_seats DESC
if user.has_role(db, "admin").await {
return Self::all(db).await;
}
let mut boats = if user.has_role(db, "cox").await {
let boats = if user.has_role(db, "cox").await {
sqlx::query_as!(
Boat,
"
@ -215,23 +215,6 @@ ORDER BY amount_seats DESC
.unwrap() //TODO: fixme
};
if user.has_role(db, "Rennrudern").await {
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
.await
.unwrap();
let boats_in_ottensheim = sqlx::query_as!(
Boat,
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
FROM boat
WHERE owner is null and location_id = ?
ORDER BY amount_seats DESC
",ottensheim.id)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
boats.extend(boats_in_ottensheim.into_iter());
}
Self::boats_to_details(db, boats).await
}

View File

@ -1,83 +0,0 @@
use serde::Serialize;
use sqlx::{sqlite::SqliteQueryResult, FromRow, SqlitePool};
use super::user::User;
#[derive(FromRow, Serialize, Clone)]
pub struct Family {
id: i64,
}
#[derive(Serialize, Clone)]
pub struct FamilyWithMembers {
id: i64,
names: Option<String>,
}
impl Family {
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id FROM role")
.fetch_all(db)
.await
.unwrap()
}
pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db)
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
sqlx::query_as!(
FamilyWithMembers,
"
SELECT
family.id as id,
GROUP_CONCAT(user.name, ', ') as names
FROM family
LEFT JOIN
user ON family.id = user.family_id
GROUP BY family.id;"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
if let Some(id) = id {
Self::find_by_id(db, id).await
} else {
None
}
}
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@ -264,16 +264,14 @@ ORDER BY departure DESC
return Err(LogbookCreateError::BoatNotFound);
};
if boat.amount_seats == 1 && log.rowers.is_empty() {
log.rowers = vec![created_by_user.id];
}
if boat.amount_seats == 1 {
log.shipmaster = Some(log.rowers[0]);
log.steering_person = Some(log.rowers[0]);
}
if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) {
//TODO: fix clone() above
if !boat.shipmaster_allowed(db, created_by_user).await {
return Err(LogbookCreateError::UserNotAllowedToUseBoat);
}

View File

@ -1,7 +1,10 @@
use std::{error::Error, fs};
use std::error::Error;
use lettre::{
message::{header::ContentType, Attachment, MultiPart, SinglePart},
message::{
header::{self, ContentType},
MultiPart, SinglePart,
},
transport::smtp::authentication::Credentials,
Message, SmtpTransport, Transport,
};
@ -9,7 +12,7 @@ use sqlx::SqlitePool;
use crate::tera::admin::mail::MailToSend;
use super::{family::Family, log::Log, role::Role, user::User};
use super::role::Role;
pub struct Mail {}
@ -33,38 +36,17 @@ impl Mail {
for rec in role.mails_from_role(db).await {
let splitted = rec.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {rec}, because it could not be parsed"),
)
.await;
}
}
email = email.bcc(single_rec.parse().unwrap());
}
}
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
// TODO: handle attachments
for temp_file in &data.files {
let content = fs::read(temp_file.path().unwrap()).unwrap();
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
let content_type = ContentType::parse(&media_type).unwrap();
if let Some(name) = temp_file.name() {
let attachment = Attachment::new(format!(
"{}.{}",
name,
temp_file.content_type().unwrap().extension().unwrap()
))
.body(content, content_type);
multipart = multipart.singlepart(attachment);
}
}
let email = email.subject(data.subject).multipart(multipart).unwrap();
let email = email
.subject(data.subject)
.header(ContentType::TEXT_PLAIN)
.body(String::from(data.body))
.unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
@ -80,106 +62,4 @@ impl Mail {
};
false
}
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
let users = User::all_payer_groups(db).await;
for user in users {
if !user.has_role(db, "paid").await {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\
Der Vorstand
");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds =
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
}
}
}
}
}
}

View File

@ -9,7 +9,6 @@ use self::{
pub mod boat;
pub mod boatdamage;
pub mod family;
pub mod location;
pub mod log;
pub mod logbook;

View File

@ -210,7 +210,6 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
pub async fn update(
&self,
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
max_people: i32,
notes: Option<&str>,
@ -218,8 +217,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
is_locked: bool,
) {
sqlx::query!(
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
name,
"UPDATE planned_event SET planned_amount_cox = ? WHERE id = ?",
planned_amount_cox,
self.id
)

View File

@ -3,8 +3,8 @@ use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize, Clone)]
pub struct Role {
pub(crate) id: i64,
pub(crate) name: String,
id: i64,
name: String,
}
impl Role {
@ -30,28 +30,13 @@ WHERE id like ?
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM role
WHERE name like ?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!(
"SELECT u.mail
FROM user u
JOIN user_role ur ON u.id = ur.user_id
JOIN role r ON ur.role_id = r.id
WHERE r.id = {} AND deleted=0;",
WHERE r.id = {}",
self.id
);

View File

@ -16,7 +16,7 @@ impl Rower {
sqlx::query_as!(
User,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
",

View File

@ -66,14 +66,9 @@ SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM user u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE u.id NOT IN (
SELECT ur.user_id
FROM user_role ur
INNER JOIN role ro ON ur.role_id = ro.id
WHERE ro.name = 'Donau Linz'
)
AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%';
INNER JOIN user_role ur ON u.id = ur.user_id
INNER JOIN role ro ON ur.role_id = ro.id
WHERE ro.name = 'scheckbuch' AND l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
"
))
.fetch_one(db)
@ -98,10 +93,10 @@ AND l.arrival LIKE '{year}-%';
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM (
SELECT * FROM user
WHERE id IN (
WHERE id NOT IN (
SELECT user_id FROM user_role
JOIN role ON user_role.role_id = role.id
WHERE role.name = 'Donau Linz'
WHERE role.name = 'scheckbuch'
)
) u
INNER JOIN rower r ON u.id = r.rower_id

View File

@ -13,18 +13,9 @@ use rocket::{
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{family::Family, log::Log, role::Role, tripdetails::TripDetails, Day};
use super::{log::Log, tripdetails::TripDetails, Day};
use crate::tera::admin::user::UserEditForm;
const RENNRUDERBEITRAG: i32 = 11000;
const BOAT_STORAGE: i32 = 4500;
const FAMILY_TWO: i32 = 30000;
const FAMILY_THREE_OR_MORE: i32 = 35000;
const STUDENT_OR_PUPIL: i32 = 8000;
const REGULAR: i32 = 22000;
const UNTERSTUETZEND: i32 = 2500;
const FOERDERND: i32 = 8500;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct User {
pub id: i64,
@ -42,7 +33,6 @@ pub struct User {
pub notes: Option<String>,
pub phone: Option<String>,
pub address: Option<String>,
pub family_id: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -99,132 +89,7 @@ pub enum LoginError {
DeserializationError,
}
#[derive(Debug, Serialize)]
pub struct Fee {
pub sum_in_cents: i32,
pub parts: Vec<(String, i32)>,
pub name: String,
pub user_ids: String,
pub paid: bool,
}
impl Fee {
pub fn new() -> Self {
Self {
sum_in_cents: 0,
name: "".into(),
parts: Vec::new(),
user_ids: "".into(),
paid: false,
}
}
pub fn add(&mut self, desc: String, price_in_cents: i32) {
self.sum_in_cents += price_in_cents;
self.parts.push((desc, price_in_cents));
}
pub fn add_person(&mut self, user: &User) {
if !self.name.is_empty() {
self.name.push_str(" + ");
self.user_ids.push('&');
}
self.name.push_str(&user.name);
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
}
pub fn paid(&mut self) {
self.paid = true;
}
pub fn merge(&mut self, fee: Fee) {
for (desc, price_in_cents) in fee.parts {
self.add(desc, price_in_cents);
}
}
}
impl User {
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
if !self.has_role(db, "Donau Linz").await {
return None;
}
if self.deleted {
return None;
}
let mut fee = Fee::new();
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
for member in family.members(db).await {
fee.add_person(&member);
if member.has_role(db, "paid").await {
fee.paid();
}
fee.merge(member.fee_without_families(db).await);
}
if family.amount_family_members(db).await > 2 {
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
} else {
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
}
} else {
fee.add_person(self);
if self.has_role(db, "paid").await {
fee.paid();
}
fee.merge(self.fee_without_families(db).await);
}
Some(fee)
}
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
let mut fee = Fee::new();
if !self.has_role(db, "Donau Linz").await {
return fee;
}
if self.has_role(db, "Rennrudern").await {
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
}
let amount_boats = self.amount_boats(db).await;
if amount_boats > 0 {
fee.add(
format!("{}x Bootsplatz", amount_boats),
amount_boats * BOAT_STORAGE,
);
}
if self.has_role(db, "Unterstützend").await {
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
} else if self.has_role(db, "Förderndes Mitglied").await {
fee.add("Förderndes Mitglied".into(), FOERDERND);
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
} else {
fee.add("Mitgliedsbeitrag".into(), REGULAR);
}
}
fee
}
pub async fn amount_boats(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COUNT(*) as count FROM boat WHERE owner = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn rowed_km(&self, db: &SqlitePool) -> i32 {
sqlx::query!(
"SELECT COALESCE(SUM(distance_in_km),0) as rowed_km
@ -296,7 +161,7 @@ impl User {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE id like ?
",
@ -311,7 +176,7 @@ WHERE id like ?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE id like ?
",
@ -326,7 +191,7 @@ WHERE id like ?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE name like ?
",
@ -368,7 +233,7 @@ WHERE name like ?
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE deleted = 0
ORDER BY last_access DESC
@ -379,31 +244,11 @@ ORDER BY last_access DESC
.unwrap()
}
pub async fn all_payer_groups(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
WHERE family_id IS NOT NULL
GROUP BY family_id
UNION
-- Select users with a null family_id, without grouping
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
WHERE family_id IS NULL;
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn ergo(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
ORDER BY name
@ -418,7 +263,7 @@ ORDER BY name
sqlx::query_as!(
Self,
"
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address
FROM user
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
ORDER BY last_access DESC
@ -437,14 +282,8 @@ ORDER BY last_access DESC
}
pub async fn update(&self, db: &SqlitePool, data: UserEditForm) {
let mut family_id = data.family_id;
if family_id.is_some_and(|x| x == -1) {
family_id = Some(Family::insert(db).await)
}
sqlx::query!(
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=?, family_id = ? where id = ?",
"UPDATE user SET dob = ?, weight = ?, sex = ?, member_since_date=?, birthdate=?, mail=?, nickname=?, notes=?, phone=?, address=? where id = ?",
data.dob,
data.weight,
data.sex,
@ -455,7 +294,6 @@ ORDER BY last_access DESC
data.notes,
data.phone,
data.address,
family_id,
self.id
)
.execute(db)
@ -469,66 +307,21 @@ ORDER BY last_access DESC
.unwrap();
for role_id in data.roles.into_keys() {
self.add_role(
db,
&Role::find_by_id(db, role_id.parse::<i32>().unwrap())
.await
.unwrap(),
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role_id
)
.await;
.execute(db)
.await
.unwrap();
}
}
pub async fn add_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
pub async fn remove_role(&self, db: &SqlitePool, role: &Role) {
sqlx::query!(
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
self.id,
role.id
)
.execute(db)
.await
.unwrap();
}
pub async fn login(db: &SqlitePool, name: &str, pw: &str) -> Result<Self, LoginError> {
let name = name.trim(); // just to make sure...
let Some(user) = User::find_by_name(db, name).await else {
if ![
"n-sageder",
"p-hofer",
"m-birner",
"s-sollberger",
"d-kortschak",
"wwwadmin",
"wadminw",
"admin",
"m sageder",
"d kortschak",
"a almousa",
"p hofer",
"s sollberger",
"n sageder",
"wp-system",
"s.sollberger",
"m.birner",
"m-sageder",
"a-almousa",
]
.contains(&name)
{
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
}
Log::create(db, format!("Username ({name}) not found (tried to login)")).await;
return Err(LoginError::InvalidAuthenticationCombo); // Username not found
};
@ -647,20 +440,23 @@ impl<'r> FromRequest<'r> for User {
Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().unwrap();
let Some(user) = User::find_by_id(db, user_id).await else {
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound));
};
if user.deleted {
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted));
}
user.logged_in(db).await;
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12));
req.cookies().add_private(cookie);
Outcome::Success(user)
}
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
Err(_) => {
println!("{:?}", user_id.value());
Outcome::Error((Status::Unauthorized, LoginError::DeserializationError))
}
},
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
}
@ -691,7 +487,7 @@ impl<'r> FromRequest<'r> for TechUser {
if user.has_role(db, "tech").await {
Outcome::Success(TechUser { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -734,7 +530,7 @@ impl<'r> FromRequest<'r> for CoxUser {
if user.has_role(db, "cox").await {
Outcome::Success(CoxUser { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -759,7 +555,7 @@ impl<'r> FromRequest<'r> for AdminUser {
if user.has_role(db, "admin").await {
Outcome::Success(AdminUser { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -769,20 +565,22 @@ impl<'r> FromRequest<'r> for AdminUser {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AllowedForPlannedTripsUser(pub(crate) User);
pub struct NonGuestUser {
pub(crate) user: User,
}
#[async_trait]
impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
impl<'r> FromRequest<'r> for NonGuestUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
Outcome::Success(AllowedForPlannedTripsUser(user))
if !user.has_role(db, "scheckbuch").await {
Outcome::Success(NonGuestUser { user })
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -791,125 +589,6 @@ impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
}
}
impl Into<User> for AllowedForPlannedTripsUser {
fn into(self) -> User {
self.0
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DonauLinzUser(pub(crate) User);
impl Into<User> for DonauLinzUser {
fn into(self) -> User {
self.0
}
}
impl Deref for DonauLinzUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for DonauLinzUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "Donau Linz").await
&& !user.has_role(db, "Unterstützend").await
&& !user.has_role(db, "Förderndes Mitglied").await
{
Outcome::Success(DonauLinzUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VorstandUser(pub(crate) User);
impl Into<User> for VorstandUser {
fn into(self) -> User {
self.0
}
}
impl Deref for VorstandUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for VorstandUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "Vorstand").await {
Outcome::Success(VorstandUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PlannedEventUser(pub(crate) User);
impl Into<User> for PlannedEventUser {
fn into(self) -> User {
self.0
}
}
impl Deref for PlannedEventUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<'r> FromRequest<'r> for PlannedEventUser {
type Error = LoginError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let db = req.rocket().state::<SqlitePool>().unwrap();
match User::from_request(req).await {
Outcome::Success(user) => {
if user.has_role(db, "planned_event").await {
Outcome::Success(PlannedEventUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
@ -995,7 +674,6 @@ mod test {
notes: None,
phone: None,
address: None,
family_id: None,
},
)
.await;

View File

@ -1,4 +1,4 @@
use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
use rocket::{form::Form, fs::FileServer, post, routes, Build, FromForm, Rocket, State};
use serde_json::json;
use sqlx::SqlitePool;
@ -27,7 +27,7 @@ async fn login(login: Form<LoginForm<'_>>, db: &State<SqlitePool>) -> String {
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
//.mount("/", FileServer::from("svelte/build").rank(0))
.mount("/", FileServer::from("svelte/build").rank(0))
.mount("/api/login", routes![login])
}

View File

@ -33,12 +33,6 @@ async fn index(
Template::render("admin/mail", context.into_json())
}
#[get("/mail/fee")]
async fn fee(db: &State<SqlitePool>, _admin: AdminUser, config: &State<Config>) -> &'static str {
Mail::fees(db, config.smtp_pw.clone()).await;
"SUCC"
}
#[derive(FromForm, Debug)]
pub struct MailToSend<'a> {
pub(crate) role_id: i32,
@ -56,14 +50,14 @@ async fn update(
) -> Flash<Redirect> {
let d = data.into_inner();
if Mail::send(db, d, config.smtp_pw.clone()).await {
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
return Flash::success(Redirect::to("/admin/mail"), "Mail versendet");
} else {
Flash::error(Redirect::to("/admin/mail"), "Fehler")
return Flash::error(Redirect::to("/admin/mail"), "Fehler");
}
}
pub fn routes() -> Vec<Route> {
routes![index, update, fee]
routes![index, update]
}
#[cfg(test)]

View File

@ -10,7 +10,7 @@ use sqlx::SqlitePool;
use crate::model::{
planned_event::PlannedEvent,
tripdetails::{TripDetails, TripDetailsToAdd},
user::PlannedEventUser,
user::AdminUser,
};
//TODO: add constraints (e.g. planned_amount_cox > 0)
@ -25,7 +25,7 @@ struct AddPlannedEventForm<'r> {
async fn create(
db: &State<SqlitePool>,
data: Form<AddPlannedEventForm<'_>>,
_admin: PlannedEventUser,
_admin: AdminUser,
) -> Flash<Redirect> {
let data = data.into_inner();
@ -36,14 +36,13 @@ async fn create(
PlannedEvent::create(db, data.name, data.planned_amount_cox, trip_details).await;
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
Flash::success(Redirect::to("/"), "Event hinzugefügt")
}
//TODO: add constraints (e.g. planned_amount_cox > 0)
#[derive(FromForm)]
struct UpdatePlannedEventForm<'r> {
id: i64,
name: &'r str,
planned_amount_cox: i32,
max_people: i32,
notes: Option<&'r str>,
@ -55,14 +54,13 @@ struct UpdatePlannedEventForm<'r> {
async fn update(
db: &State<SqlitePool>,
data: Form<UpdatePlannedEventForm<'_>>,
_admin: PlannedEventUser,
_admin: AdminUser,
) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, data.id).await {
Some(planned_event) => {
planned_event
.update(
db,
data.name,
data.planned_amount_cox,
data.max_people,
data.notes,
@ -70,20 +68,20 @@ async fn update(
data.is_locked,
)
.await;
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
Flash::success(Redirect::to("/"), "Successfully edited the event")
}
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
None => Flash::error(Redirect::to("/"), "Planned event id not found"),
}
}
#[get("/planned-event/<id>/delete")]
async fn delete(db: &State<SqlitePool>, id: i64, _admin: PlannedEventUser) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, id: i64, _admin: AdminUser) -> Flash<Redirect> {
match PlannedEvent::find_by_id(db, id).await {
Some(planned_event) => {
planned_event.delete(db).await;
Flash::success(Redirect::to("/planned"), "Event gelöscht")
Flash::success(Redirect::to("/"), "Event gelöscht")
}
None => Flash::error(Redirect::to("/planned"), "PlannedEvent does not exist"),
None => Flash::error(Redirect::to("/"), "PlannedEvent does not exist"),
}
}
@ -122,7 +120,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -153,7 +151,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -185,11 +183,11 @@ mod test {
let req = client
.put("/admin/planned-event")
.header(ContentType::Form) // Set the content type to form
.body("id=1&planned_amount_cox=2&max_people=3&notes=new-planned-event-text&name=test"); // Add the form data to the request body;
.body("id=1&planned_amount_cox=2&max_people=3&notes=new-planned-event-text"); // Add the form data to the request body;
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -198,7 +196,7 @@ mod test {
assert_eq!(
flash_cookie.value(),
"7:successEvent erfolgreich bearbeitet"
"7:successSuccessfully edited the event"
);
let event = PlannedEvent::find_by_id(&db, 1).await.unwrap();
@ -222,13 +220,11 @@ mod test {
let req = client
.put("/admin/planned-event")
.header(ContentType::Form) // Set the content type to form
.body(
"id=1337&planned_amount_cox=2&max_people=3&notes=new-planned-event-text&name=test",
); // Add the form data to the request body;
.body("id=1337&planned_amount_cox=2&max_people=3&notes=new-planned-event-text"); // Add the form data to the request body;
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -259,7 +255,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()

View File

@ -1,9 +1,8 @@
use std::collections::HashMap;
use crate::model::{
family::Family,
role::Role,
user::{AdminUser, User, UserWithRoles, VorstandUser},
user::{AdminUser, User, UserWithRoles},
};
use futures::future::join_all;
use rocket::{
@ -19,7 +18,7 @@ use sqlx::SqlitePool;
#[get("/user")]
async fn index(
db: &State<SqlitePool>,
user: VorstandUser,
admin: AdminUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user_futures: Vec<_> = User::all(db)
@ -28,81 +27,22 @@ async fn index(
.map(|u| async move { UserWithRoles::from_user(u, db).await })
.collect();
let user: User = user.into();
let allowed_to_edit = user.has_role(db, "admin").await;
let users: Vec<UserWithRoles> = join_all(user_futures).await;
let roles = Role::all(db).await;
let families = Family::all_with_members(db).await;
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("allowed_to_edit", &allowed_to_edit);
context.insert("users", &users);
context.insert("roles", &roles);
context.insert("families", &families);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
Template::render("admin/user/index", context.into_json())
}
#[get("/user/fees")]
async fn fees(
db: &State<SqlitePool>,
admin: VorstandUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let mut context = Context::new();
let users = User::all_payer_groups(db).await;
let mut fees = Vec::new();
for user in users {
if let Some(fee) = user.fee(db).await {
fees.push(fee);
}
}
context.insert("fees", &fees);
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert(
"loggedin_user",
&UserWithRoles::from_user(admin.into(), db).await,
&UserWithRoles::from_user(admin.user, db).await,
);
Template::render("admin/user/fees", context.into_json())
}
#[get("/user/fees/paid?<user_ids>")]
async fn fees_paid(
db: &State<SqlitePool>,
_admin: AdminUser,
user_ids: Vec<i32>,
) -> Flash<Redirect> {
let mut res = String::new();
for user_id in user_ids {
let user = User::find_by_id(db, user_id).await.unwrap();
res.push_str(&format!("{} + ", user.name));
if user.has_role(db, "paid").await {
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
} else {
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
.await;
}
}
res.truncate(res.len() - 3); // remove ' + ' from the end
Flash::success(
Redirect::to("/admin/user/fees"),
format!("Zahlungsstatus von {} erfolgreich geändert", res),
)
Template::render("admin/user/index", context.into_json())
}
#[get("/user/<user>/reset-pw")]
@ -149,7 +89,6 @@ pub struct UserEditForm {
pub(crate) notes: Option<String>,
pub(crate) phone: Option<String>,
pub(crate) address: Option<String>,
pub(crate) family_id: Option<i64>,
}
#[post("/user", data = "<data>")]
@ -193,5 +132,5 @@ async fn create(
}
pub fn routes() -> Vec<Route> {
routes![index, resetpw, update, create, delete, fees, fees_paid]
routes![index, resetpw, update, create, delete]
}

View File

@ -89,15 +89,7 @@ async fn login(
)
.await;
// Check for redirect_url cookie and redirect accordingly
match cookies.get_private("redirect_url") {
Some(redirect_cookie) => {
let redirect_url = redirect_cookie.value().to_string();
cookies.remove_private(redirect_cookie); // Remove the cookie after using it
Flash::success(Redirect::to(redirect_url), "Login erfolgreich")
}
None => Flash::success(Redirect::to("/"), "Login erfolgreich"),
}
Flash::success(Redirect::to("/"), "Login erfolgreich")
}
#[get("/set-pw/<userid>")]

View File

@ -13,7 +13,7 @@ use crate::{
model::{
boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles},
user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles},
},
tera::log::KioskCookie,
};
@ -45,7 +45,7 @@ async fn index_kiosk(
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
user: NonGuestUser,
) -> Template {
let boatdamages = BoatDamage::all(db).await;
let boats = Boat::all(db).await;
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithRoles::from_user(user.into(), db).await,
&UserWithRoles::from_user(user.user, db).await,
);
Template::render("boatdamages", context.into_json())
@ -76,14 +76,13 @@ pub struct FormBoatDamageToAdd<'r> {
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageToAdd<'r>>,
user: DonauLinzUser,
user: NonGuestUser,
) -> Flash<Redirect> {
let user: User = user.into();
let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id,
desc: data.desc,
lock_boat: data.lock_boat,
user_id_created: user.id as i32,
user_id_created: user.user.id as i32,
};
match BoatDamage::create(db, boatdamage_to_add).await {
Ok(_) => Flash::success(

View File

@ -34,7 +34,7 @@ async fn create(
//)
//.await;
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich erstellt.")
}
#[derive(FromForm)]
@ -66,19 +66,16 @@ async fn update(
)
.await
{
Ok(_) => Flash::success(
Redirect::to("/planned"),
"Ausfahrt erfolgreich aktualisiert.",
),
Ok(_) => Flash::success(Redirect::to("/"), "Ausfahrt erfolgreich aktualisiert."),
Err(TripUpdateError::NotYourTrip) => {
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
}
Err(TripUpdateError::TripDetailsDoesNotExist) => {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
}
}
} else {
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
Flash::error(Redirect::to("/"), "Ausfahrt gibt's nicht")
}
}
@ -95,21 +92,21 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
),
)
.await;
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
Flash::success(Redirect::to("/"), "Danke für's helfen!")
}
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
Flash::error(Redirect::to("/"), "Du hilfst bereits aus!")
}
Err(CoxHelpError::AlreadyRegisteredAsRower) => Flash::error(
Redirect::to("/planned"),
Redirect::to("/"),
"Du hast dich bereits als Ruderer angemeldet!",
),
Err(CoxHelpError::DetailsLocked) => {
Flash::error(Redirect::to("/planned"), "Boot ist bereits eingeteilt.")
Flash::error(Redirect::to("/"), "Boot ist bereits eingeteilt.")
}
}
} else {
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
Flash::error(Redirect::to("/"), "Event gibt's nicht")
}
}
@ -117,18 +114,18 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Fl
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
let trip = Trip::find_by_id(db, trip_id).await;
match trip {
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
None => Flash::error(Redirect::to("/"), "Trip gibt's nicht!"),
Some(trip) => match trip.delete(db, &cox).await {
Ok(_) => {
Log::create(db, format!("Cox {} deleted trip.id={}", cox.name, trip_id)).await;
Flash::success(Redirect::to("/planned"), "Erfolgreich gelöscht!")
Flash::success(Redirect::to("/"), "Erfolgreich gelöscht!")
}
Err(TripDeleteError::SomebodyAlreadyRegistered) => Flash::error(
Redirect::to("/planned"),
Redirect::to("/"),
"Ausfahrt kann nicht gelöscht werden, da bereits jemand registriert ist!",
),
Err(TripDeleteError::NotYourTrip) => {
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
Flash::error(Redirect::to("/"), "Nicht deine Ausfahrt!")
}
},
}
@ -148,17 +145,17 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(TripHelpDeleteError::DetailsLocked) => {
Flash::error(Redirect::to("/planned"), "Boot bereits eingeteilt")
Flash::error(Redirect::to("/"), "Boot bereits eingeteilt")
}
Err(TripHelpDeleteError::CoxNotHelping) => {
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
Flash::error(Redirect::to("/"), "Steuermann hilft nicht aus...")
}
}
} else {
Flash::error(Redirect::to("/planned"), "Planned_event does not exist.")
Flash::error(Redirect::to("/"), "Planned_event does not exist.")
}
}
@ -205,7 +202,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -253,7 +250,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -291,7 +288,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -329,7 +326,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -357,7 +354,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -370,7 +367,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -394,14 +391,14 @@ mod test {
.body("name=cox&password=cox"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/1");
let req = client.get("/join/1");
let _ = req.dispatch().await;
let req = client.get("/cox/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -432,7 +429,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -473,7 +470,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -501,7 +498,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
@ -529,7 +526,7 @@ mod test {
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()

View File

@ -23,7 +23,7 @@ use crate::model::{
LogbookUpdateError,
},
logtype::LogType,
user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus},
user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus},
};
pub struct KioskCookie(String);
@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie {
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: DonauLinzUser,
user: NonGuestUser,
) -> Template {
let boats = Boat::for_user(db, &user).await;
let boats = Boat::for_user(db, &user.user).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
User::cox(db)
@ -78,7 +78,7 @@ async fn index(
context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithRoles::from_user(user.into(), db).await,
&UserWithRoles::from_user(user.user, db).await,
);
context.insert("on_water", &on_water);
context.insert("distances", &distances);
@ -87,12 +87,12 @@ async fn index(
}
#[get("/show", rank = 2)]
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
let logs = Logbook::completed(db).await;
Template::render(
"log.completed",
context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await),
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
)
}
@ -125,7 +125,7 @@ async fn new_kiosk(
async fn kiosk(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
_kiosk: KioskCookie,
kiosk: KioskCookie,
) -> Template {
let boats = Boat::all(db).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
@ -166,12 +166,12 @@ async fn kiosk(
async fn create_logbook(
db: &SqlitePool,
data: Form<LogToAdd>,
user: &DonauLinzUser,
user: &NonGuestUser,
) -> Flash<Redirect> {
match Logbook::create(
db,
data.into_inner(),
user
&user.user
)
.await
{
@ -188,7 +188,7 @@ async fn create_logbook(
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in den letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
}
}
@ -197,11 +197,14 @@ async fn create_logbook(
async fn create(
db: &State<SqlitePool>,
data: Form<LogToAdd>,
user: DonauLinzUser,
user: NonGuestUser,
) -> Flash<Redirect> {
Log::create(
db,
format!("User {} tries to create log entry={:?}", &user.name, data),
format!(
"User {} tries to create log entry={:?}",
user.user.name, data
),
)
.await;
@ -235,14 +238,14 @@ async fn create_kiosk(
)
.await;
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme
}
async fn home_logbook(
db: &SqlitePool,
data: Form<LogToFinalize>,
logbook_id: i32,
user: &DonauLinzUser,
user: &NonGuestUser,
) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else {
@ -252,7 +255,7 @@ async fn home_logbook(
);
};
match logbook.home(db,user, data.into_inner()).await {
match logbook.home(db, &user.user, data.into_inner()).await {
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
@ -282,11 +285,11 @@ async fn home_kiosk(
db,
data,
logbook_id,
&DonauLinzUser(
User::find_by_id(db, logbook.shipmaster as i32)
&NonGuestUser {
user: User::find_by_id(db, logbook.shipmaster as i32)
.await
.unwrap(),
), //TODO: fixme
.unwrap(), //TODO: fixme
},
)
.await
}
@ -296,13 +299,13 @@ async fn home(
db: &State<SqlitePool>,
data: Form<LogToFinalize>,
logbook_id: i32,
user: DonauLinzUser,
user: NonGuestUser,
) -> Flash<Redirect> {
Log::create(
db,
format!(
"User {} tries to finish log entry {logbook_id} {data:?}",
&user.name
user.user.name
),
)
.await;
@ -311,12 +314,12 @@ async fn home(
}
#[get("/<logbook_id>/delete", rank = 2)]
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: User) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await;
if let Some(logbook) = logbook {
Log::create(
db,
format!("User {} tries to delete log entry {logbook_id}", &user.name),
format!("User {} tries to delete log entry {logbook_id}", user.name),
)
.await;
match logbook.delete(db, &user).await {

View File

@ -3,21 +3,22 @@ use rocket::{
fairing::AdHoc,
form::Form,
fs::FileServer,
get,
http::Cookie,
post,
get, post,
request::FlashMessage,
response::{Flash, Redirect},
routes,
time::{Duration, OffsetDateTime},
Build, FromForm, Request, Rocket, State,
routes, Build, FromForm, Rocket, State,
};
use rocket_dyn_templates::Template;
use rocket_dyn_templates::{tera::Context, Template};
use serde::Deserialize;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::user::{User, UserWithRoles};
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
pub(crate) mod admin;
mod auth;
@ -26,7 +27,6 @@ mod cox;
mod ergo;
mod log;
mod misc;
mod planned;
mod stat;
#[derive(FromForm, Debug)]
@ -35,17 +35,6 @@ struct LoginForm<'r> {
password: &'r str,
}
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
Template::render("index", context.into_json())
}
#[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
match User::login(db, login.name, login.password).await {
@ -54,20 +43,162 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
}
}
#[catch(401)] //Unauthorized
fn unauthorized_error(req: &Request) -> Redirect {
// Save the URL the user tried to access, to be able to go there once logged in
let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
println!("{}", req.uri());
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
req.cookies().add_private(redirect_cookie);
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
Redirect::to("/auth")
if user.has_role(db, "cox").await || user.has_role(db, "admin").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("index", context.into_json())
}
#[catch(403)] //forbidden
fn forbidden_error() -> Flash<Redirect> {
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
user_note: Option<String>,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
match UserTrip::create(db, &user, &trip_details, user_note).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} registered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
name: String,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
#[catch(401)] //unauthorized
fn unauthorized_error() -> Redirect {
Redirect::to("/auth")
}
#[derive(Deserialize)]
@ -79,11 +210,10 @@ pub struct Config {
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
.mount("/", routes![index])
.mount("/", routes![index, join, remove, remove_guest])
.mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth])
.mount("/log", log::routes())
.mount("/planned", planned::routes())
.mount("/ergo", ergo::routes())
.mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes())
@ -91,7 +221,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/admin", admin::routes())
.mount("/", misc::routes())
.mount("/public", FileServer::from("static/"))
.register("/", catchers![unauthorized_error, forbidden_error])
.register("/", catchers![unauthorized_error])
.attach(Template::fairing())
.attach(AdHoc::config::<Config>())
}
@ -125,11 +255,7 @@ mod test {
assert_eq!(response.status(), Status::Ok);
assert!(response
.into_string()
.await
.unwrap()
.contains("Ruderassistent"));
assert!(response.into_string().await.unwrap().contains("Ausfahrten"));
}
#[sqlx::test]
@ -148,6 +274,75 @@ mod test {
assert_eq!(response.headers().get("Location").next(), Some("/auth"));
}
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
#[sqlx::test]
fn test_public() {
let db = testdb!();

View File

@ -1,272 +0,0 @@
use rocket::{
get,
request::FlashMessage,
response::{Flash, Redirect},
routes, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
#[get("/")]
async fn index(
db: &State<SqlitePool>,
user: AllowedForPlannedTripsUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user: User = user.into();
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "planned_event").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("fee", &user.fee(db).await);
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("planned", context.into_json())
}
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
user_note: Option<String>,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
match UserTrip::create(db, &user, &trip_details, user_note).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} registered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/planned"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/planned"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/planned"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/planned"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/planned"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
name: String,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/planned"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
pub fn routes() -> Vec<Route> {
routes![index, join, remove, remove_guest]
}
#[cfg(test)]
mod test {
use rocket::{
http::{ContentType, Status},
local::asynchronous::Client,
};
use sqlx::SqlitePool;
use crate::testdb;
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/planned/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/planned"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
}

View File

@ -4,19 +4,19 @@ use sqlx::SqlitePool;
use crate::model::{
stat::{self, Stat},
user::{DonauLinzUser, UserWithRoles},
user::{NonGuestUser, UserWithRoles},
};
use super::log::KioskCookie;
#[get("/boats?<year>", rank = 2)]
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
let stat = Stat::boats(db, year).await;
let kiosk = false;
Template::render(
"stat.boats",
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk),
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk),
)
}
@ -33,15 +33,15 @@ async fn index_boat_kiosk(
}
#[get("/?<year>", rank = 2)]
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await;
let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user).await;
let personal = stat::get_personal(db, &user.user).await;
let kiosk = false;
Template::render(
"stat.people",
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km),
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km),
)
}

1
svelte/.gitignore vendored
View File

@ -1,5 +1,6 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env

View File

@ -1,47 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
<h1 class="h1">Gebühren</h1>
<!-- START filterBar -->
<div class="search-wrapper">
<label for="name" class="sr-only">Suche</label>
<input type="search" name="name" id="filter-js" class="search-bar" placeholder="Suchen nach Name"/>
</div>
<!-- END filterBar -->
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
<div id="filter-result-js" class="text-primary-950 dark:text-white text-right"></div>
{% for fee in fees | sort(attribute="name") %}
<div {% if fee.paid %} style="background-color: green;" {% endif %} data-filterable="true" data-filter="{{ fee.name }} {% if fee.paid %} has-already-paid {% else %} has-not-paid {% endif %}" class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
<div class="grid sm:grid-cols-1 gap-3">
<div style="width: 100%" class="col-span-2">
<b>{{ fee.name }}</b>
</div>
<div style="width: 100%">
{{ fee.sum_in_cents / 100 }}€:
</div>
<div style="width: 100%">
{% for p in fee.parts %}
{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}
{% endfor %}
</div>
{% if "admin" in loggedin_user.roles %}
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@ -10,7 +10,6 @@
<h1 class="h1">Users</h1>
{% if allowed_to_edit %}
<form action="/admin/user/new" method="post" class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
<div class="w-full">
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
@ -25,7 +24,6 @@
<input value="Hinzufügen" type="submit" class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"/>
</div>
</form>
{% endif %}
<!-- START filterBar -->
<div class="search-wrapper">
@ -62,24 +60,20 @@
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{% for role in roles %}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles) }}
{% endfor%}
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
{% if allowed_to_edit %}
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
{% endif %}
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }}
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }}
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }}
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date) }}
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate) }}
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail) }}
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname) }}
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }}
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }}
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }}
</div>
</div>
{% if allowed_to_edit %}
<div class="mt-3 text-right">
<a href="/admin/user/{{ user.id }}/delete" class="w-28 btn btn-alert" onclick="return confirm('Wirklich löschen?');">
{% include "includes/delete-icon" %}
@ -87,7 +81,6 @@
</a>
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1"/>
</div>
{% endif %}
</form>
</div>
{% endfor %}

View File

@ -8,5 +8,9 @@
{% include "includes/funnel-icon" %}
Steuerleute gesucht
</button>
<button type="button" title="Toggle View" class="group btn btn-primary filter-trips-js" data-action="filter-months" id="filtermonth-js" aria-pressed="false" data-month="{{ now() | date(format='%m') }}">
{% include "includes/funnel-icon" %}
Aktuellen Monat anzeigen
</button>
</div>
{% endif %}

View File

@ -1,8 +1,12 @@
{# Shows a fancy, optional lists of boats. They are grouped by boat category.
Inputs: boats
Parameters: only_ones: if set, only 1x boats are shown
#}
{% macro show_boats() %}
{% macro show_boats(only_ones) %}
{% if only_ones %}
{% set_global boats = boats | filter(attribute="amount_seats", value=1) %}
{% endif %}
{% for amount_seats, grouped_boats in boats | group_by(attribute="amount_seats") %}
<div class="pb-2">
<div class="bg-gray-100 dark:bg-primary-600 text-primary-950 dark:text-white text-center text-sm mb-2">
@ -23,16 +27,20 @@
{% endmacro show_boats %}
{# Shows the form for creating a new logbook entry. #}
{% macro new(shipmaster) %}
{% macro new(only_ones, shipmaster) %}
<form action="/log" method="post" id="form" class="grid grid-cols-4 gap-3" onsubmit="Array.from(this.elements).forEach(e=>!e.value.trim()&&(e.disabled=true));">
{{ log::boat_select() }}
{{ log::boat_select(only_ones=only_ones) }}
{% if not only_ones %}
<div class="col-span-4 md:col-span-1">
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
<div class="h-10 flex items-center">
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
</div>
</div>
{% endif %}
{% if not only_ones %}
{{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }}
{% endif %}
{{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-newrowerjs", wrapper_class="col-span-2") }}
{{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-newrowerjs", wrapper_class="col-span-2") }}
{{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, wrapper_class='col-span-2') }}
@ -56,8 +64,13 @@
{% endmacro new %}
{% macro boat_select(id="boat_id") %}
{% macro boat_select(only_ones, id="boat_id") %}
{% if not only_ones %}
{{ macros::select(label="Boot", data=boats, name="boat_id", id=id, display=["name", " (","amount_seats", " x)"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true) }}
{% else %}
{% set ones = boats | filter(attribute="amount_seats", value=1) %}
{{ macros::select(label="Boot", data=ones, name="boat_id", id=id, display=["name", " (","amount_seats", " x)"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true) }}
{% endif %}
{% endmacro boat_select %}
{% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %}
@ -72,7 +85,7 @@
{% set_global sel = true %}
{% endif %}
{% endfor %}
<option value="{{ user.id }}" {% if sel %} selected {% endif %} {% if user.on_water %} disabled="disabled" {% endif %} data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat}}}'>
<option value="{{ user.id }}" {% if sel %} selected {% endif %} {% if user.on_water %} disabled="disabled" {% endif %} data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat}}}'>
{{user.name}}
{% if user.on_water %}
(am Wasser)
@ -84,7 +97,7 @@
{#{% endif %}#}
{% endmacro rower_select %}
{% macro show(log, state, allowed_to_close=false) %}
{% macro show(log, state, allowed_to_close=false, only_ones) %}
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
<div class="pt-2 px-3 {% if not loop.first %} border-t {% endif %}">
<div class="w-full">
@ -110,7 +123,7 @@
<div class="hidden">
{% if allowed_to_close and state == "on_water" %}
<div id="close{{ log.id }}">
{{ log::home(log=log) }}
{{ log::home(log=log, only_ones=only_ones) }}
</div>
<div>
LÖSCHEN
@ -142,7 +155,7 @@
</div>
{% endmacro show %}
{% macro show_old(log, state, allowed_to_close=false, index) %}
{% macro show_old(log, state, allowed_to_close=false, only_ones, index) %}
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative" data-filterable="true" data-filter="{{ log.boat.name }} {% for rower in log.rowers %} {{ rower.name }} {% endfor %}">
{% if log.logtype %}
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
@ -170,7 +183,7 @@
{% set amount_rowers = log.rowers | length %}
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
{% if allowed_to_close and state == "on_water" %}
{{ log::home(log=log) }}
{{ log::home(log=log, only_ones=only_ones) }}
{% else %}
<div class="text-black dark:text-white">
{{ log.destination }}
@ -198,7 +211,7 @@
</div>
{% endmacro show_old %}
{% macro home(log) %}
{% macro home(log, only_ones) %}
<form class="grid grid-cols-1 gap-3" action="/log/{{log.id}}" method="post">
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}

View File

@ -4,11 +4,7 @@
>
<div class="max-w-screen-xl w-full flex justify-between items-center">
<div class="w-1/3 truncate">
{% if "Donau Linz" in loggedin_user.roles %}
<a href="/planned">
{% else %}
<a href="/">
{% endif %}
<a href="/">
{{ loggedin_user.name }}
</a>
@ -52,7 +48,7 @@
class="inline-flex justify-center rounded-md bg-primary-600 mx-1 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"
data-sidebar="true"
data-trigger="sidebar"
data-header="Menü"
data-header="Logbuch"
data-body="#mobile-menu"
>
{% include "includes/book" %}
@ -154,10 +150,10 @@
<div class="h-8"></div>
{% endmacro header %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='', readonly=false) %}
{% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false, id='', autofocus=false, wrapper_class='', pattern='') %}
<div class="{{wrapper_class}}">
<label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} text-sm text-gray-600 dark:text-white {% endif %}">{{ label }}</label>
<input {% if type=='datetime-local' %} onclick='if (!this.value) setCurrentdate(this)' {% endif %}{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}{% if readonly %}readonly{% endif %}>
<input {% if type=='datetime-local' %} onclick='if (!this.value) setCurrentdate(this)' {% endif %}{% if id %} id="{{ id }}" {% else %} id="{{ name }}" {% endif %} name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min is defined %} min="{{ min }}" {% endif %} {% if autofocus %} autofocus {% endif %}{% if pattern %}pattern="{{ pattern }}"{% endif %}>
</div>
{% endmacro input %}
@ -168,7 +164,7 @@
</label>
{% endmacro checkbox %}
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false, new_last_entry='') %}
{% macro select(label, data, name='trip_type', default='', id='', selected_id='', display='', extras='', class='', wrapper_class='', required=false, show_seats=false) %}
<div class="{{wrapper_class}}">
<label for="{{ name }}" class="text-sm text-gray-600 dark:text-gray-100">{{ label }}</label>
{% if display == '' %}
@ -179,7 +175,7 @@
<option selected value>{{ default }}</option>
{% endif %}
{% for d in data %}
<option value="{{ d.id }}" {% if d.id == selected_id %} selected {% endif %} {% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data-{{extra}}={{d[extra]}} {% else %} {% if d[extra] %} disabled {% endif %} {% endif %} {% endfor %} {% endif %} {% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}", "boat_in_ottensheim": {{ d["location_id"] == 2 }}}'{% endif %}>
<option value="{{ d.id }}" {% if d.id == selected_id %} selected {% endif %} {% if extras != '' %} {% for extra in extras %} {% if extra != 'on_water' and d[extra] %} data-{{extra}}={{d[extra]}} {% else %} {% if d[extra] %} disabled {% endif %} {% endif %} {% endfor %} {% endif %} {% if show_seats %} data-custom-properties='{"amount_seats": {{ d["amount_seats"] }}, "owner": "{{ d["owner"] }}", "default_destination": "{{ d["default_destination"] }}"}'{% endif %}>
{% for displa in display -%}
{%- if d[displa] -%}
{{- d[displa] -}}
@ -189,13 +185,11 @@
{%- endfor %}
</option>
{% endfor %}
{% if new_last_entry %}
<option value="-1">{{ new_last_entry }}</option>
{% endif %}
</select>
</div>
{% endmacro select %}
{% macro alert(message, type, class='') %}
<div class="{{ class }} alert-{{ type }} text-white px-3 py-1 rounded-md text-center">
{{ message }}
@ -215,7 +209,7 @@
{% if rower.is_real_guest %}
<small class="text-gray-600 dark:text-gray-100">(Gast)</small>
{% if allow_removing %}
<a href="/planned/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a>
<a href="/remove/{{ trip_details_id }}/{{ rower.name }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% endif %}
<span class="hidden">(angemeldet seit

View File

@ -1,4 +0,0 @@
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>
<script>var QRCodep;!function(){function a(a){this.mode=j.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=0,c=this.data.length;c>b;b++){var d=[],e=this.data.charCodeAt(b);e>65536?(d[0]=240|(1835008&e)>>>18,d[1]=128|(258048&e)>>>12,d[2]=128|(4032&e)>>>6,d[3]=128|63&e):e>2048?(d[0]=224|(61440&e)>>>12,d[1]=128|(4032&e)>>>6,d[2]=128|63&e):e>128?(d[0]=192|(1984&e)>>>6,d[1]=128|63&e):d[0]=e,this.parsedData.push(d)}this.parsedData=Array.prototype.concat.apply([],this.parsedData),this.parsedData.length!=this.data.length}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function c(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function d(a,b){this.totalCount=a,this.dataCount=b}function e(){this.buffer=[],this.length=0}function f(){return"undefined"!=typeof CanvasRenderingContext2D}function g(){var a=!1,b=navigator.userAgent;if(/android/i.test(b)){a=!0;var c=b.toString().match(/android ([0-9]\.[0-9])/i);c&&c[1]&&(a=parseFloat(c[1]))}return a}function h(a,b){for(var c=1,d=i(a),e=0,f=p.length;f>=e;e++){var g=0;switch(b){case k.L:g=p[e][0];break;case k.M:g=p[e][1];break;case k.Q:g=p[e][2];break;case k.H:g=p[e][3]}if(g>=d)break;c++}if(c>p.length)throw new Error("Too long data");return c}function i(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(a){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?this.modules[a+c][b+d]=!0:this.modules[a+c][b+d]=!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=m.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=a%2==0);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=b%2==0)},setupPositionAdjustPattern:function(){for(var a=m.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var f=-2;2>=f;f++)for(var g=-2;2>=g;g++)-2==f||2==f||-2==g||2==g||0==f&&0==g?this.modules[d+f][e+g]=!0:this.modules[d+f][e+g]=!1}},setupTypeNumber:function(a){for(var b=m.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(b>>c&1);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=m.getBCHTypeInfo(c),e=0;15>e;e++){var f=!a&&1==(d>>e&1);6>e?this.modules[e][8]=f:8>e?this.modules[e+1][8]=f:this.modules[this.moduleCount-15+e][8]=f}for(var e=0;15>e;e++){var f=!a&&1==(d>>e&1);8>e?this.modules[8][this.moduleCount-e-1]=f:9>e?this.modules[8][15-e-1+1]=f:this.modules[8][15-e-1]=f}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,f=0,g=this.moduleCount-1;g>0;g-=2)for(6==g&&g--;;){for(var h=0;2>h;h++)if(null==this.modules[d][g-h]){var i=!1;f<a.length&&(i=1==(a[f]>>>e&1));var j=m.getMask(b,d,g-h);j&&(i=!i),this.modules[d][g-h]=i,e--,-1==e&&(f++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,f){for(var g=d.getRSBlocks(a,c),h=new e,i=0;i<f.length;i++){var j=f[i];h.put(j.mode,4),h.put(j.getLength(),m.getLengthInBits(j.mode,a)),j.write(h)}for(var k=0,i=0;i<g.length;i++)k+=g[i].dataCount;if(h.getLengthInBits()>8*k)throw new Error("code length overflow. ("+h.getLengthInBits()+">"+8*k+")");for(h.getLengthInBits()+4<=8*k&&h.put(0,4);h.getLengthInBits()%8!=0;)h.putBit(!1);for(;;){if(h.getLengthInBits()>=8*k)break;if(h.put(b.PAD0,8),h.getLengthInBits()>=8*k)break;h.put(b.PAD1,8)}return b.createBytes(h,g)},b.createBytes=function(a,b){for(var d=0,e=0,f=0,g=new Array(b.length),h=new Array(b.length),i=0;i<b.length;i++){var j=b[i].dataCount,k=b[i].totalCount-j;e=Math.max(e,j),f=Math.max(f,k),g[i]=new Array(j);for(var l=0;l<g[i].length;l++)g[i][l]=255&a.buffer[l+d];d+=j;var n=m.getErrorCorrectPolynomial(k),o=new c(g[i],n.getLength()-1),p=o.mod(n);h[i]=new Array(n.getLength()-1);for(var l=0;l<h[i].length;l++){var q=l+p.getLength()-h[i].length;h[i][l]=q>=0?p.get(q):0}}for(var r=0,l=0;l<b.length;l++)r+=b[l].totalCount;for(var s=new Array(r),t=0,l=0;e>l;l++)for(var i=0;i<b.length;i++)l<g[i].length&&(s[t++]=g[i][l]);for(var l=0;f>l;l++)for(var i=0;i<b.length;i++)l<h[i].length&&(s[t++]=h[i][l]);return s};for(var j={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},k={L:1,M:0,Q:3,H:2},l={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},m={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;m.getBCHDigit(b)-m.getBCHDigit(m.G15)>=0;)b^=m.G15<<m.getBCHDigit(b)-m.getBCHDigit(m.G15);return(a<<10|b)^m.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;m.getBCHDigit(b)-m.getBCHDigit(m.G18)>=0;)b^=m.G18<<m.getBCHDigit(b)-m.getBCHDigit(m.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return m.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case l.PATTERN000:return(b+c)%2==0;case l.PATTERN001:return b%2==0;case l.PATTERN010:return c%3==0;case l.PATTERN011:return(b+c)%3==0;case l.PATTERN100:return(Math.floor(b/2)+Math.floor(c/3))%2==0;case l.PATTERN101:return b*c%2+b*c%3==0;case l.PATTERN110:return(b*c%2+b*c%3)%2==0;case l.PATTERN111:return(b*c%3+(b+c)%2)%2==0;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new c([1],0),d=0;a>d;d++)b=b.multiply(new c([1,n.gexp(d)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case j.MODE_NUMBER:return 10;case j.MODE_ALPHA_NUM:return 9;case j.MODE_8BIT_BYTE:return 8;case j.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case j.MODE_NUMBER:return 12;case j.MODE_ALPHA_NUM:return 11;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case j.MODE_NUMBER:return 14;case j.MODE_ALPHA_NUM:return 13;case j.MODE_8BIT_BYTE:return 16;case j.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||0==h&&0==i||g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,0!=j&&4!=j||(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},n={glog:function(a){if(1>a)throw new Error("glog("+a+")");return n.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return n.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},o=0;8>o;o++)n.EXP_TABLE[o]=1<<o;for(var o=8;256>o;o++)n.EXP_TABLE[o]=n.EXP_TABLE[o-4]^n.EXP_TABLE[o-5]^n.EXP_TABLE[o-6]^n.EXP_TABLE[o-8];for(var o=0;255>o;o++)n.LOG_TABLE[n.EXP_TABLE[o]]=o;c.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),d=0;d<this.getLength();d++)for(var e=0;e<a.getLength();e++)b[d+e]^=n.gexp(n.glog(this.get(d))+n.glog(a.get(e)));return new c(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=n.glog(this.get(0))-n.glog(a.get(0)),d=new Array(this.getLength()),e=0;e<this.getLength();e++)d[e]=this.get(e);for(var e=0;e<a.getLength();e++)d[e]^=n.gexp(n.glog(a.get(e))+b);return new c(d,0).mod(a)}},d.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],d.getRSBlocks=function(a,b){var c=d.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var e=c.length/3,f=[],g=0;e>g;g++)for(var h=c[3*g+0],i=c[3*g+1],j=c[3*g+2],k=0;h>k;k++)f.push(new d(i,j));return f},d.getRsBlockTable=function(a,b){switch(b){case k.L:return d.RS_BLOCK_TABLE[4*(a-1)+0];case k.M:return d.RS_BLOCK_TABLE[4*(a-1)+1];case k.Q:return d.RS_BLOCK_TABLE[4*(a-1)+2];case k.H:return d.RS_BLOCK_TABLE[4*(a-1)+3];default:return}},e.prototype={get:function(a){var b=Math.floor(a/8);return 1==(this.buffer[b]>>>7-a%8&1)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(a>>>b-c-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var p=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],q=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function b(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var c=this._htOption,d=this._el,e=a.getModuleCount();Math.floor(c.width/e),Math.floor(c.height/e);this.clear();var f=b("svg",{viewBox:"0 0 "+String(e)+" "+String(e),width:"100%",height:"100%",fill:c.colorLight});f.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),d.appendChild(f),f.appendChild(b("rect",{fill:c.colorLight,width:"100%",height:"100%"})),f.appendChild(b("rect",{fill:c.colorDark,width:"1",height:"1",id:"template"}));for(var g=0;e>g;g++)for(var h=0;e>h;h++)if(a.isDark(g,h)){var i=b("use",{x:String(h),y:String(g)});i.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),f.appendChild(i)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),r="svg"===document.documentElement.tagName.toLowerCase(),s=r?q:f()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function b(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&c._fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,void(d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var c=1/window.devicePixelRatio,d=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,b,e,f,g,h,i,j,k){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*c;else"undefined"==typeof j&&(arguments[1]*=c,arguments[2]*=c,arguments[3]*=c,arguments[4]*=c);d.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=g(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.alt="Scan me!",this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&b.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCodep=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:k.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._htOption.useSVG&&(s=q),this._android=g(),this._el=a,this._oQRCode=null,this._htOption.text&&this.makeCode(this._htOption.text)},QRCodep.prototype.makeCode=function(a){this._oQRCode=new b(h(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,-1!=this._htOption.dpi&&-1!=this._htOption.mmPerDot&&(this._htOption.width=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot,this._htOption.height=this._oQRCode.moduleCount*this._htOption.dpi/25.4*this._htOption.mmPerDot),this._oDrawing=new s(this._el,this._htOption),this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCodep.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCodep.prototype.clear=function(){this._oDrawing.clear()},QRCodep.CorrectLevel=k}();var sepaQR;!function(){"use strict";var a={UTF_8:1,ISO8859_1:2,ISO8859_2:3,ISO8859_4:4,ISO8859_5:5,ISO8859_7:6,ISO8859_10:7,ISO8859_15:8};sepaQR=function(b){if(this._sOpt={serviceTag:"BCD",version:"001",charset:a.UTF_8,identificationCode:"SCT",benefBIC:"",benefName:"",benefAccNr:"",amountEuro:"",purpose:"",creditorRef:"",remittanceInf:"",information:""},b)for(var c in b)this._sOpt[c]=b[c]},sepaQR.prototype.validServiceTag=function(){return"BCD"===this._sOpt.serviceTag},sepaQR.prototype.validVersion=function(){return"001"===this._sOpt.version||"002"===this._sOpt.version},sepaQR.prototype.validCharset=function(){return this._sOpt.charset>0&&this._sOpt.charset<=8},sepaQR.prototype.validIdentificationCode=function(){return"SCT"===this._sOpt.identificationCode},sepaQR.prototype.validBenefName=function(){var a="string"==typeof this._sOpt.benefName&&this._sOpt.benefName.length>=1&&this._sOpt.benefName.length<=70;if(!a)throw new Error("benefName not valid!");return a},sepaQR.prototype.validBenefAccNr=function(){var a="string"==typeof this._sOpt.benefAccNr&&this._sOpt.benefAccNr.length>=1&&this._sOpt.benefAccNr.length<=34;if(!a)throw new Error("benefAccNr not valid!");return a},sepaQR.prototype.validAmountEuro=function(){if("string"==typeof this._sOpt.amountEuro)return 0===this._sOpt.amountEuro.length;if("number"==typeof this._sOpt.amountEuro){this._sOpt.amountEuro=Math.round(100*this._sOpt.amountEuro)/100;var a=this._sOpt.amountEuro>.01&&this._sOpt.amountEuro<=999999999.99;if(!a)throw new Error("Amount not valid!");return a}},sepaQR.prototype.validBenefBic=function(){var a="002"==this._sOpt.version||"string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11;if(!a)throw new Error("BIC is mandatory in Version 001!");if(a="string"==typeof this._sOpt.benefBIC&&this._sOpt.benefBIC.length>=0&&this._sOpt.benefBIC.length<=11,!a)throw new Error("benefBIC not valid!");return a},sepaQR.prototype.validPurpose=function(){var a="string"==typeof this._sOpt.purpose&&this._sOpt.purpose.length>=0&&this._sOpt.purpose.length<=4;if(!a)throw new Error("Purpose not valid!");return a},sepaQR.prototype.validInformation=function(){var a="string"==typeof this._sOpt.information&&this._sOpt.information.length>=0&&this._sOpt.information.length<=70;if(!a)throw new Error("Information not valid!");return a},sepaQR.prototype.validCreditorRefOrRemittance=function(){var a="string"==typeof this._sOpt.creditorRef&&0===this._sOpt.creditorRef.length,b="string"==typeof this._sOpt.remittanceInf&&0===this._sOpt.remittanceInf.length,c=a&&"string"==typeof this._sOpt.remittanceInf&&this._sOpt.remittanceInf.length<=140||b&&"string"==typeof this._sOpt.creditorRef&&this._sOpt.creditorRef.length<=35;if(!c)throw new Error("creditorRef or Remittance not valid!");return c},sepaQR.prototype.validQRTextLength=function(){for(var a=this.prepareQRText(),b=0,c=0,d=a.length;d>c;c++){var e=a.charCodeAt(c);b+=e>65536?4:e>2048?3:e>128?2:1}return 328>=b},sepaQR.prototype.valid=function(){var a=this.validServiceTag()&&this.validVersion()&&this.validCharset()&&this.validIdentificationCode()&&this.validBenefName()&&this.validBenefAccNr()&&this.validAmountEuro()&&this.validBenefBic()&&this.validPurpose()&&this.validInformation()&&this.validPurpose()&&this.validCreditorRefOrRemittance()&&this.validQRTextLength();return a},sepaQR.prototype.prepareQRText=function(){return(this._sOpt.serviceTag+"\n"+this._sOpt.version+"\n"+this._sOpt.charset+"\n"+this._sOpt.identificationCode+"\n"+this._sOpt.benefBIC+"\n"+this._sOpt.benefName+"\n"+this._sOpt.benefAccNr+"\nEUR"+this._sOpt.amountEuro+"\n"+this._sOpt.purpose+"\n"+this._sOpt.creditorRef+"\n"+this._sOpt.remittanceInf+"\n"+this._sOpt.information).trim()},sepaQR.prototype.toQRText=function(){return this.valid()?this.prepareQRText():""},sepaQR.prototype.makeCodeInto=function(a,b){var c={width:256,height:256,mmPerDot:.85,dpi:92,correctLevel:QRCodep.CorrectLevel.M,text:this.toQRText()};if(0!==c.text.length){if(b)for(var d in b)c[d]=b[d];return this.qrcode=new QRCodep(a,c),this.drawExplanatoryLink(document.getElementById(a).getElementsByTagName("canvas")[0],document.createElement("canvas")),this.qrcode}},sepaQR.prototype.drawExplanatoryLink=function(a,b){var c=3,d=12,e=8,f=6,g=a;b.width=g.width,b.height=g.height,b.getContext("2d").drawImage(g,0,0),g.width=b.width+2*(e+c+f),g.height=b.height+2*(e+c+f),CanvasRenderingContext2D.prototype.roundRect=function(a,b,c,d,e,f){return 2*e>c&&(e=c/2),2*e>d&&(e=d/2),this.beginPath(),this.moveTo(a+e,b),this.arcTo(a+c,b,a+c,b+d,e),this.arcTo(a+c,b+d,a,b+d,e),this.lineTo(a+c-f,b+d),this.moveTo(a+c-110,b+d),this.arcTo(a,b+d,a,b,e),this.arcTo(a,b,a+c,b,e),this};var h=g.getContext("2d");h.fillStyle="white",h.rect(0,0,g.width,g.height),h.fill(),h.drawImage(b,e+c+f,e+c+f),h.lineWidth=c,h.roundRect(f+c/2,f+c/2,g.width-c-2*f,g.height-c-2*f,d,3*e).stroke(),h.fillStyle="black",h.font=4.5*c+"px Arial",h.fillText("sepaQR.eu",g.width-110,g.height-f/2)},sepaQR.Charset=a}();</script>

View File

@ -3,101 +3,292 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-lg w-full">
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
<h1 class="h1">Ruderassistent</h1>
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}">
<div>
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div
class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in planned_event.rower%}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if planned_event.max_people > planned_event.rower | length %}
{% if cur_user_participates == false %}
<a href="/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in planned_event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "admin" in loggedin_user.roles %}
<form action="/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Allgemein</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="https://rudernlinz.at/termin" target="_blank" class="link-primary">FAQ (extern)</a></li>
</ul>
</div>
</div>
</div>
{% if planned_event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% endif %}
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Ergo</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/ergo" class="link-primary">Ergo</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Aktives Vereinsmitglied</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
<li class="py-1"><a href="/log" class="link-primary">Ausfahrt eintragen</a></li>
<li class="py-1"><a href="/log/show" class="link-primary">Logbuch</a></li>
<li class="py-1"><a href="/stat" class="link-primary">Statistik</a></li>
<li class="py-1"><a href="/stat/boats" class="link-primary">Bootsauswertung</a></li>
<li class="py-1"><a href="/boatdamage" class="link-primary">Bootsschaden</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{# --- START Edit Form --- #}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="{{ planned_event.id }}"/>
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{# --- END Edit Form --- #}
{% if "scheckbuch" in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Scheckbuch</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{# --- START Delete Btn --- #}
<div class="text-right">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{# --- END Delete Btn --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{% if "Vorstand" in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Vorstand</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/admin/user/fees" class="link-primary">Übersicht User Gebühren</a></li>
<li class="py-1"><a href="/admin/user" class="link-primary">User</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{# --- START Trips --- #}
{% if day.trips | length > 0 %}
{% for trip in day.trips | sort(attribute="planned_starting_time") %}
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-[#f43f5e]">(Absage
{{ trip.cox_name }}
{% if trip.trip_type %}
-
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
{% endif %})</small>
{% else %}
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
{% if "admin" in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Admin</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/admin/boat" class="link-primary">Boote</a></li>
<li class="py-1"><a href="/admin/user" class="link-primary">User</a></li>
<li class="py-1"><a href="/admin/mail" class="link-primary">Mail (beautifully layouted)</a></li>
<li class="py-1"><a href="/admin/rss" class="link-primary">Logs</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
<div>
{% set_global cur_user_participates = false %}
{% for rower in trip.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%}
<a href="/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="trip{{ trip.trip_details_id }}">
{% if trip.max_people == 0 %}
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/join/{{ trip.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
</div>
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "admin" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% endif %}
{% endblock content%}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}

View File

@ -24,7 +24,7 @@
<div class="md:col-span-3 bg-white dark:bg-primary-900 rounded-md shadow">
<h2 class="h2">Neue Ausfahrt</h2>
<div class="p-3">
{{ log::new(shipmaster=loggedin_user.id) }}
{{ log::new(only_ones="cox" not in loggedin_user.roles, shipmaster=loggedin_user.id) }}
</div>
</div>
<div class="bg-white dark:bg-primary-900 rounded-md shadow">

View File

@ -1,343 +0,0 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
{% if "paid" not in loggedin_user.roles %}
<div class="grid gap-3 sm:col-span-2 lg:col-span-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Vereinsgebühren</h2>
<div class="text-sm p-3">
{% include "includes/qrcode" %}
<div id="qrcode" style="float: left; padding-top: 10 pt; padding-right: 10pt; padding-bottom: 10pt;"></div>
<script type="text/javascript">
var sepaqr = new sepaQR({
benefName: 'ASKÖ Ruderverein Donau Linz',
benefBIC: 'BKAUATWWXXX',
benefAccNr: 'AT131200080413001200',
amountEuro: {{ fee.sum_in_cents/100 }},
remittanceInf: 'Vereinsgebühren {{ fee.name }}',
});
var code = sepaqr.makeCodeInto("qrcode");
</script>
<b>Dein Vereinsbeitrag ({{ fee.name }}): {{ fee.sum_in_cents / 100 }}€ {% if fee.parts | length == 1 %} ({{ fee.parts[0].0 }}) {% endif %}</b><br />
{% if fee.parts | length > 1 %}
<small>
Setzt sich zusammen aus:
<ul style="list-style: circle; padding-left: 1em;">
{% for p in fee.parts %}
<li>{{ p.0 }} ({{ p.1 / 100 }}€) {% if not loop.last %} + {% endif %}</li>
{% endfor %}
</ul>
</small>
{% endif %}
Bitte auf folgendes Konto überweisen: IBAN AT13 1200 0804 1300 1200. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.<br />
Falls die Berechnung nicht stimmt (korrekte Preise findest du <a href="https://rudernlinz.at/unser-verein/gebuhren/" target="_blank" rel="noopener noreferrer">hier</a>) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.<br />
<small>Wir aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren :^)</small>
</div>
</div>
</div>
{% endif %}
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js" style="min-height: 10rem;" data-trips="{{ amount_trips }}" data-month="{{ day.day| date(format='%m') }}" data-coxneeded="{{ day_cox_needed }}">
<div>
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div
class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in planned_event.rower%}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if planned_event.max_people > planned_event.rower | length %}
{% if cur_user_participates == false %}
<a href="/planned/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in planned_event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="planned_event" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "planned_event" in loggedin_user.roles %}
<form action="/planned/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% if planned_event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% endif %}
{% if "planned_event" in loggedin_user.roles %}
{# --- START Edit Form --- #}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="{{ planned_event.id }}"/>
{{ macros::input(label='Titel', name='name', type='input', value=planned_event.name) }}
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #}
<div class="text-right">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{# --- END Delete Btn --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{# --- START Trips --- #}
{% if day.trips | length > 0 %}
{% for trip in day.trips | sort(attribute="planned_starting_time") %}
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-[#f43f5e]">(Absage
{{ trip.cox_name }}
{% if trip.trip_type %}
-
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
{% endif %})</small>
{% else %}
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div>
{% set_global cur_user_participates = false %}
{% for rower in trip.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%}
<a href="/planned/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div id="trip{{ trip.trip_details_id }}">
{% if trip.max_people == 0 %}
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
{% else %}
{% set amount_cur_rower = trip.rower | length %}
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/planned/join/{{ trip.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3>
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min='0') }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "planned_event" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "planned_event" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "planned_event" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "planned_event" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "planned_event" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}

13
update.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
git pull
cargo b -r
cd frontend
npm install
npm run build
cd ..
cd svelte
npm install
npm run build
sudo systemctl restart rot
sudo systemctl restart rot-svelte