Compare commits
69 Commits
a9f2e11fe6
...
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
714ca0219a | ||
|
|
dae921528e | ||
|
|
bdcceb53e0 | ||
|
|
62549c9039 | ||
|
|
5ecf1eca18 | ||
|
|
a81e9f8349 | ||
|
|
256b968509 | ||
|
|
26300e27c3 | ||
|
|
afff7c123a | ||
|
|
94dc125a1b | ||
|
|
f070a4b0df | ||
|
|
260767680b | ||
|
|
09299c65de | ||
|
|
39e5479947 | ||
|
|
5c15fb5599 | ||
|
|
dce451bfc2 | ||
|
|
bba4486136 | ||
|
|
a18b01450d | ||
|
|
a6f4b47d92 | ||
|
|
8d9220ae2c | ||
|
|
7b17cd99fe | ||
|
|
44f355a5ea | ||
|
|
fb27e9c026 | ||
|
|
14d2270260 | ||
|
|
7d4d2e4c54 | ||
|
|
352d0aa4c6 | ||
|
|
76269e7358 | ||
|
|
87297a3b97 | ||
|
|
b12574994c | ||
|
|
66bb7c1942 | ||
|
|
f8b79de6a2 | ||
|
|
c7fe7490f1 | ||
|
|
334c14a307 | ||
|
|
b9f009162d | ||
|
|
b844f487bc | ||
|
|
829dc074e2 | ||
|
|
63c7e89dd4 | ||
|
|
53a6408466 | ||
|
|
0793bb5e31 | ||
|
|
8d243302f0 | ||
|
|
2cce310fc4 | ||
|
|
a9707dc799 | ||
|
|
67dee7a746 | ||
|
|
c7c1b6c7c6 | ||
|
|
f7d5d9d019 | ||
|
|
f1d7d3617d | ||
|
|
df580d98c0 | ||
|
|
06afdf4349 | ||
|
|
db96732a62 | ||
|
|
aba4bafb65 | ||
|
|
d57ee4c1e7 | ||
|
|
9866cc3ffd | ||
| 6ec42c6c75 | |||
|
|
7b760afeb3 | ||
|
|
21dcf1a476 | ||
|
|
35d1a0f734 | ||
|
|
effbb61707 | ||
|
|
7eb5ab9ab2 | ||
|
|
aec741f094 | ||
|
|
632163d751 | ||
|
|
a93f063009 | ||
|
|
cc3a547961 | ||
| fdc8525abe | |||
| 8075d1fe1f | |||
| 8709989fbe | |||
|
|
b0d6bb5512 | ||
|
|
690b7f4034 | ||
|
|
fdf56a4c0b | ||
|
|
a908db4f38 |
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
1
.env.development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL= https://localhost:9500/api
|
||||||
1
.env.production
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL= https://members.tecamino.com/api
|
||||||
120
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
name: Build Quasar SPA and Go Backend for memberApp
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-spa:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Install Quasar CLI
|
||||||
|
run: |
|
||||||
|
npm cache clean --force
|
||||||
|
npm install -g @quasar/cli --force
|
||||||
|
|
||||||
|
- name: Build Quasar SPA
|
||||||
|
run: quasar build
|
||||||
|
|
||||||
|
- name: Upload SPA artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: quasar-dist
|
||||||
|
path: ./dist/spa
|
||||||
|
|
||||||
|
build-backend:
|
||||||
|
needs: build-spa
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- goos: windows
|
||||||
|
arch: amd64
|
||||||
|
ext: .exe
|
||||||
|
- goos: linux
|
||||||
|
arch: amd64
|
||||||
|
ext: ""
|
||||||
|
- goos: linux
|
||||||
|
arch: arm64
|
||||||
|
ext: ""
|
||||||
|
- goos: linux
|
||||||
|
arch: arm
|
||||||
|
arm_version: 6
|
||||||
|
ext: ""
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download SPA artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: quasar-dist
|
||||||
|
path: ./dist/spa
|
||||||
|
|
||||||
|
- name: Ensure latest Go is installed in /data/go
|
||||||
|
run: |
|
||||||
|
export GOROOT=/data/go/go
|
||||||
|
export PATH=$GOROOT/bin:$PATH
|
||||||
|
export GOCACHE=/data/gocache
|
||||||
|
export GOMODCACHE=/data/gomodcache
|
||||||
|
mkdir -p $GOCACHE $GOMODCACHE
|
||||||
|
|
||||||
|
if [ ! -x "$GOROOT/bin/go" ]; then
|
||||||
|
echo "Go not found in $GOROOT, downloading latest stable..."
|
||||||
|
GO_VERSION=$(curl -s https://go.dev/VERSION?m=text | head -n1)
|
||||||
|
echo "Latest version is $GO_VERSION"
|
||||||
|
mkdir -p /data/go
|
||||||
|
curl -sSL "https://go.dev/dl/${GO_VERSION}.linux-amd64.tar.gz" -o /tmp/go.tar.gz
|
||||||
|
tar -C /data/go -xzf /tmp/go.tar.gz
|
||||||
|
else
|
||||||
|
echo "Using cached Go from $GOROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
go version
|
||||||
|
|
||||||
|
- name: Go Mod Tidy & Download
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
export GOROOT=/data/go/go
|
||||||
|
export PATH=$GOROOT/bin:$PATH
|
||||||
|
export GOCACHE=/data/gocache
|
||||||
|
export GOMODCACHE=/data/gomodcache
|
||||||
|
mkdir -p $GOCACHE $GOMODCACHE
|
||||||
|
go mod tidy -v
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
- name: Build Go backend binary
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
export GOROOT=/data/go/go
|
||||||
|
export PATH=$GOROOT/bin:$PATH
|
||||||
|
export GOCACHE=/data/gocache
|
||||||
|
export GOMODCACHE=/data/gomodcache
|
||||||
|
mkdir -p $GOCACHE $GOMODCACHE
|
||||||
|
|
||||||
|
OUTPUT="../memberApp-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }}"
|
||||||
|
if [ "${{ matrix.arch }}" = "arm" ]; then
|
||||||
|
GOARM=${{ matrix.arm_version }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.arch }} go build -ldflags="-s -w" -trimpath -o "$OUTPUT" main.go
|
||||||
|
|
||||||
|
- name: Upload combined package
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: memberApp-${{ matrix.goos }}-${{ matrix.arch }}
|
||||||
|
path: |
|
||||||
|
./dist/spa
|
||||||
|
memberApp-${{ matrix.goos }}-${{ matrix.arch }}${{ matrix.ext }}
|
||||||
49
.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
.DS_Store
|
||||||
|
.thumbs.db
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Quasar core related directories
|
||||||
|
.quasar
|
||||||
|
/dist
|
||||||
|
/quasar.config.*.temporary.compiled*
|
||||||
|
|
||||||
|
# Cordova related directories and files
|
||||||
|
/src-cordova/node_modules
|
||||||
|
/src-cordova/platforms
|
||||||
|
/src-cordova/plugins
|
||||||
|
/src-cordova/www
|
||||||
|
|
||||||
|
# Capacitor related directories and files
|
||||||
|
/src-capacitor
|
||||||
|
/src-capacitor/www
|
||||||
|
/src-capacitor/node_modules
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.scene
|
||||||
|
|
||||||
|
# local .env files
|
||||||
|
.env.local*
|
||||||
|
|
||||||
|
# local .db files
|
||||||
|
*.db
|
||||||
|
*.dba
|
||||||
|
|
||||||
|
# local personal backup files
|
||||||
|
*.back
|
||||||
|
|
||||||
|
# local .log files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# golang quasar websever executable
|
||||||
|
backend/server-linux-arm64
|
||||||
|
backend/cert
|
||||||
5
.npmrc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# pnpm-related options
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
# to get the latest compatible packages when creating the project https://github.com/pnpm/pnpm/issues/6463
|
||||||
|
resolution-mode=highest
|
||||||
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
15
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"vue.volar",
|
||||||
|
"wayou.vscode-todo-highlight"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"octref.vetur",
|
||||||
|
"hookyqr.beautify",
|
||||||
|
"dbaeumer.jshint",
|
||||||
|
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
16
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.codeActionsOnSave": [
|
||||||
|
"source.fixAll.eslint"
|
||||||
|
],
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"vue"
|
||||||
|
],
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
63
backend/env/env.go
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EnvKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Env EnvKey = "ENV"
|
||||||
|
GinMode EnvKey = "GIN_MODE"
|
||||||
|
Debug EnvKey = "DEBUG"
|
||||||
|
PrivKey EnvKey = "PRIVKEY"
|
||||||
|
Fullchain EnvKey = "FULLCHAIN"
|
||||||
|
Https EnvKey = "HTTPS"
|
||||||
|
HostUrl EnvKey = "HOST_URL"
|
||||||
|
HostPort EnvKey = "HOST_PORT"
|
||||||
|
WorkingDir EnvKey = "WORKING_DIR"
|
||||||
|
Spa EnvKey = "SPA"
|
||||||
|
AccessSecret EnvKey = "ACCESS_SECRET"
|
||||||
|
RefreshSecret EnvKey = "REFRESH_SECRET"
|
||||||
|
Organization EnvKey = "ORGANIZATION"
|
||||||
|
DOMAIN EnvKey = "DOMAIN"
|
||||||
|
AllowOrigin EnvKey = "ALLOWORIGIN"
|
||||||
|
TimeZone EnvKey = "TMEZONE"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
EnvDevelopment = "development"
|
||||||
|
EnvProduction = "Prodction"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (key EnvKey) GetValue() string {
|
||||||
|
return os.Getenv(string(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key EnvKey) GetBoolValue() bool {
|
||||||
|
return strings.ToLower(os.Getenv(string(key))) == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (key EnvKey) GetUIntValue() uint {
|
||||||
|
value, err := strconv.ParseUint(os.Getenv(string(key)), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(file string) error {
|
||||||
|
return godotenv.Load(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InDevelopmentMode() bool {
|
||||||
|
return Env.GetValue() == EnvDevelopment
|
||||||
|
}
|
||||||
|
|
||||||
|
func InDebugMode() bool {
|
||||||
|
return strings.ToLower(Debug.GetValue()) == "true"
|
||||||
|
}
|
||||||
15
backend/example/example.env
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
ENV=development
|
||||||
|
GIN_MODE=release
|
||||||
|
DEBUG=false
|
||||||
|
SPA=directory_of_spa_files
|
||||||
|
WORKING_DIR=.
|
||||||
|
HOST_URL=your_local_url
|
||||||
|
HOST_PORT=your_local_port
|
||||||
|
HTTPS=true
|
||||||
|
PRIVKEY=your_certificate_key_file
|
||||||
|
FULLCHAIN=your_certificate_fullchain_file
|
||||||
|
ACCESS_SECRET=your_32bit_long_access_secret
|
||||||
|
REFRESH_SECRET=your_32bit_long_referesh_secret
|
||||||
|
ALLOWORIGIN=all_allowed_urls
|
||||||
|
DOMAIN=your_domain
|
||||||
|
TIMEZONE=your_timezone in IANA format
|
||||||
67
backend/go.mod
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
module backend
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitea.tecamino.com/paadi/access-handler v1.0.29
|
||||||
|
gitea.tecamino.com/paadi/memberDB v1.1.13
|
||||||
|
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
|
||||||
|
gitea.tecamino.com/paadi/tecamino-logger v0.2.1
|
||||||
|
github.com/gin-contrib/cors v1.7.6
|
||||||
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
golang.org/x/crypto v0.43.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
gitea.tecamino.com/paadi/dbHandler v1.1.7 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
|
golang.org/x/net v0.45.0 // indirect
|
||||||
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gorm.io/gorm v1.31.0 // indirect
|
||||||
|
modernc.org/libc v1.66.3 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.39.0 // indirect
|
||||||
|
)
|
||||||
163
backend/go.sum
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
gitea.tecamino.com/paadi/access-handler v1.0.29 h1:FZ67co/rfJffftT6xOp6psZKFtdEReaAR7PnEZi7ltI=
|
||||||
|
gitea.tecamino.com/paadi/access-handler v1.0.29/go.mod h1:Dmme8URu3lENPhlkZcdEeIKm8VMlAgT/jNLECLLS7Vs=
|
||||||
|
gitea.tecamino.com/paadi/dbHandler v1.1.7 h1:NqVbxbUwd7EZX6HYntyLYwwPbyTPevOhIBTFqoCVqOU=
|
||||||
|
gitea.tecamino.com/paadi/dbHandler v1.1.7/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
|
||||||
|
gitea.tecamino.com/paadi/memberDB v1.1.13 h1:P5UsTt3d8829H9d3vfMAWpDN7ONqwhr8ndIuL9lBuvQ=
|
||||||
|
gitea.tecamino.com/paadi/memberDB v1.1.13/go.mod h1:FRbhFgXq4jDpfCrCfHCVr7VcA44fR8J3XXQFeO6QSBk=
|
||||||
|
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1 h1:vAq7mwUxlxJuLzCQSDMrZCwo8ky5usWi9Qz+UP+WnkI=
|
||||||
|
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1/go.mod h1:+tmf1rjPaKEoNeUcr1vdtoFIFweNG3aUGevDAl3NMBk=
|
||||||
|
gitea.tecamino.com/paadi/tecamino-logger v0.2.1 h1:sQTBKYPdzn9mmWX2JXZBtGBvNQH7cuXIwsl4TD0aMgE=
|
||||||
|
gitea.tecamino.com/paadi/tecamino-logger v0.2.1/go.mod h1:FkzRTldUBBOd/iy2upycArDftSZ5trbsX5Ira5OzJgM=
|
||||||
|
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||||
|
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||||
|
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||||
|
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||||
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
|
||||||
|
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
|
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
|
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||||
|
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
|
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
|
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||||
|
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
|
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||||
|
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||||
|
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||||
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||||
|
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||||
|
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
|
||||||
|
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||||
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
|
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||||
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
|
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
||||||
|
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||||
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
|
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
|
||||||
|
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
||||||
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
226
backend/main.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"backend/env"
|
||||||
|
"backend/models"
|
||||||
|
"backend/server"
|
||||||
|
"backend/utils"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
AccessHandler "gitea.tecamino.com/paadi/access-handler"
|
||||||
|
dbApi "gitea.tecamino.com/paadi/memberDB/api"
|
||||||
|
"gitea.tecamino.com/paadi/tecamino-dbm/cert"
|
||||||
|
|
||||||
|
"gitea.tecamino.com/paadi/tecamino-logger/logging"
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// set cli flage
|
||||||
|
envFile := flag.String("env", ".env", "enviroment file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// load enviroment file if exists
|
||||||
|
if err := env.Load(*envFile); err != nil {
|
||||||
|
fmt.Println("no .env found path: ", *envFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
devMode := env.InDevelopmentMode()
|
||||||
|
// set gin mode
|
||||||
|
if !devMode {
|
||||||
|
gin.SetMode(env.GinMode.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
workingDir := env.WorkingDir.GetValue()
|
||||||
|
spa := env.Spa.GetValue()
|
||||||
|
|
||||||
|
//change working directory only if value is given
|
||||||
|
if workingDir != "." && workingDir != "" {
|
||||||
|
os.Chdir(workingDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
//set allowed origins
|
||||||
|
var allowOrigins models.StringSlice
|
||||||
|
|
||||||
|
if strings.Contains(env.DOMAIN.GetValue(), "http") {
|
||||||
|
allowOrigins.Set(env.DOMAIN.GetValue())
|
||||||
|
}
|
||||||
|
if env.AllowOrigin.GetValue() != "" {
|
||||||
|
allowOrigins.Set(env.AllowOrigin.GetValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Could not get working directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
folderName := filepath.Base(wd)
|
||||||
|
logFileName := folderName + ".log"
|
||||||
|
|
||||||
|
logger, err := logging.NewLogger(logFileName, &logging.Config{
|
||||||
|
MaxSize: 1,
|
||||||
|
MaxBackup: 3,
|
||||||
|
MaxAge: 28,
|
||||||
|
Debug: env.InDebugMode(),
|
||||||
|
TerminalOut: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("main new logger", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//new login manager
|
||||||
|
accessHandler, err := AccessHandler.NewAccessHandler(".", logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("main login manager", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// new server
|
||||||
|
s := server.NewServer()
|
||||||
|
|
||||||
|
// initiate Database handler
|
||||||
|
dbHandler, err := dbApi.NewAPIHandler(logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("main login manager", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//get local ip
|
||||||
|
httpString := "http://"
|
||||||
|
if env.Https.GetBoolValue() {
|
||||||
|
httpString = "https://"
|
||||||
|
|
||||||
|
}
|
||||||
|
allowOrigins = append(allowOrigins, httpString+"localhost:9000", httpString+"localhost:9500", httpString+"127.0.0.1:9000", httpString+"0.0.0.0:9500")
|
||||||
|
|
||||||
|
localIP, err := utils.GetLocalIP()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("main", fmt.Sprintf("get local ip : %s", err))
|
||||||
|
} else {
|
||||||
|
allowOrigins = append(allowOrigins, fmt.Sprintf("%s%s:9000", httpString, localIP), fmt.Sprintf("%s%s:9500", httpString, localIP))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(100, allowOrigins)
|
||||||
|
|
||||||
|
s.Routes.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: allowOrigins,
|
||||||
|
//AllowOrigins: []string{"*"},
|
||||||
|
AllowMethods: []string{"POST", "GET", "DELETE", "OPTIONS"},
|
||||||
|
AllowHeaders: []string{"Origin", "Content-Type"},
|
||||||
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 12 * time.Hour,
|
||||||
|
}))
|
||||||
|
|
||||||
|
//set logger for AuthMiddleware
|
||||||
|
accessHandler.SetMiddlewareLogger(s.Routes)
|
||||||
|
api := s.Routes.Group("/api")
|
||||||
|
//set routes
|
||||||
|
|
||||||
|
//public
|
||||||
|
api.GET("/logout", accessHandler.Logout)
|
||||||
|
api.GET("/login/me", accessHandler.Me)
|
||||||
|
|
||||||
|
api.POST("/login", accessHandler.Login)
|
||||||
|
|
||||||
|
//private
|
||||||
|
auth := api.Group("", accessHandler.AuthMiddleware())
|
||||||
|
|
||||||
|
role := auth.Group("", accessHandler.AuthorizeRole("/api"))
|
||||||
|
role.GET("/members", dbHandler.GetMember)
|
||||||
|
auth.GET("/events", dbHandler.GetEvent)
|
||||||
|
auth.GET("/groups", dbHandler.GetGroup)
|
||||||
|
|
||||||
|
auth.GET("/users", accessHandler.GetUser)
|
||||||
|
auth.GET("/roles", accessHandler.GetRole)
|
||||||
|
|
||||||
|
auth.POST("database/open", dbHandler.OpenDatabase)
|
||||||
|
auth.POST("/members/add", dbHandler.AddNewMember)
|
||||||
|
auth.POST("/members/edit", dbHandler.UpdateMember)
|
||||||
|
auth.POST("/members/delete", dbHandler.DeleteMember)
|
||||||
|
auth.POST("/members/import/csv", dbHandler.ImportCSV)
|
||||||
|
|
||||||
|
auth.POST("/events/add", dbHandler.StartNewEvent)
|
||||||
|
auth.POST("/events/edit", dbHandler.UpdateEvent)
|
||||||
|
auth.POST("/events/add/attendees", dbHandler.AddNewAttendees)
|
||||||
|
auth.POST("/events/delete/attendees", dbHandler.DeleteAttendee)
|
||||||
|
auth.POST("/events/delete", dbHandler.DeleteEvent)
|
||||||
|
|
||||||
|
auth.POST("/groups/add", dbHandler.NewGroup)
|
||||||
|
auth.POST("/groups/edit", dbHandler.UpdateGroup)
|
||||||
|
auth.POST("/groups/delete", dbHandler.DeleteGroup)
|
||||||
|
|
||||||
|
auth.GET("/responsible", dbHandler.GetResponsible)
|
||||||
|
auth.POST("/responsible/add", dbHandler.AddNewResponsible)
|
||||||
|
auth.POST("/responsible/delete", dbHandler.DeleteResponsible)
|
||||||
|
|
||||||
|
auth.POST("/roles/add", accessHandler.AddRole)
|
||||||
|
auth.POST("/roles/update", accessHandler.UpdateRole)
|
||||||
|
auth.POST("/roles/delete", accessHandler.DeleteRole)
|
||||||
|
|
||||||
|
auth.POST("/users/add", accessHandler.AddUser)
|
||||||
|
auth.POST("/users/update", accessHandler.UpdateUser)
|
||||||
|
auth.POST("/users/new/password", accessHandler.ChangePassword)
|
||||||
|
auth.POST("/users/delete", accessHandler.DeleteUser)
|
||||||
|
|
||||||
|
api.POST("/login/refresh", accessHandler.Refresh)
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
s.Routes.StaticFS("/assets", gin.Dir(filepath.Join(spa, "assets"), true))
|
||||||
|
s.Routes.NoRoute(func(c *gin.Context) {
|
||||||
|
// Disallow fallback for /api paths
|
||||||
|
if strings.HasPrefix(c.Request.URL.Path, "/api") {
|
||||||
|
c.JSON(http.StatusNotFound, models.NewJsonMessageResponse("API endpoint not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Try to serve file from SPA directory
|
||||||
|
filePath := filepath.Join(spa, c.Request.URL.Path)
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
c.File(filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Fallback to index.html for SPA routing
|
||||||
|
c.File(filepath.Join(spa, "index.html"))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
if err := utils.OpenBrowser(fmt.Sprintf("%slocalhost:%s", httpString, env.HostPort.GetValue()), logger); err != nil {
|
||||||
|
logger.Error("main", fmt.Sprintf("starting browser error : %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if env.Https.GetBoolValue() {
|
||||||
|
if env.Fullchain.GetValue() == "" {
|
||||||
|
logger.Error("ssl certificate", "-cert flag not given for https server")
|
||||||
|
log.Fatal("-cert flag not given for https server")
|
||||||
|
}
|
||||||
|
if env.PrivKey.GetValue() == "" {
|
||||||
|
logger.Error("ssl key", "-chain flag not given for https server")
|
||||||
|
log.Fatal("-chain flag not given for https server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// start https server
|
||||||
|
logger.Info("main", fmt.Sprintf("https listen on ip: %s port: %s", env.HostUrl.GetValue(), env.HostPort.GetValue()))
|
||||||
|
if err := s.ServeHttps(env.HostUrl.GetValue(), env.HostPort.GetUIntValue(), cert.Cert{Organization: env.Organization.GetValue(), CertFile: env.Fullchain.GetValue(), KeyFile: env.PrivKey.GetValue()}); err != nil {
|
||||||
|
logger.Error("main", "error https server "+err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start http server
|
||||||
|
logger.Info("main", fmt.Sprintf("http listen on ip: %s port: %s", env.HostUrl.GetValue(), env.HostPort.GetValue()))
|
||||||
|
if err := s.ServeHttp(env.HostUrl.GetValue(), env.HostPort.GetUIntValue()); err != nil {
|
||||||
|
logger.Error("main", "error http server "+err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
18
backend/models/jsonResponse.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type JsonResponse struct {
|
||||||
|
Error bool `json:"error,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJsonMessageResponse(msg string) JsonResponse {
|
||||||
|
return JsonResponse{
|
||||||
|
Message: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewJsonErrorResponse(err error) JsonResponse {
|
||||||
|
return JsonResponse{
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/models/stringSlice.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
func (s *StringSlice) Set(value string) {
|
||||||
|
if strings.Contains(value, ",") {
|
||||||
|
*s = append(*s, strings.Split(value, ",")...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*s = append(*s, value)
|
||||||
|
}
|
||||||
38
backend/server/server.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.tecamino.com/paadi/tecamino-dbm/cert"
|
||||||
|
"gitea.tecamino.com/paadi/tecamino-logger/logging"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// server model for database manager websocket
|
||||||
|
type Server struct {
|
||||||
|
Routes *gin.Engine
|
||||||
|
sync.RWMutex
|
||||||
|
Logger *logging.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// initalizes new dbm server
|
||||||
|
func NewServer() *Server {
|
||||||
|
return &Server{
|
||||||
|
Routes: gin.Default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve dbm as http
|
||||||
|
func (s *Server) ServeHttp(ip string, port uint) error {
|
||||||
|
return s.Routes.Run(fmt.Sprintf("%s:%d", ip, port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve dbm as http
|
||||||
|
func (s *Server) ServeHttps(url string, port uint, cert cert.Cert) error {
|
||||||
|
// generate self signed tls certificate
|
||||||
|
if err := cert.GenerateSelfSignedCert(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.Routes.RunTLS(fmt.Sprintf("%s:%d", url, port), cert.CertFile, cert.KeyFile)
|
||||||
|
}
|
||||||
14
backend/utils/hash.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check password
|
||||||
|
func CheckPassword(password, hash string) bool {
|
||||||
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||||
|
}
|
||||||
22
backend/utils/ip.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLocalIP() (string, error) {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
||||||
|
if ipNet.IP.To4() != nil {
|
||||||
|
return ipNet.IP.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no local IP address found")
|
||||||
|
}
|
||||||
14
backend/utils/secret.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateJWTSecret(length int) ([]byte, error) {
|
||||||
|
bytes := make([]byte, length)
|
||||||
|
_, err := rand.Read(bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes, nil
|
||||||
|
}
|
||||||
65
backend/utils/utils.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"gitea.tecamino.com/paadi/tecamino-logger/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OpenBrowser(url string, logger *logging.Logger) error {
|
||||||
|
var commands [][]string
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
// Try with Chrome in kiosk mode
|
||||||
|
commands = [][]string{
|
||||||
|
{`C:\Program Files\Google\Chrome\Application\chrome.exe`, "--kiosk", url},
|
||||||
|
{"rundll32", "url.dll,FileProtocolHandler", url}, // fallback
|
||||||
|
}
|
||||||
|
case "darwin":
|
||||||
|
// macOS: open with Chrome in kiosk
|
||||||
|
commands = [][]string{
|
||||||
|
{"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "--kiosk", url},
|
||||||
|
{"open", url}, // fallback
|
||||||
|
}
|
||||||
|
default: // Linux
|
||||||
|
if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" && os.Getenv("XDG_SESSION_TYPE") != "wayland" {
|
||||||
|
|
||||||
|
return fmt.Errorf("os is running i headless mode do not start browser")
|
||||||
|
}
|
||||||
|
commands = [][]string{
|
||||||
|
{"chromium-browser", "--kiosk", url},
|
||||||
|
{"google-chrome", "--kiosk", url},
|
||||||
|
{"firefox", "--kiosk", url},
|
||||||
|
{"xdg-open", url}, // fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range commands {
|
||||||
|
execCmd := exec.Command(cmd[0], cmd[1:]...)
|
||||||
|
if err := execCmd.Start(); err == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Error("utils.OpenBrowser", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("could not open browser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindAllFiles(rootDir, fileExtention string) (files []string, err error) {
|
||||||
|
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
} else if filepath.Ext(d.Name()) == fileExtention {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
83
eslint.config.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import pluginVue from 'eslint-plugin-vue';
|
||||||
|
import pluginQuasar from '@quasar/app-vite/eslint';
|
||||||
|
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';
|
||||||
|
import prettierSkipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
||||||
|
|
||||||
|
export default defineConfigWithVueTs(
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Ignore the following files.
|
||||||
|
* Please note that pluginQuasar.configs.recommended() already ignores
|
||||||
|
* the "node_modules" folder for you (and all other Quasar project
|
||||||
|
* relevant folders and files).
|
||||||
|
*
|
||||||
|
* ESLint requires "ignores" key to be the only one in this object
|
||||||
|
*/
|
||||||
|
// ignores: []
|
||||||
|
},
|
||||||
|
|
||||||
|
pluginQuasar.configs.recommended(),
|
||||||
|
js.configs.recommended,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://eslint.vuejs.org
|
||||||
|
*
|
||||||
|
* pluginVue.configs.base
|
||||||
|
* -> Settings and rules to enable correct ESLint parsing.
|
||||||
|
* pluginVue.configs[ 'flat/essential']
|
||||||
|
* -> base, plus rules to prevent errors or unintended behavior.
|
||||||
|
* pluginVue.configs["flat/strongly-recommended"]
|
||||||
|
* -> Above, plus rules to considerably improve code readability and/or dev experience.
|
||||||
|
* pluginVue.configs["flat/recommended"]
|
||||||
|
* -> Above, plus rules to enforce subjective community defaults to ensure consistency.
|
||||||
|
*/
|
||||||
|
pluginVue.configs['flat/essential'],
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.d.ts', '**/*.vue'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// https://github.com/vuejs/eslint-config-typescript
|
||||||
|
vueTsConfigs.recommendedTypeChecked,
|
||||||
|
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node, // SSR, Electron, config files
|
||||||
|
process: 'readonly', // process.env.*
|
||||||
|
ga: 'readonly', // Google Analytics
|
||||||
|
cordova: 'readonly',
|
||||||
|
Capacitor: 'readonly',
|
||||||
|
chrome: 'readonly', // BEX related
|
||||||
|
browser: 'readonly', // BEX related
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'prefer-promise-reject-errors': 'off',
|
||||||
|
|
||||||
|
// allow debugger during development only
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
files: ['src-pwa/custom-service-worker.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.serviceworker,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
prettierSkipFormatting,
|
||||||
|
);
|
||||||
24
index.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= productName %></title>
|
||||||
|
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="description" content="<%= productDescription %>" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<meta name="msapplication-tap-highlight" content="no" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png" />
|
||||||
|
<link rel="icon" type="image/ico" href="favicon.ico" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- quasar:entry-point -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8445
package-lock.json
generated
Normal file
53
package.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "lightcontrol",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"description": "A Tecamino App",
|
||||||
|
"productName": "Attendence Records",
|
||||||
|
"author": "A. Zuercher",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
|
||||||
|
"format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore",
|
||||||
|
"test": "echo \"No test specified\" && exit 0",
|
||||||
|
"dev": "quasar dev",
|
||||||
|
"build": "quasar build",
|
||||||
|
"postinstall": "quasar prepare"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor-community/sqlite": "^7.0.1",
|
||||||
|
"@quasar/extras": "^1.16.4",
|
||||||
|
"axios": "^1.10.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"quasar": "^2.16.0",
|
||||||
|
"vue": "^3.4.18",
|
||||||
|
"vue-i18n": "^11.1.12",
|
||||||
|
"vue-router": "^4.0.12",
|
||||||
|
"vue3-touch-events": "^5.0.13",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
|
"zxcvbn": "^4.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@quasar/app-vite": "^2.1.0",
|
||||||
|
"@types/node": "^20.5.9",
|
||||||
|
"@types/zxcvbn": "^4.4.5",
|
||||||
|
"@vue/eslint-config-prettier": "^10.1.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.4.0",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"eslint": "^9.14.0",
|
||||||
|
"eslint-plugin-vue": "^9.30.0",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"typescript": "~5.5.3",
|
||||||
|
"vite-plugin-checker": "^0.9.0",
|
||||||
|
"vue-tsc": "^2.0.29"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^28 || ^26 || ^24 || ^22 || ^20 || ^18",
|
||||||
|
"npm": ">= 6.13.4",
|
||||||
|
"yarn": ">= 1.21.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
postcss.config.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||||
|
|
||||||
|
import autoprefixer from 'autoprefixer'
|
||||||
|
// import rtlcss from 'postcss-rtlcss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: [
|
||||||
|
// https://github.com/postcss/autoprefixer
|
||||||
|
autoprefixer({
|
||||||
|
overrideBrowserslist: [
|
||||||
|
'last 4 Chrome versions',
|
||||||
|
'last 4 Firefox versions',
|
||||||
|
'last 4 Edge versions',
|
||||||
|
'last 4 Safari versions',
|
||||||
|
'last 4 Android versions',
|
||||||
|
'last 4 ChromeAndroid versions',
|
||||||
|
'last 4 FirefoxAndroid versions',
|
||||||
|
'last 4 iOS versions'
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
|
||||||
|
// https://github.com/elchininet/postcss-rtlcss
|
||||||
|
// If you want to support RTL css, then
|
||||||
|
// 1. yarn/pnpm/bun/npm install postcss-rtlcss
|
||||||
|
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||||
|
// 3. uncomment the following line (and its import statement above):
|
||||||
|
// rtlcss()
|
||||||
|
]
|
||||||
|
}
|
||||||
31
public/LOGO_CF-ICON_color.svg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Ebene_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="246px" height="246px" viewBox="0 0 246 246" style="enable-background:new 0 0 246 246;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#113A68;}
|
||||||
|
.st1{fill:#9E2D29;}
|
||||||
|
.st2{fill:#9B1515;}
|
||||||
|
.st3{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M166.6,210.9c-11.6,5-24.4,7.8-37.8,7.8c-52.8,0-95.6-42.8-95.6-95.6c0-2.3,0.1-4.6,0.2-6.8
|
||||||
|
c2.4-1.8,10.8-5.2,31.7-5.2s46.4,3.4,71.6,9.4c-13,22.2-15.9,44.7-7.8,62.2C133.6,192.6,143.9,205.2,166.6,210.9"/>
|
||||||
|
<path class="st0" d="M146.9,103.8C102.4,90.7,61.4,88.5,39.7,88.4c0-0.1,0.1-0.2,0.1-0.2c2.5-1.4,8.9-3.3,22.1-3.3
|
||||||
|
c23.7,0,55.7,5.9,86.9,15.8C148.2,101.7,147.5,102.7,146.9,103.8"/>
|
||||||
|
<path class="st0" d="M168.4,49.9c0.7,7.9-2.7,18.9-10.6,34.4C117.3,69.2,78.9,63.7,55.4,61.7c17.5-21,43.9-34.3,73.4-34.3
|
||||||
|
c7.8,0,15.5,0.9,22.8,2.7l3.8,2.5C164.2,38.3,167.8,43.1,168.4,49.9"/>
|
||||||
|
<path class="st0" d="M209.6,71.8c8.7,13.7,14,29.8,14.7,47c-3.9-2.6-8.2-5.3-13.1-8.3C214,95.5,212.6,82.5,209.6,71.8"/>
|
||||||
|
<path class="st0" d="M206.8,144.8c4.5,2.1,9,4.5,13.2,6.7c-2.2,6.9-5.1,13.5-8.7,19.6c-0.6-4-2.2-7.4-3.5-10.3
|
||||||
|
c-2.5-5.6-4.2-9.3-1.2-15.7C206.8,145,206.8,144.9,206.8,144.8"/>
|
||||||
|
<path class="st0" d="M207.2,125.1c5.4,2.9,11.3,6.2,16.6,9.2c-0.1,1.2-0.3,2.4-0.5,3.7c-5.7-3.5-11.8-7-17.2-9.9
|
||||||
|
C206.4,127.1,206.8,126.1,207.2,125.1"/>
|
||||||
|
<path class="st1" d="M185.2,205.6c41.5-36.8,4.9-39.5,15.9-63.1c-14,12.6-44,47.2-18,64.1C164.7,168.9,207.4,157.8,185.2,205.6"/>
|
||||||
|
<path class="st2" d="M198.1,130.8c9.6,5,24.5,13.5,33.5,20c-9.4-5.4-24.7-13-34.9-17.4c-2,3.9-4.3,7.8-7,11.8
|
||||||
|
c-10.3,15-36.7,44.6-12.5,61.4c-51.2-7.1-57.1-49.2-33.1-86.7c0.8-1.2,1.5-2.4,2.3-3.6c-62.2-16.2-119.6-14.7-119.8,0.3
|
||||||
|
c-14.3-6.8-26.2-20.1-2.1-21.8c9.6-0.7,65.2-2.4,125.1,16.2c3-4.9,5.8-9.6,8.2-13.9c-63.3-21.6-124.4-24-125.5-8.6
|
||||||
|
C18.4,81,7.4,67,31.6,66.7c9.9-0.1,68.5,1.5,129.1,25.3c21.2-39.5,16.4-52.7-2-64.6c21.6,1.5,57.2,37,45.6,86.1
|
||||||
|
c8.8,5.4,22.8,14.1,31,20.7c-8.4-5.5-22.6-13.3-31.7-17.9C202.2,121.1,200.4,125.9,198.1,130.8"/>
|
||||||
|
<path class="st2" d="M185.2,205.6c41.5-36.8,4.9-39.5,15.9-63.1c-14,12.6-44,47.2-18,64.1C164.7,168.9,207.4,157.8,185.2,205.6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/icons/favicon-128x128.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
public/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icons/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
231
quasar.config.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
// Configuration for your app
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file
|
||||||
|
|
||||||
|
import { defineConfig } from '#q-app/wrappers';
|
||||||
|
|
||||||
|
export default defineConfig((/* ctx */) => {
|
||||||
|
return {
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
|
||||||
|
// preFetch: true,
|
||||||
|
|
||||||
|
// app boot file (/src/boot)
|
||||||
|
// --> boot files are part of "main.js"
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
|
boot: ['auth', 'axios', 'lang', 'quasar-global', 'restore-route'],
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
||||||
|
css: ['app.scss'],
|
||||||
|
|
||||||
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
|
extras: [
|
||||||
|
// 'ionicons-v4',
|
||||||
|
// 'mdi-v7',
|
||||||
|
// 'fontawesome-v6',
|
||||||
|
// 'eva-icons',
|
||||||
|
// 'themify',
|
||||||
|
// 'line-awesome',
|
||||||
|
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||||
|
|
||||||
|
'roboto-font', // optional, you are not bound to it
|
||||||
|
'material-icons', // optional, you are not bound to it
|
||||||
|
],
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#build
|
||||||
|
build: {
|
||||||
|
target: {
|
||||||
|
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||||
|
node: 'node20',
|
||||||
|
publicPath: '/',
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
strict: true,
|
||||||
|
vueShim: true,
|
||||||
|
// extendTsConfig (tsConfig) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||||
|
// vueRouterBase,
|
||||||
|
// vueDevtools,
|
||||||
|
// vueOptionsAPI: false,
|
||||||
|
|
||||||
|
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||||
|
|
||||||
|
// publicPath: '/',
|
||||||
|
// analyze: true,
|
||||||
|
// env: {},
|
||||||
|
// rawDefine: {}
|
||||||
|
// ignorePublicFolder: true,
|
||||||
|
// minify: false,
|
||||||
|
// polyfillModulePreload: true,
|
||||||
|
// distDir
|
||||||
|
|
||||||
|
// extendViteConf (viteConf) {},
|
||||||
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
|
vitePlugins: [
|
||||||
|
[
|
||||||
|
'vite-plugin-checker',
|
||||||
|
{
|
||||||
|
vueTsc: true,
|
||||||
|
eslint: {
|
||||||
|
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{ts,js,mjs,cjs,vue}"',
|
||||||
|
useFlatConfig: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ server: false },
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
|
||||||
|
devServer: {
|
||||||
|
open: true, // opens browser window automatically
|
||||||
|
https: true, // or true if you want HTTPS
|
||||||
|
port: 9000, // your custom port
|
||||||
|
// host: '0.0.0.0', // allows external access (not just localhost)
|
||||||
|
// allowedHosts: ['members.tecamino.com'],
|
||||||
|
// headers: {
|
||||||
|
// 'Access-Control-Allow-Origin': '*',
|
||||||
|
// },
|
||||||
|
// proxy: {
|
||||||
|
// '/v1': {
|
||||||
|
// target: 'http://0.0.0.0:4040/',
|
||||||
|
// changeOrigin: true,
|
||||||
|
// secure: false,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||||
|
framework: {
|
||||||
|
config: {},
|
||||||
|
|
||||||
|
// iconSet: 'material-icons', // Quasar icon set
|
||||||
|
// lang: 'en-US', // Quasar language pack
|
||||||
|
|
||||||
|
// For special cases outside of where the auto-import strategy can have an impact
|
||||||
|
// (like functional components as one of the examples),
|
||||||
|
// you can manually specify Quasar components/directives to be available everywhere:
|
||||||
|
//
|
||||||
|
// components: [],
|
||||||
|
//directives: [],
|
||||||
|
|
||||||
|
// Quasar plugins
|
||||||
|
plugins: ['Notify', 'Dialog'],
|
||||||
|
},
|
||||||
|
|
||||||
|
// animations: 'all', // --- includes all animations
|
||||||
|
// https://v2.quasar.dev/options/animations
|
||||||
|
animations: [],
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#sourcefiles
|
||||||
|
// sourceFiles: {
|
||||||
|
// rootComponent: 'src/App.vue',
|
||||||
|
// router: 'src/router/index',
|
||||||
|
// store: 'src/store/index',
|
||||||
|
// pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
|
||||||
|
// pwaServiceWorker: 'src-pwa/custom-service-worker',
|
||||||
|
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||||
|
// electronMain: 'src-electron/electron-main',
|
||||||
|
// electronPreload: 'src-electron/electron-preload'
|
||||||
|
// bexManifestFile: 'src-bex/manifest.json
|
||||||
|
// },
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||||
|
ssr: {
|
||||||
|
prodPort: 3000, // The default port that the production server should use
|
||||||
|
// (gets superseded if process.env.PORT is specified at runtime)
|
||||||
|
|
||||||
|
middlewares: [
|
||||||
|
'render', // keep this as last one
|
||||||
|
],
|
||||||
|
|
||||||
|
// extendPackageJson (json) {},
|
||||||
|
// extendSSRWebserverConf (esbuildConf) {},
|
||||||
|
|
||||||
|
// manualStoreSerialization: true,
|
||||||
|
// manualStoreSsrContextInjection: true,
|
||||||
|
// manualStoreHydration: true,
|
||||||
|
// manualPostHydrationTrigger: true,
|
||||||
|
|
||||||
|
pwa: false,
|
||||||
|
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
|
|
||||||
|
// pwaExtendGenerateSWOptions (cfg) {},
|
||||||
|
// pwaExtendInjectManifestOptions (cfg) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||||
|
pwa: {
|
||||||
|
workboxMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
||||||
|
// swFilename: 'sw.js',
|
||||||
|
// manifestFilename: 'manifest.json',
|
||||||
|
// extendManifestJson (json) {},
|
||||||
|
// useCredentialsForManifestTag: true,
|
||||||
|
// injectPwaMetaTags: false,
|
||||||
|
// extendPWACustomSWConf (esbuildConf) {},
|
||||||
|
// extendGenerateSWOptions (cfg) {},
|
||||||
|
// extendInjectManifestOptions (cfg) {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
|
||||||
|
cordova: {
|
||||||
|
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||||
|
capacitor: {
|
||||||
|
hideSplashscreen: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
||||||
|
electron: {
|
||||||
|
// extendElectronMainConf (esbuildConf) {},
|
||||||
|
// extendElectronPreloadConf (esbuildConf) {},
|
||||||
|
|
||||||
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
|
// Electron preload scripts (if any) from /src-electron, WITHOUT file extension
|
||||||
|
preloadScripts: ['electron-preload'],
|
||||||
|
|
||||||
|
// specify the debugging port to use for the Electron app when running in development mode
|
||||||
|
inspectPort: 5858,
|
||||||
|
|
||||||
|
bundler: 'packager', // 'packager' or 'builder'
|
||||||
|
|
||||||
|
packager: {
|
||||||
|
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||||
|
// OS X / Mac App Store
|
||||||
|
// appBundleId: '',
|
||||||
|
// appCategoryType: '',
|
||||||
|
// osxSign: '',
|
||||||
|
// protocol: 'myapp://path',
|
||||||
|
// Windows only
|
||||||
|
// win32metadata: { ... }
|
||||||
|
},
|
||||||
|
|
||||||
|
builder: {
|
||||||
|
// https://www.electron.build/configuration/configuration
|
||||||
|
|
||||||
|
appId: 'lightcontrol',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||||
|
bex: {
|
||||||
|
// extendBexScriptsConf (esbuildConf) {},
|
||||||
|
// extendBexManifestJson (json) {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of extra scripts (js/ts) not in your bex manifest that you want to
|
||||||
|
* compile and use in your browser extension. Maybe dynamic use them?
|
||||||
|
*
|
||||||
|
* Each entry in the list should be a relative filename to /src-bex/
|
||||||
|
*
|
||||||
|
* @example [ 'my-script.ts', 'sub-folder/my-other-script.js' ]
|
||||||
|
*/
|
||||||
|
extraScripts: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
7
src/App.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
||||||
1
src/assets/LOGO_CF-BERN_high_color.svg
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
31
src/assets/LOGO_CF-ICON_color.svg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Adobe Illustrator 24.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Ebene_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="246px" height="246px" viewBox="0 0 246 246" style="enable-background:new 0 0 246 246;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#113A68;}
|
||||||
|
.st1{fill:#9E2D29;}
|
||||||
|
.st2{fill:#9B1515;}
|
||||||
|
.st3{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M166.6,210.9c-11.6,5-24.4,7.8-37.8,7.8c-52.8,0-95.6-42.8-95.6-95.6c0-2.3,0.1-4.6,0.2-6.8
|
||||||
|
c2.4-1.8,10.8-5.2,31.7-5.2s46.4,3.4,71.6,9.4c-13,22.2-15.9,44.7-7.8,62.2C133.6,192.6,143.9,205.2,166.6,210.9"/>
|
||||||
|
<path class="st0" d="M146.9,103.8C102.4,90.7,61.4,88.5,39.7,88.4c0-0.1,0.1-0.2,0.1-0.2c2.5-1.4,8.9-3.3,22.1-3.3
|
||||||
|
c23.7,0,55.7,5.9,86.9,15.8C148.2,101.7,147.5,102.7,146.9,103.8"/>
|
||||||
|
<path class="st0" d="M168.4,49.9c0.7,7.9-2.7,18.9-10.6,34.4C117.3,69.2,78.9,63.7,55.4,61.7c17.5-21,43.9-34.3,73.4-34.3
|
||||||
|
c7.8,0,15.5,0.9,22.8,2.7l3.8,2.5C164.2,38.3,167.8,43.1,168.4,49.9"/>
|
||||||
|
<path class="st0" d="M209.6,71.8c8.7,13.7,14,29.8,14.7,47c-3.9-2.6-8.2-5.3-13.1-8.3C214,95.5,212.6,82.5,209.6,71.8"/>
|
||||||
|
<path class="st0" d="M206.8,144.8c4.5,2.1,9,4.5,13.2,6.7c-2.2,6.9-5.1,13.5-8.7,19.6c-0.6-4-2.2-7.4-3.5-10.3
|
||||||
|
c-2.5-5.6-4.2-9.3-1.2-15.7C206.8,145,206.8,144.9,206.8,144.8"/>
|
||||||
|
<path class="st0" d="M207.2,125.1c5.4,2.9,11.3,6.2,16.6,9.2c-0.1,1.2-0.3,2.4-0.5,3.7c-5.7-3.5-11.8-7-17.2-9.9
|
||||||
|
C206.4,127.1,206.8,126.1,207.2,125.1"/>
|
||||||
|
<path class="st1" d="M185.2,205.6c41.5-36.8,4.9-39.5,15.9-63.1c-14,12.6-44,47.2-18,64.1C164.7,168.9,207.4,157.8,185.2,205.6"/>
|
||||||
|
<path class="st2" d="M198.1,130.8c9.6,5,24.5,13.5,33.5,20c-9.4-5.4-24.7-13-34.9-17.4c-2,3.9-4.3,7.8-7,11.8
|
||||||
|
c-10.3,15-36.7,44.6-12.5,61.4c-51.2-7.1-57.1-49.2-33.1-86.7c0.8-1.2,1.5-2.4,2.3-3.6c-62.2-16.2-119.6-14.7-119.8,0.3
|
||||||
|
c-14.3-6.8-26.2-20.1-2.1-21.8c9.6-0.7,65.2-2.4,125.1,16.2c3-4.9,5.8-9.6,8.2-13.9c-63.3-21.6-124.4-24-125.5-8.6
|
||||||
|
C18.4,81,7.4,67,31.6,66.7c9.9-0.1,68.5,1.5,129.1,25.3c21.2-39.5,16.4-52.7-2-64.6c21.6,1.5,57.2,37,45.6,86.1
|
||||||
|
c8.8,5.4,22.8,14.1,31,20.7c-8.4-5.5-22.6-13.3-31.7-17.9C202.2,121.1,200.4,125.9,198.1,130.8"/>
|
||||||
|
<path class="st2" d="M185.2,205.6c41.5-36.8,4.9-39.5,15.9-63.1c-14,12.6-44,47.2-18,64.1C164.7,168.9,207.4,157.8,185.2,205.6"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
142
src/assets/lang/de-CH.yaml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
language: Sprach
|
||||||
|
prename: Vorname
|
||||||
|
lastName: Nachname
|
||||||
|
birthday: Geburtstag
|
||||||
|
email: Email
|
||||||
|
group: Gruppe
|
||||||
|
groups: Gruppen
|
||||||
|
age: Auter
|
||||||
|
address: Adresse
|
||||||
|
town: Ort
|
||||||
|
zipCode: Postleitzau
|
||||||
|
phone: Telefon
|
||||||
|
responsible: Verantwortlech
|
||||||
|
firstVisit: Erste Bsuech
|
||||||
|
lastVisit: Letscht Bsuech
|
||||||
|
search: Suechi
|
||||||
|
noDataAvailable: Keni Date
|
||||||
|
importCSV: importier CSV
|
||||||
|
exportCSV: exportier CSV
|
||||||
|
selectMemberOptions: Wähle Mitglieder Optione
|
||||||
|
addNewMember: Neues Mitglied
|
||||||
|
csvOptions: CSV Optionen
|
||||||
|
delete: Lösche
|
||||||
|
edit: Editiere
|
||||||
|
confirm: Bestätige
|
||||||
|
cancel: Abbreche
|
||||||
|
doYouWantToDelete: Wosch dä Iitrag lösche
|
||||||
|
close: Zu tue
|
||||||
|
loading: lade
|
||||||
|
recordsPerPage: 'Iiträg pro Site:'
|
||||||
|
recordSelected: Iiträg usgwäut
|
||||||
|
selected: Usgwäut
|
||||||
|
settings: Iistellige
|
||||||
|
databaseName: Datebank Name
|
||||||
|
token: Schlüssu
|
||||||
|
login: Amäude
|
||||||
|
logout: Abmäude
|
||||||
|
user: Benutzer
|
||||||
|
password: Passwort
|
||||||
|
isRequired: isch erforderlich
|
||||||
|
colors: Farbe
|
||||||
|
primaryColor: Primär Farb
|
||||||
|
primaryColorText: Primär Text Farb
|
||||||
|
secondaryColor: Sekondär Farb
|
||||||
|
secondaryColorText: Sekondär Text Farb
|
||||||
|
database: Datebank
|
||||||
|
general: Augemein
|
||||||
|
setColors: Setz Farbe
|
||||||
|
icon: Ikon
|
||||||
|
resetColors: Farbe zrügsetze
|
||||||
|
save: Spichere
|
||||||
|
users: Benutzer
|
||||||
|
roles: Rollen
|
||||||
|
name: Name
|
||||||
|
role: Rolle
|
||||||
|
addNewUser: Füeg neue Benutzer hinzue
|
||||||
|
expires: Ablouf
|
||||||
|
selectUserOptions: Wähle Benutzer Optione
|
||||||
|
prenameIsRequired: Vorname isch erforderlich
|
||||||
|
lastNameIsRequired: Nachname isch erforderlich
|
||||||
|
birthdayIsRequired: Geburtstag isch erforderlich
|
||||||
|
userIsRequired: Benutzer isch erforderlich
|
||||||
|
emailIsRequired: Email isch erforderlich
|
||||||
|
roleIsRequired: Rolle isch erforderlich
|
||||||
|
permissions: Recht
|
||||||
|
selectRoleOptions: Wähle Roue Optione
|
||||||
|
selectEventOptions: Wähle Verastautigs Optione
|
||||||
|
addNewRole: Füeg neui Roue hinzue
|
||||||
|
addNewEvent: Füeg neui Verastautig hinzue
|
||||||
|
veryWeak: sehr Schwach
|
||||||
|
weak: Schwach
|
||||||
|
fair: So so
|
||||||
|
good: Guet
|
||||||
|
strong: Stark
|
||||||
|
passwordIsRequired: Password isch erforderlich
|
||||||
|
passwordTooShort: Ds Passwort mues mindestens 8 Zeiche läng si
|
||||||
|
passwordNeedsUppercase: Ds Passwort mues mindestens ei Grossbuechstabe enthaute
|
||||||
|
passwordNeedsLowercase: Ds Passwort mues mindestens ei Chlibuechstabe enthaute
|
||||||
|
passwordNeedsNumber: Ds Passwort mues mindestens ei Zau enthaute
|
||||||
|
passwordNeedsSpecial: Ds Passwort mues mindestens eis Sonderzeiche enthaute
|
||||||
|
passwordDoNotMatch: Passwörter stimme nid überei
|
||||||
|
read: Lese
|
||||||
|
write: Schribe
|
||||||
|
userSettings: Benutzer Istellige
|
||||||
|
members: Mitglider
|
||||||
|
attendanceTable: Anweseheits Tabelle
|
||||||
|
excursionTable: Usflugs Tabelle
|
||||||
|
updated: aktualisiert
|
||||||
|
events: Verastautige
|
||||||
|
eventNameIsRequired: Verastautigssname isch erforderlich
|
||||||
|
eventName: Verastautigssname
|
||||||
|
attendees: Teilnähmer
|
||||||
|
now: Jetzt
|
||||||
|
addToEvent: Füge zu Veranstautig
|
||||||
|
add: Hinzuefüege
|
||||||
|
event: Verastautig
|
||||||
|
dateAndTime: Datum und Zyt
|
||||||
|
count: Anzau
|
||||||
|
selectAttendeesOptions: Wähle Teilnehmer Optionen
|
||||||
|
addNewAttendees: Füeg neue Teilnehmer hinzue
|
||||||
|
notAllRequiredFieldsFilled: Nid aui erforderliche Felder usgfüet
|
||||||
|
memberUpdated: Mitglied aktualisiert
|
||||||
|
membersUpdated: Mitglieder aktualisiert
|
||||||
|
deleteAttendee: Teilnehmer entfernt
|
||||||
|
deleteAttendees: Teilnehmer entfernt
|
||||||
|
deleteRoles: Rolen entfernt
|
||||||
|
attendeeAdded: Teilnämer hinzuegfüegt
|
||||||
|
attendeesAdded: Teilnämer hinzuegfüegt
|
||||||
|
eventAdded: Verastautig hinzuegfüegt
|
||||||
|
userUpdated: Benutzer aktualisiert
|
||||||
|
selectResponsibleOptions: Wähle Verantwortliche Optionen
|
||||||
|
addNewResponsible: Füeg neue Verantwortliche hinzue
|
||||||
|
responsibleAdded: Veratwortläche hinzuegfüegt
|
||||||
|
responsiblesAdded: Veratwortläche hinzuegfüegt
|
||||||
|
deleteResponsible: Veratwortläche entfernt
|
||||||
|
deleteResponsibles: Veratwortläche entfernt
|
||||||
|
expiration: Ablauf
|
||||||
|
never: Nie
|
||||||
|
responsibles: Verantwortliche
|
||||||
|
comment: Bemerkung
|
||||||
|
dark_mode: Dunkel-Modus
|
||||||
|
light_mode: Hell-Modus
|
||||||
|
import: Import
|
||||||
|
export: Export
|
||||||
|
changePassword: Passwort ändere
|
||||||
|
noneAttendees: Fählendi Telnähmer
|
||||||
|
addNewgroup: Neui Gruppe
|
||||||
|
selectgroupOptions: Wähle Gruppe Optionen
|
||||||
|
groupNameIsRequired: Gruppename isch erforderlich
|
||||||
|
groupName: Gruppename
|
||||||
|
filterByColumn: Spaltenfilter
|
||||||
|
filterByColumnValue: Spaltenwerte
|
||||||
|
saveAsDefault: Aus Standard spichere
|
||||||
|
day: Tag
|
||||||
|
Monday: Mäntig
|
||||||
|
Tuesday: Zistig
|
||||||
|
Wednesday: Mittwuch
|
||||||
|
Thursday: Donstig
|
||||||
|
Friday: Fritig
|
||||||
|
Saturday: Samstig
|
||||||
|
Sunday: Suntig
|
||||||
|
currentPassword: Aktuelles Passwort
|
||||||
142
src/assets/lang/de-DE.yaml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
language: Sprache
|
||||||
|
prename: Vorname
|
||||||
|
lastName: Nachname
|
||||||
|
birthday: Geburtstag
|
||||||
|
email: Email
|
||||||
|
group: Gruppe
|
||||||
|
groups: Gruppen
|
||||||
|
age: Alter
|
||||||
|
address: Adresse
|
||||||
|
town: Ort
|
||||||
|
zipCode: Postleitzahl
|
||||||
|
phone: Telefon
|
||||||
|
responsible: Verantwortlich
|
||||||
|
firstVisit: Erster Besuch
|
||||||
|
lastVisit: Letzter Besuch
|
||||||
|
search: Suche
|
||||||
|
noDataAvailable: Keine Daten
|
||||||
|
importCSV: importiere CSV
|
||||||
|
exportCSV: exportiere CSV
|
||||||
|
selectMemberOptions: Wähle Mitglieder Optionen
|
||||||
|
addNewMember: Neues Mitglied
|
||||||
|
csvOptions: CSV Optionen
|
||||||
|
delete: Löschen
|
||||||
|
edit: Editieren
|
||||||
|
confirm: Bestätigen
|
||||||
|
cancel: Abbrechen
|
||||||
|
doYouWantToDelete: Willst du folgenden Eintrag löschen
|
||||||
|
close: Schliessen
|
||||||
|
loading: laden
|
||||||
|
recordsPerPage: 'Einträge pro Seite:'
|
||||||
|
recordSelected: Eintrag ausgewählt
|
||||||
|
selected: Ausgewählt
|
||||||
|
settings: Einstellungen
|
||||||
|
databaseName: Datenbank Name
|
||||||
|
token: Schlüssel
|
||||||
|
login: Anmelden
|
||||||
|
logout: Abmelden
|
||||||
|
user: Benutzer
|
||||||
|
password: Passwort
|
||||||
|
isRequired: ist erforderlich
|
||||||
|
colors: Farben
|
||||||
|
primaryColor: Primär Farbe
|
||||||
|
primaryColorText: Primär Text Farbe
|
||||||
|
secondaryColor: Sekondär Farbe
|
||||||
|
secondaryColorText: Sekondär Text Farbe
|
||||||
|
database: Datenbank
|
||||||
|
general: Allgemein
|
||||||
|
setColors: Setze Farben
|
||||||
|
icon: Ikon
|
||||||
|
resetColors: Farben zurücksetzen
|
||||||
|
save: Speichern
|
||||||
|
users: Benutzer
|
||||||
|
roles: Rollen
|
||||||
|
name: Name
|
||||||
|
role: Rolle
|
||||||
|
addNewUser: Füge neuen Benutzer hinzu
|
||||||
|
expires: Ablauf
|
||||||
|
selectUserOptions: Wähle Benutzer Optionen
|
||||||
|
prenameIsRequired: Vorname ist erforderlich
|
||||||
|
lastNameIsRequired: Nachname ist erforderlich
|
||||||
|
birthdayIsRequired: Geburtstag ist erforderlich
|
||||||
|
userIsRequired: Benutzer ist erforderlich
|
||||||
|
emailIsRequired: Email ist erforderlich
|
||||||
|
roleIsRequired: Rolle ist erforderlich
|
||||||
|
permissions: Rechte
|
||||||
|
selectRoleOptions: Wähle Rollen Optionen
|
||||||
|
selectEventOptions: Wähle Veranstaltungs Optionen
|
||||||
|
addNewRole: Füge neue Rolle hinzu
|
||||||
|
addNewEvent: Füeg neue Veranstaltung hinzu
|
||||||
|
veryWeak: sehr Schwach
|
||||||
|
weak: Schwach
|
||||||
|
fair: Ausreichend
|
||||||
|
good: Gut
|
||||||
|
strong: Stark
|
||||||
|
passwordIsRequired: Password ist erforderlich
|
||||||
|
passwordTooShort: Das Passwort muss mindestens 8 Zeichen lang sein
|
||||||
|
passwordNeedsUppercase: Das Passwort muss mindestens einen Großbuchstaben enthalten
|
||||||
|
passwordNeedsLowercase: Das Passwort muss mindestens einen Kleinbuchstaben enthalten
|
||||||
|
passwordNeedsNumber: Das Passwort muss mindestens eine Zahl enthalten
|
||||||
|
passwordNeedsSpecial: Das Passwort muss mindestens ein Sonderzeichen enthalten
|
||||||
|
passwordDoNotMatch: Passwörter stimmen nicht überein
|
||||||
|
read: Lesen
|
||||||
|
write: Schreiben
|
||||||
|
userSettings: Benutzer Einstellungen
|
||||||
|
members: Mitglieder
|
||||||
|
attendanceTable: Anwesenheits Tabelle
|
||||||
|
excursionTable: Ausflugs Tabelle
|
||||||
|
updated: aktualisiert
|
||||||
|
events: Veranstaltungen
|
||||||
|
eventNameIsRequired: Veranstaltungsname ist erforderlich
|
||||||
|
eventName: Veranstaltungsname
|
||||||
|
attendees: Teilnehmer
|
||||||
|
now: Jetzt
|
||||||
|
addToEvent: Füge zu Veranstaltung
|
||||||
|
add: Hinzufügen
|
||||||
|
event: Veranstaltung
|
||||||
|
dateAndTime: Datum und Zeit
|
||||||
|
count: Anzahl
|
||||||
|
selectAttendeesOptions: Wähle Teilnehmer Optionen
|
||||||
|
addNewAttendees: Füge neuen Teilnehmer hinzu
|
||||||
|
notAllRequiredFieldsFilled: Nicht alle erforderlichen Felder ausgefüllt
|
||||||
|
memberUpdated: Mitglied aktualisiert
|
||||||
|
membersUpdated: Mitglieder aktualisiert
|
||||||
|
deleteAttendee: Teilnehmer entfernt
|
||||||
|
deleteAttendees: Teilnehmer entfernt
|
||||||
|
deleteRoles: Rolen entfernt
|
||||||
|
attendeeAdded: Teilnehmer hinzugefügt
|
||||||
|
attendeesAdded: Teilnehmer hinzugefügt
|
||||||
|
eventAdded: Veranstaltung hinzugefügt
|
||||||
|
userUpdated: Benutzer aktualisiert
|
||||||
|
selectResponsibleOptions: Wähle Verantwortliche Optionen
|
||||||
|
addNewResponsible: Füge neuen Verantwortlichen hinzu
|
||||||
|
responsibleAdded: Veratwortläche hinzuegfüegt
|
||||||
|
responsiblesAdded: Verantwortliche hinzuegfüegt
|
||||||
|
deleteResponsible: Verantwortliche entfernt
|
||||||
|
deleteResponsibles: Verantwortliche entfernt
|
||||||
|
expiration: Ablauf
|
||||||
|
never: Nie
|
||||||
|
responsibles: Verantwortliche
|
||||||
|
comment: Bemerkung
|
||||||
|
dark_mode: Dunkel-Modus
|
||||||
|
light_mode: Hell-Modus
|
||||||
|
import: Import
|
||||||
|
export: Export
|
||||||
|
changePassword: Passwort ändern
|
||||||
|
noneAttendees: Fehlende Teilnehmer
|
||||||
|
addNewgroup: Neue Gruppe
|
||||||
|
selectgroupOptions: Wähle Gruppen Optionen
|
||||||
|
groupNameIsRequired: Gruppenname ist erforderlich
|
||||||
|
groupName: Gruppenname
|
||||||
|
filterByColumn: Spaltenfilter
|
||||||
|
filterByColumnValue: Spaltenwerte
|
||||||
|
saveAsDefault: Als Standard speichern
|
||||||
|
day: Tag
|
||||||
|
Monday: Montag
|
||||||
|
Tuesday: Dienstag
|
||||||
|
Wednesday: Mittwoch
|
||||||
|
Thursday: Donnerstag
|
||||||
|
Friday: Freitag
|
||||||
|
Saturday: Samstag
|
||||||
|
Sunday: Sonntag
|
||||||
|
currentPassword: Aktuelles Passwort
|
||||||
142
src/assets/lang/en-US.yaml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
language: Language
|
||||||
|
prename: Prename
|
||||||
|
lastName: Name
|
||||||
|
birthday: Birthday
|
||||||
|
email: Email
|
||||||
|
group: Group
|
||||||
|
groups: Groups
|
||||||
|
age: Age
|
||||||
|
address: Address
|
||||||
|
town: Town
|
||||||
|
zipCode: Zip Code
|
||||||
|
phone: Phone
|
||||||
|
responsible: Responsible
|
||||||
|
firstVisit: First Visit
|
||||||
|
lastVisit: Last Visit
|
||||||
|
search: Search
|
||||||
|
noDataAvailable: No data available
|
||||||
|
importCSV: Import CSV
|
||||||
|
exportCSV: Export CSV
|
||||||
|
selectMemberOptions: Select Member Options
|
||||||
|
addNewMember: Add new Member
|
||||||
|
csvOptions: CSV Options
|
||||||
|
delete: Delete
|
||||||
|
edit: Edit
|
||||||
|
confirm: Confirm
|
||||||
|
cancel: Cancel
|
||||||
|
doYouWantToDelete: Do you want to delete
|
||||||
|
close: Close
|
||||||
|
loading: loading
|
||||||
|
recordsPerPage: 'Records per page:'
|
||||||
|
recordSelected: record selected
|
||||||
|
selected: Selected
|
||||||
|
settings: Settings
|
||||||
|
databaseName: Database Name
|
||||||
|
token: Token
|
||||||
|
login: Login
|
||||||
|
logout: Logout
|
||||||
|
user: User
|
||||||
|
password: Password
|
||||||
|
isRequired: is required
|
||||||
|
colors: Colors
|
||||||
|
primaryColor: Primary Color
|
||||||
|
primaryColorText: Primary Text Color
|
||||||
|
secondaryColor: Secondary Color
|
||||||
|
secondaryColorText: Secondary Text Color
|
||||||
|
database: Database
|
||||||
|
general: General
|
||||||
|
setColors: Set Colors
|
||||||
|
icon: Icon
|
||||||
|
resetColors: Reset Colors
|
||||||
|
save: Save
|
||||||
|
users: Users
|
||||||
|
roles: Roles
|
||||||
|
name: Name
|
||||||
|
role: Role
|
||||||
|
addNewUser: Add new User
|
||||||
|
expires: Expires
|
||||||
|
selectUserOptions: Select User Options
|
||||||
|
prenameIsRequired: Fist Name is required
|
||||||
|
lastNameIsRequired: Last Name is required
|
||||||
|
birthdayIsRequired: Birthday is required
|
||||||
|
userIsRequired: User is required
|
||||||
|
emailIsRequired: Email is required
|
||||||
|
roleIsRequired: Role is required
|
||||||
|
permissions: Permissions
|
||||||
|
selectRoleOptions: Select Role Options
|
||||||
|
selectEventOptions: Select Event Options
|
||||||
|
addNewRole: Add new Role
|
||||||
|
addNewEvent: Add new Event
|
||||||
|
veryWeak: very Weak
|
||||||
|
weak: Weak
|
||||||
|
fair: Fair
|
||||||
|
good: Good
|
||||||
|
strong: Strong
|
||||||
|
passwordIsRequired: Password is required
|
||||||
|
passwordTooShort: Password must be at least 8 characters long
|
||||||
|
passwordNeedsUppercase: Password must contain at least one uppercase letter
|
||||||
|
passwordNeedsLowercase: Password must contain at least one lowercase letter
|
||||||
|
passwordNeedsNumber: Password must contain at least one number
|
||||||
|
passwordNeedsSpecial: Password must contain at least one special character
|
||||||
|
passwordDoNotMatch: Password do not match
|
||||||
|
read: Read
|
||||||
|
write: Write
|
||||||
|
userSettings: User Settings
|
||||||
|
members: Members
|
||||||
|
attendanceTable: Attendance Table
|
||||||
|
excursionTable: Excursion Table
|
||||||
|
updated: updated
|
||||||
|
events: Events
|
||||||
|
eventNameIsRequired: Eventname is required
|
||||||
|
eventName: Eventname
|
||||||
|
attendees: Attendees
|
||||||
|
now: Now
|
||||||
|
addToEvent: Add to event
|
||||||
|
add: Add
|
||||||
|
event: Event
|
||||||
|
dateAndTime: Date and Time
|
||||||
|
count: Count
|
||||||
|
selectAttendeesOptions: Select Attendees Options
|
||||||
|
addNewAttendees: Add new Attendee
|
||||||
|
notAllRequiredFieldsFilled: Not all required fields are filled in
|
||||||
|
memberUpdated: Member updated
|
||||||
|
membersUpdated: Members updated
|
||||||
|
deleteAttendee: Attendee deleted
|
||||||
|
deleteAttendees: Attendees deleted
|
||||||
|
deleteRoles: Roles deleted
|
||||||
|
attendeeAdded: Attendee added
|
||||||
|
attendeesAdded: Attendees added
|
||||||
|
eventAdded: Event added
|
||||||
|
userUpdated: User updated
|
||||||
|
selectResponsibleOptions: Select Responsible Options
|
||||||
|
addNewResponsible: Add Responsible
|
||||||
|
responsibleAdded: Responsible hinzuegfüegt
|
||||||
|
responsiblesAdded: Responsibles hinzuegfüegt
|
||||||
|
deleteResponsible: Responsible deleted
|
||||||
|
deleteResponsibles: Responsibles deleted
|
||||||
|
expiration: Expiration
|
||||||
|
never: Never
|
||||||
|
responsibles: Responsibles
|
||||||
|
comment: Comment
|
||||||
|
dark_mode: Dark-Mode
|
||||||
|
light_mode: Light-Mode
|
||||||
|
import: Import
|
||||||
|
export: Export
|
||||||
|
changePassword: change Password
|
||||||
|
noneAttendees: Missing Attendees
|
||||||
|
addNewgroup: New Group
|
||||||
|
selectgroupOptions: Select Group Options
|
||||||
|
groupNameIsRequired: Groupname is required
|
||||||
|
groupName: Groupname
|
||||||
|
filterByColumn: Columnfilter
|
||||||
|
filterByColumnValue: Columnvalues
|
||||||
|
saveAsDefault: Save a Default
|
||||||
|
day: Day
|
||||||
|
Monday: Monday
|
||||||
|
Tuesday: Tuesday
|
||||||
|
Wednesday: Wednesday
|
||||||
|
Thursday: Thursday
|
||||||
|
Friday: Friday
|
||||||
|
Saturday: Saturday
|
||||||
|
Sunday: Sunday
|
||||||
|
currentPassword: Current Password
|
||||||
3
src/assets/lang/es-ES.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
hello: hola
|
||||||
|
goodbye: adios
|
||||||
|
language: Lengua
|
||||||
1
src/assets/pottershouse.svg
Normal file
|
After Width: | Height: | Size: 233 KiB |
15
src/assets/quasar-logo-vertical.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
|
||||||
|
<path
|
||||||
|
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
|
||||||
|
<path fill="#050A14"
|
||||||
|
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
|
||||||
|
<path fill="#00B4FF"
|
||||||
|
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
|
||||||
|
<path fill="#00B4FF"
|
||||||
|
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
|
||||||
|
<path fill="#050A14"
|
||||||
|
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
|
||||||
|
<path fill="#00B4FF"
|
||||||
|
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
|
||||||
|
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
0
src/boot/.gitkeep
Normal file
29
src/boot/auth.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { appApi } from './axios';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import { useLogin } from 'src/vueLib/login/useLogin';
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
|
||||||
|
export default boot(async ({ app }) => {
|
||||||
|
app.use(pinia);
|
||||||
|
const useStore = useUserStore();
|
||||||
|
const login = useLogin();
|
||||||
|
|
||||||
|
await appApi
|
||||||
|
.get('/login/me')
|
||||||
|
.then((resp) => {
|
||||||
|
useStore
|
||||||
|
.setUser({
|
||||||
|
id: resp.data.id,
|
||||||
|
username: resp.data.username,
|
||||||
|
role: { role: resp.data.role, permissions: [] },
|
||||||
|
})
|
||||||
|
.catch((err) => console.error(err));
|
||||||
|
login.refresh().catch((err) => console.error(err));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
login.logout().catch((err) => console.error(err));
|
||||||
|
});
|
||||||
|
});
|
||||||
139
src/boot/axios.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useLogin } from 'src/vueLib/login/useLogin';
|
||||||
|
|
||||||
|
const host = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
|
// Create axios instance
|
||||||
|
export const appApi: AxiosInstance = axios.create({
|
||||||
|
baseURL: host,
|
||||||
|
timeout: 10000,
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface RetryRequestConfig extends AxiosRequestConfig {
|
||||||
|
_retry?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noRefreshEndpoints = ['/login', '/login/refresh', '/logout'];
|
||||||
|
|
||||||
|
// ========= Refresh Queue Handling ========= //
|
||||||
|
let isRefreshing = false;
|
||||||
|
|
||||||
|
interface FailedRequest {
|
||||||
|
resolve: (value?: unknown) => void;
|
||||||
|
reject: (reason?: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let failedQueue: FailedRequest[] = [];
|
||||||
|
|
||||||
|
const processQueue = (error: Error | null): void => {
|
||||||
|
failedQueue.forEach((prom) => {
|
||||||
|
if (error) prom.reject(error);
|
||||||
|
else prom.resolve();
|
||||||
|
});
|
||||||
|
failedQueue = [];
|
||||||
|
};
|
||||||
|
// ========================================= //
|
||||||
|
|
||||||
|
appApi.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => response,
|
||||||
|
|
||||||
|
async (error: AxiosError<unknown, RetryRequestConfig>): Promise<AxiosResponse> => {
|
||||||
|
const { refresh, logout } = useLogin();
|
||||||
|
const originalRequest = error.config as RetryRequestConfig | undefined;
|
||||||
|
|
||||||
|
// Skip refresh for login/logout endpoints
|
||||||
|
if (
|
||||||
|
!originalRequest ||
|
||||||
|
noRefreshEndpoints.some((url) => originalRequest.url?.includes(url ?? ''))
|
||||||
|
) {
|
||||||
|
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unauthorized responses
|
||||||
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
|
const data = error.response?.data;
|
||||||
|
const serverMessage =
|
||||||
|
typeof data === 'object' && data !== null && 'message' in data
|
||||||
|
? (data as { message: string }).message
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (['no refresh token', 'is expired'].some((msg) => serverMessage?.includes(msg))) {
|
||||||
|
console.warn('[Axios] No refresh token — logging out');
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
} catch (logoutErr) {
|
||||||
|
console.error('[Axios] Logout failed:', logoutErr);
|
||||||
|
}
|
||||||
|
throw new Error('Session expired: no refresh token');
|
||||||
|
}
|
||||||
|
if (isRefreshing) {
|
||||||
|
// Wait until refresh completes
|
||||||
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
||||||
|
failedQueue.push({
|
||||||
|
resolve: () => {
|
||||||
|
void appApi(originalRequest).then(resolve).catch(reject);
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshed = await refresh().catch(() => false);
|
||||||
|
processQueue(null);
|
||||||
|
|
||||||
|
if (refreshed) {
|
||||||
|
// Token refreshed successfully → retry request
|
||||||
|
return appApi(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh returned false → logout
|
||||||
|
console.warn('[Axios] Refresh returned false, logging out');
|
||||||
|
await logout();
|
||||||
|
throw new Error('Token refresh failed');
|
||||||
|
} catch (err) {
|
||||||
|
const e = err instanceof Error ? err : new Error(String(err));
|
||||||
|
console.error('[Axios] Token refresh failed:', e.message);
|
||||||
|
|
||||||
|
// Always logout, even if refresh throws
|
||||||
|
try {
|
||||||
|
await logout();
|
||||||
|
} catch (logoutErr) {
|
||||||
|
console.error('[Axios] Logout failed after token refresh error:', logoutErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
processQueue(e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a 401 — rethrow as Error
|
||||||
|
|
||||||
|
let msg = '';
|
||||||
|
if (error && (error as AxiosError).isAxiosError) {
|
||||||
|
const axiosError = error as AxiosError<{ message?: string }>;
|
||||||
|
msg = axiosError.response?.data?.message ?? axiosError.message;
|
||||||
|
} else {
|
||||||
|
msg = error instanceof Error ? error.message : JSON.stringify(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(msg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ======== Boot registration for Quasar ======== //
|
||||||
|
export default boot(({ app }) => {
|
||||||
|
app.config.globalProperties.$axios = axios;
|
||||||
|
app.config.globalProperties.$appApi = appApi;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { axios };
|
||||||
55
src/boot/lang.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import { getLocalLanguage } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
export const lang = [];
|
||||||
|
|
||||||
|
const systemLocale = navigator.language || 'en-US';
|
||||||
|
|
||||||
|
const messages = {};
|
||||||
|
const modules = import.meta.glob('src/assets/lang/*.yaml', {
|
||||||
|
eager: true,
|
||||||
|
import: 'default',
|
||||||
|
query: '?raw',
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const path in modules) {
|
||||||
|
const raw = modules[path];
|
||||||
|
const parsed = yaml.load(raw);
|
||||||
|
|
||||||
|
// Extract the file name (e.g., "en.yaml" → "en")
|
||||||
|
const locale = path.split('/').pop().replace('.yaml', '');
|
||||||
|
|
||||||
|
lang.push(locale);
|
||||||
|
messages[locale] = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocale(desiredLocale) {
|
||||||
|
if (messages[desiredLocale]) return desiredLocale;
|
||||||
|
|
||||||
|
const baseLang = desiredLocale.split('-')[0];
|
||||||
|
// exact base match (e.g. en)
|
||||||
|
if (messages[baseLang]) return baseLang;
|
||||||
|
|
||||||
|
// first locale starting with that base (e.g. en-US, en-GB)
|
||||||
|
const partialMatch = Object.keys(messages).find((l) => l.startsWith(baseLang));
|
||||||
|
if (partialMatch) return partialMatch;
|
||||||
|
|
||||||
|
// fallback to English or the first available
|
||||||
|
return messages['en'] ? 'en' : Object.keys(messages)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedLocale = resolveLocale(getLocalLanguage() || systemLocale);
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // Composition API mode
|
||||||
|
locale: selectedLocale,
|
||||||
|
fallbackLocale: resolveLocale(selectedLocale),
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ({ app }) => {
|
||||||
|
app.use(i18n);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { i18n };
|
||||||
35
src/boot/quasar-global.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { setQuasarInstance } from 'src/vueLib/utils/globalQ';
|
||||||
|
import { setRouterInstance } from 'src/vueLib/utils/globalRouter';
|
||||||
|
import { databaseName, logo, appName } from 'src/vueLib/models/settings';
|
||||||
|
import { Dark } from 'quasar';
|
||||||
|
import { getLocalDarkMode, getLocalSettings } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
export default boot(({ app, router }) => {
|
||||||
|
setRouterInstance(router); // store router for global access
|
||||||
|
const $q = app.config.globalProperties.$q;
|
||||||
|
setQuasarInstance($q);
|
||||||
|
|
||||||
|
Dark.set(getLocalDarkMode());
|
||||||
|
|
||||||
|
const settings = getLocalSettings();
|
||||||
|
|
||||||
|
if (settings.icon) {
|
||||||
|
logo.value = settings.icon;
|
||||||
|
}
|
||||||
|
if (settings.appName) {
|
||||||
|
appName.value = settings.appName;
|
||||||
|
}
|
||||||
|
databaseName.value = settings.databaseName ?? databaseName.value;
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty('--q-primary', settings.primaryColor ?? '#1976d2');
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--q-primary-text',
|
||||||
|
settings.primaryColorText ?? '#ffffff',
|
||||||
|
);
|
||||||
|
document.documentElement.style.setProperty('--q-secondary', settings.secondaryColor ?? '#26a69a');
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
'--q-secondary-text',
|
||||||
|
settings.secondaryColorText ?? '#ffffff',
|
||||||
|
);
|
||||||
|
});
|
||||||
43
src/boot/restore-route.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { boot } from 'quasar/wrappers';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import { appApi } from './axios';
|
||||||
|
import { getLocalLastRoute, setLocalLastRoute } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
export default boot(async ({ router }) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
// load user
|
||||||
|
try {
|
||||||
|
const { data } = await appApi.get('/login/me');
|
||||||
|
|
||||||
|
data.role.role = data.role;
|
||||||
|
await userStore.setUser(data);
|
||||||
|
} catch {
|
||||||
|
/* ignore error */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore logic after router is ready but before navigation
|
||||||
|
router.isReady().then(() => {
|
||||||
|
const lastRoute = getLocalLastRoute();
|
||||||
|
const currentPath = router.currentRoute.value.fullPath;
|
||||||
|
|
||||||
|
// Restore only if:
|
||||||
|
// - we’re on root ("/" or "/#/"), and
|
||||||
|
// - a last route exists, and
|
||||||
|
// - the user is authenticated
|
||||||
|
if (
|
||||||
|
lastRoute &&
|
||||||
|
['/', '/#/', '/#/index.html'].includes(currentPath) &&
|
||||||
|
userStore.isAuthenticated
|
||||||
|
) {
|
||||||
|
router.replace(lastRoute).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the route after every successful navigation
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// Don't save login page as "last route"
|
||||||
|
if (to.path !== '/login' && to.path !== '/') {
|
||||||
|
setLocalLastRoute(to.fullPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
124
src/components/AddToEvent.vue
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame ref="dialog" :header-title="localTitle">
|
||||||
|
<div class="row justify-center">
|
||||||
|
<SearchableSelect
|
||||||
|
class="q-mx-xl"
|
||||||
|
dense
|
||||||
|
autofocus
|
||||||
|
:label="$t('event')"
|
||||||
|
filled
|
||||||
|
:options="events"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
v-model="selected"
|
||||||
|
@keyup.enter="addAttendees"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
></SearchableSelect>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="addAttendees">{{ localTitle }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
<EditAllDialog
|
||||||
|
ref="newEventRef"
|
||||||
|
v-on:update="
|
||||||
|
(val) => {
|
||||||
|
resolveNewEvent(val);
|
||||||
|
NotifyResponse($t('memberUpdated'));
|
||||||
|
}
|
||||||
|
"
|
||||||
|
></EditAllDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
import type { Event, Events } from 'src/vueLib/models/event';
|
||||||
|
import type { Members } from 'src/vueLib/models/member';
|
||||||
|
import EditAllDialog from 'src/components/EventEditAllDialog.vue';
|
||||||
|
import { useAttendeesTable } from 'src/vueLib/tables/attendees/AttendeesTable';
|
||||||
|
import { useEventTable } from 'src/vueLib/tables/events/EventsTable';
|
||||||
|
import SearchableSelect from 'src/vueLib/general/SearchableSelect .vue';
|
||||||
|
|
||||||
|
const dialog = ref();
|
||||||
|
const newEventRef = ref();
|
||||||
|
const localTitle = ref('');
|
||||||
|
const events = ref<Events>([{ id: -1, name: i18n.global.t('addNewEvent'), attendees: [] }]);
|
||||||
|
const selected = ref<Event>({ id: -1, name: i18n.global.t('addNewEvent'), attendees: [] });
|
||||||
|
const localMembers = ref<Members>([]);
|
||||||
|
const { updateAttendees } = useAttendeesTable();
|
||||||
|
const { updateEvents } = useEventTable();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
endpoint: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
queryId: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-event']);
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
function open(title: string, members: Members) {
|
||||||
|
localTitle.value = title;
|
||||||
|
localMembers.value = members;
|
||||||
|
|
||||||
|
events.value = [{ id: -1, name: i18n.global.t('addNewEvent'), attendees: [] }];
|
||||||
|
appApi
|
||||||
|
.get('events')
|
||||||
|
.then((resp) => {
|
||||||
|
events.value.push(...resp.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
});
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addAttendees() {
|
||||||
|
const payload = {
|
||||||
|
id: Number(selected.value),
|
||||||
|
attendees: localMembers.value,
|
||||||
|
};
|
||||||
|
if (selected.value.id == -1) {
|
||||||
|
const event = await addNewEvent();
|
||||||
|
NotifyResponse(i18n.global.t('eventAdded') + ': ' + event.name);
|
||||||
|
payload.id = event.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await appApi
|
||||||
|
.post(props.endpoint, payload)
|
||||||
|
.then(() => {
|
||||||
|
emit('update-event', localMembers.value);
|
||||||
|
if (localMembers.value.length > 1) {
|
||||||
|
NotifyResponse(i18n.global.t('attendeesAdded'));
|
||||||
|
} else {
|
||||||
|
NotifyResponse(i18n.global.t('attendeeAdded'));
|
||||||
|
}
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateAttendees(0);
|
||||||
|
updateEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolveNewEvent!: (value: Event) => void;
|
||||||
|
function addNewEvent(): Promise<Event> {
|
||||||
|
newEventRef.value?.open(null);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolveNewEvent = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
135
src/components/EditOneDialog.vue
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame ref="dialog" :header-title="'Edit ' + localTitle">
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-input autofocus :label="localTitle" filled v-model="value" @keyup.enter="save">
|
||||||
|
<template
|
||||||
|
v-if="['firstVisit', 'lastVisit', 'date', 'expiration'].includes(localField)"
|
||||||
|
v-slot:prepend
|
||||||
|
>
|
||||||
|
<q-icon name="event" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-date v-model="value" mask="YYYY-MM-DD HH:mm:ss">
|
||||||
|
<div class="row items-center justify-end">
|
||||||
|
<q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" />
|
||||||
|
<q-btn
|
||||||
|
v-if="localField"
|
||||||
|
:label="$t('never')"
|
||||||
|
color="primary"
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
@click="value = 'never'"
|
||||||
|
/>
|
||||||
|
<q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat />
|
||||||
|
</div>
|
||||||
|
</q-date>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-if="['firstVisit', 'lastVisit', 'date', 'expiration'].includes(localField)"
|
||||||
|
v-slot:append
|
||||||
|
>
|
||||||
|
<q-icon name="access_time" class="cursor-pointer">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-time with-seconds v-model="value" mask="YYYY-MM-DD HH:mm:ss" format24h>
|
||||||
|
<div class="row items-center justify-end">
|
||||||
|
<q-btn :label="$t('now')" color="primary" no-caps flat @click="setTimeNow" />
|
||||||
|
<q-btn
|
||||||
|
v-if="localField"
|
||||||
|
:label="$t('never')"
|
||||||
|
color="primary"
|
||||||
|
no-caps
|
||||||
|
flat
|
||||||
|
@click="value = 'never'"
|
||||||
|
/>
|
||||||
|
<q-btn no-caps v-close-popup :label="$t('close')" color="primary" flat />
|
||||||
|
</div>
|
||||||
|
</q-time>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import type { Member } from 'src/vueLib/models/member';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
|
||||||
|
const dialog = ref();
|
||||||
|
const localMember = ref();
|
||||||
|
const localTitle = ref('');
|
||||||
|
const localField = ref('');
|
||||||
|
const value = ref('');
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
endpoint: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
function open(label: string, field: string, member: Member) {
|
||||||
|
localTitle.value = label;
|
||||||
|
localField.value = field;
|
||||||
|
localMember.value = member;
|
||||||
|
|
||||||
|
value.value = localMember.value[field];
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
let payload = {};
|
||||||
|
|
||||||
|
if (value.value === localMember.value[localField.value]) {
|
||||||
|
dialog.value.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.endpoint) {
|
||||||
|
localMember.value[localField.value] = value.value;
|
||||||
|
emit('update', localMember.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload = [
|
||||||
|
{
|
||||||
|
id: localMember.value.id,
|
||||||
|
[localField.value]: value.value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
appApi
|
||||||
|
.post(props.endpoint, payload)
|
||||||
|
.then(() => {
|
||||||
|
emit('update');
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTimeNow() {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-based
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
value.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
95
src/components/EventEditAllDialog.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="newEvent ? $t('addNewEvent') : 'Edit ' + localEvent.name"
|
||||||
|
:height="250"
|
||||||
|
:width="500"
|
||||||
|
>
|
||||||
|
<q-form ref="form">
|
||||||
|
<div class="row justify-center q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5 required"
|
||||||
|
:label="$t('eventName')"
|
||||||
|
filled
|
||||||
|
:rules="[(val) => !!val || $t('eventNameIsRequired')]"
|
||||||
|
v-model="localEvent.name"
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="save"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import type { Event } from 'src/vueLib/models/event';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const dialog = ref();
|
||||||
|
const form = ref();
|
||||||
|
const newEvent = ref(false);
|
||||||
|
const localEvent = ref<Event>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
attendees: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
function open(Event: Event | null) {
|
||||||
|
if (Event === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Event !== null) {
|
||||||
|
localEvent.value = { ...Event };
|
||||||
|
newEvent.value = Event.id === 0;
|
||||||
|
} else {
|
||||||
|
localEvent.value = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
attendees: [],
|
||||||
|
};
|
||||||
|
newEvent.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const valid = await form.value.validate();
|
||||||
|
|
||||||
|
if (!valid) return;
|
||||||
|
|
||||||
|
let query = 'events/edit';
|
||||||
|
let payload = JSON.stringify([localEvent.value]);
|
||||||
|
if (newEvent.value) {
|
||||||
|
query = 'events/add?name=' + localEvent.value.name;
|
||||||
|
payload = JSON.stringify(localEvent.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
appApi
|
||||||
|
.post(query, payload)
|
||||||
|
.then((resp) => {
|
||||||
|
emit('update', resp.data.data);
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.required .q-field__label::after {
|
||||||
|
content: ' *';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
186
src/components/MemberEditAllDialog.vue
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="
|
||||||
|
newMember ? $t('addNewMember') : 'Edit ' + localMember.firstName + ' ' + localMember.lastName
|
||||||
|
"
|
||||||
|
:height="600"
|
||||||
|
:width="500"
|
||||||
|
>
|
||||||
|
<q-form ref="form">
|
||||||
|
<div class="row justify-center q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5 required"
|
||||||
|
:label="$t('prename')"
|
||||||
|
filled
|
||||||
|
:rules="[(val) => !!val || $t('prenameIsRequired')]"
|
||||||
|
v-model="localMember.firstName"
|
||||||
|
autofocus
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5 required"
|
||||||
|
:label="$t('lastName')"
|
||||||
|
filled
|
||||||
|
:rules="[(val) => !!val || $t('lastNameIsRequired')]"
|
||||||
|
v-model="localMember.lastName"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('birthday')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.birthday"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('address')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.address"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('town')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.town"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('zipCode')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.zip"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('phone')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.phone"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('email')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.email"
|
||||||
|
></q-input>
|
||||||
|
<q-select
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('group')"
|
||||||
|
:options="props.group"
|
||||||
|
filled
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
option-value="name"
|
||||||
|
option-label="name"
|
||||||
|
v-model="localMember.group"
|
||||||
|
></q-select>
|
||||||
|
<q-select
|
||||||
|
class="q-ml-md col-5"
|
||||||
|
:label="$t('responsible')"
|
||||||
|
filled
|
||||||
|
:options="props.responsibles"
|
||||||
|
:option-label="(opt) => opt.firstName + ' ' + opt.lastName"
|
||||||
|
v-model="localMember.responsible"
|
||||||
|
></q-select>
|
||||||
|
<q-input
|
||||||
|
v-if="!newMember"
|
||||||
|
class="q-ml-md col-11"
|
||||||
|
:label="$t('firstVisit')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.firstVisit"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="!newMember"
|
||||||
|
class="q-ml-md col-11"
|
||||||
|
:label="$t('lastVisit')"
|
||||||
|
filled
|
||||||
|
v-model="localMember.lastVisit"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { type PropType, ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import type { Member } from 'src/vueLib/models/member';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
import type { Responsibles } from 'src/vueLib/models/responsible';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const dialog = ref();
|
||||||
|
const form = ref();
|
||||||
|
const newMember = ref(false);
|
||||||
|
const localMember = ref<Member>({
|
||||||
|
id: 0,
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
responsibles: {
|
||||||
|
type: Object as PropType<Responsibles>,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
function open(member: Member | null) {
|
||||||
|
if (member === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member !== null) {
|
||||||
|
localMember.value = { ...member };
|
||||||
|
newMember.value = false;
|
||||||
|
} else {
|
||||||
|
localMember.value = {
|
||||||
|
id: 0,
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
};
|
||||||
|
newMember.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
const valid = await form.value.validate();
|
||||||
|
if (!valid) {
|
||||||
|
NotifyResponse(i18n.global.t('notAllRequiredFieldsFilled'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = 'members/edit';
|
||||||
|
let payload = JSON.stringify([localMember.value]);
|
||||||
|
if (newMember.value) {
|
||||||
|
query = 'members/add';
|
||||||
|
payload = JSON.stringify(localMember.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await appApi
|
||||||
|
.post(query, payload)
|
||||||
|
.then(() => {
|
||||||
|
emit('update');
|
||||||
|
NotifyResponse(i18n.global.t('memberUpdated'));
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.required .q-field__label::after {
|
||||||
|
content: ' *';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
108
src/components/RoleEditAllDialog.vue
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="newRole ? $t('addNewRole') : 'Edit ' + localRole.role"
|
||||||
|
:height="700"
|
||||||
|
:width="700"
|
||||||
|
>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-input
|
||||||
|
v-if="showRoleField"
|
||||||
|
class="q-my-lg col-5 required"
|
||||||
|
:label="$t('role')"
|
||||||
|
filled
|
||||||
|
:rules="[(val) => !!val || $t('roleIsRequired')]"
|
||||||
|
v-model="localRole.role"
|
||||||
|
autofocus
|
||||||
|
></q-input>
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="text-h5 text-bold text-primary flex justify-center">{{
|
||||||
|
$t('permissions')
|
||||||
|
}}</q-card-section>
|
||||||
|
<q-separator color="black" />
|
||||||
|
<PermissionsCheckBoxGroup
|
||||||
|
:permissions="localRole.permissions || []"
|
||||||
|
v-on:update="(val) => (localRole.permissions = val)"
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import type { Role } from 'src/vueLib/models/roles';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import PermissionsCheckBoxGroup from 'src/vueLib/checkboxes/CheckBoxGroupPermissions.vue';
|
||||||
|
import { defaultPermissions } from 'src/vueLib/checkboxes/permissions';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const dialog = ref();
|
||||||
|
const newRole = ref(false);
|
||||||
|
const showRoleField = ref(true);
|
||||||
|
const localRole = ref<Role>({
|
||||||
|
role: '',
|
||||||
|
permissions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
function open(role: Role | null, typ?: 'permissions') {
|
||||||
|
if (role === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showRoleField.value = typ !== 'permissions';
|
||||||
|
if (role !== null) {
|
||||||
|
localRole.value = { ...role };
|
||||||
|
localRole.value.permissions = role.permissions || defaultPermissions;
|
||||||
|
newRole.value = false;
|
||||||
|
} else {
|
||||||
|
localRole.value = {
|
||||||
|
role: '',
|
||||||
|
permissions: defaultPermissions,
|
||||||
|
};
|
||||||
|
newRole.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
let query = 'roles/update?id=' + localRole.value.id;
|
||||||
|
let update = true;
|
||||||
|
if (newRole.value) {
|
||||||
|
query = 'roles/add';
|
||||||
|
update = false;
|
||||||
|
localRole.value.permissions = localRole.value.permissions ?? defaultPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
appApi
|
||||||
|
.post(query, JSON.stringify(localRole.value))
|
||||||
|
.then(() => {
|
||||||
|
if (update) {
|
||||||
|
NotifyResponse(
|
||||||
|
i18n.global.t('role') + " '" + localRole.value.role + "' " + i18n.global.t('updated'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
emit('update');
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.required .q-field__label::after {
|
||||||
|
content: ' *';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
218
src/components/UploadDialog.vue
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame ref="dialog" header-title="Import CSV" :height="dialogHeight" :width="dialogWidth">
|
||||||
|
<div class="column q-gutter-xs">
|
||||||
|
<div class="row">
|
||||||
|
<q-uploader
|
||||||
|
style="max-width: 300px"
|
||||||
|
url="/api/members/import/csv"
|
||||||
|
label="Import CSV"
|
||||||
|
multiple
|
||||||
|
:with-credentials="true"
|
||||||
|
accept=".csv"
|
||||||
|
field-name="file"
|
||||||
|
method="POST"
|
||||||
|
:form-fields="metaData"
|
||||||
|
v-on:failed="onImportFail"
|
||||||
|
:headers="[{ name: 'Accept', value: 'application/json' }]"
|
||||||
|
@uploaded="onUploaded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<q-card :flat="!showOptions">
|
||||||
|
<div class="row q-ma-sm">
|
||||||
|
<div class="text-bold q-ma-sm">
|
||||||
|
<q-btn
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
size="sm"
|
||||||
|
:icon="showOptions ? 'remove' : 'add'"
|
||||||
|
@click="showOptions = !showOptions"
|
||||||
|
></q-btn>
|
||||||
|
CSV options
|
||||||
|
</div>
|
||||||
|
<div class="row justify-center q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-4"
|
||||||
|
label="Row Index of Column titles"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
type="number"
|
||||||
|
v-model.number="data.rowIndex"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-4"
|
||||||
|
label="Seperator"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.seperator"
|
||||||
|
></q-input>
|
||||||
|
<q-space class="col-1"></q-space>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="First Name"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.firstName"
|
||||||
|
autofocus
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Last Name"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.lastName"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Birthday"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.birthday"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Address"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.address"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Town"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.town"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Zip Code"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.zip"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="Phone"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.phone"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
v-if="showOptions"
|
||||||
|
class="col-2"
|
||||||
|
label="E-Mail"
|
||||||
|
dense
|
||||||
|
borderless
|
||||||
|
v-model="data.email"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
<div class="row justify-end q-ma-sm">
|
||||||
|
<q-btn no-caps color="primary" class="col-3" v-close-popup>{{ $t('close') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import type { MetaData } from 'src/vueLib/models/metaData';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
|
||||||
|
const dialogHeight = ref(300);
|
||||||
|
const dialogWidth = ref(500);
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-upload']);
|
||||||
|
|
||||||
|
const dialog = ref();
|
||||||
|
const showOptions = ref(true);
|
||||||
|
const data = ref<MetaData>({
|
||||||
|
rowIndex: 0,
|
||||||
|
seperator: ';',
|
||||||
|
firstName: 'firstName',
|
||||||
|
lastName: 'lastName',
|
||||||
|
birthday: 'birthday',
|
||||||
|
address: 'address',
|
||||||
|
town: 'town',
|
||||||
|
zip: 'zip',
|
||||||
|
phone: 'phone',
|
||||||
|
email: 'email',
|
||||||
|
});
|
||||||
|
|
||||||
|
const metaData = ref([
|
||||||
|
{ name: 'rowIndex', value: String(data.value.rowIndex) },
|
||||||
|
{ name: 'seperator', value: data.value.seperator },
|
||||||
|
{ name: 'firstName', value: data.value.firstName },
|
||||||
|
{ name: 'lastName', value: data.value.lastName },
|
||||||
|
{ name: 'birthday', value: data.value.birthday },
|
||||||
|
{ name: 'address', value: data.value.address },
|
||||||
|
{ name: 'town', value: data.value.town },
|
||||||
|
{ name: 'zip', value: data.value.zip },
|
||||||
|
{ name: 'phone', value: data.value.phone },
|
||||||
|
{ name: 'email', value: data.value.email },
|
||||||
|
]);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
showOptions.value = false;
|
||||||
|
dialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(showOptions, () => {
|
||||||
|
if (dialogWidth.value === 500) {
|
||||||
|
dialogWidth.value = 325;
|
||||||
|
dialogHeight.value = 325;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dialogHeight.value = 500;
|
||||||
|
dialogWidth.value = 500;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
data,
|
||||||
|
() => {
|
||||||
|
metaData.value = [
|
||||||
|
{ name: 'rowIndex', value: String(data.value.rowIndex) },
|
||||||
|
{ name: 'seperator', value: data.value.seperator },
|
||||||
|
{ name: 'firstName', value: data.value.firstName },
|
||||||
|
{ name: 'lastName', value: data.value.lastName },
|
||||||
|
{ name: 'birthday', value: data.value.birthday },
|
||||||
|
{ name: 'address', value: data.value.address },
|
||||||
|
{ name: 'town', value: data.value.town },
|
||||||
|
{ name: 'zip', value: data.value.zip },
|
||||||
|
{ name: 'phone', value: data.value.phone },
|
||||||
|
{ name: 'email', value: data.value.email },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function onImportFail(info: { files: readonly File[]; xhr: XMLHttpRequest }) {
|
||||||
|
const response = JSON.parse(info.xhr.responseText);
|
||||||
|
if (response.message) {
|
||||||
|
NotifyResponse(response.message, 'error', 15000);
|
||||||
|
}
|
||||||
|
emit('update-upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUploaded(info: { files: readonly File[]; xhr: XMLHttpRequest }) {
|
||||||
|
const response = JSON.parse(info.xhr.responseText);
|
||||||
|
if (response.message) {
|
||||||
|
NotifyResponse(response.message);
|
||||||
|
}
|
||||||
|
emit('update-upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
132
src/components/UserEditAllDialog.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="newUser ? $t('addNewUser') : 'Edit ' + localUser.user"
|
||||||
|
:height="600"
|
||||||
|
:width="500"
|
||||||
|
>
|
||||||
|
<q-form ref="form">
|
||||||
|
<div class="row justify-center q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
class="col-5 required"
|
||||||
|
:label="$t('user')"
|
||||||
|
filled
|
||||||
|
:lazy-rules="false"
|
||||||
|
:rules="[(val) => !!val || $t('userIsRequired')]"
|
||||||
|
v-model="localUser.user"
|
||||||
|
autofocus
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
class="col-5 required"
|
||||||
|
:label="$t('email')"
|
||||||
|
filled
|
||||||
|
:lazy-rules="false"
|
||||||
|
:rules="[(val) => !!val || $t('emailIsRequired')]"
|
||||||
|
v-model="localUser.email"
|
||||||
|
></q-input>
|
||||||
|
<EnterNewPassword class="col-5 required" v-model:password="localUser.password!" />
|
||||||
|
<div class="col-5">
|
||||||
|
<q-select
|
||||||
|
class="col-5 required"
|
||||||
|
:label="$t('role')"
|
||||||
|
filled
|
||||||
|
:options="props.roles"
|
||||||
|
:rules="[(val) => !!val || $t('roleIsRequired')]"
|
||||||
|
v-model="role"
|
||||||
|
></q-select>
|
||||||
|
<q-input
|
||||||
|
class="col-5 q-mt-xl"
|
||||||
|
:label="$t('expires')"
|
||||||
|
filled
|
||||||
|
type="datetime-local"
|
||||||
|
v-model="localUser.expiration"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import type { User } from 'src/vueLib/models/users';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { validateQForm } from 'src/vueLib/utils/validation';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
import { DefaultSettings } from 'src/vueLib/models/settings';
|
||||||
|
import EnterNewPassword from 'src/vueLib/login/EnterNewPassword.vue';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const dialog = ref();
|
||||||
|
const form = ref();
|
||||||
|
const newUser = ref(false);
|
||||||
|
const role = ref('');
|
||||||
|
const localUser = ref<User>({
|
||||||
|
user: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
roles: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
async function open(user: User | null) {
|
||||||
|
if (user === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user !== null) {
|
||||||
|
localUser.value = { ...user };
|
||||||
|
newUser.value = false;
|
||||||
|
} else {
|
||||||
|
localUser.value = {
|
||||||
|
user: '',
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
newUser.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dialog.value?.open();
|
||||||
|
await validateQForm(form.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!(await validateQForm(form.value))) {
|
||||||
|
NotifyResponse(i18n.global.t('notAllRequiredFieldsFilled'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let query = 'users/edit?id=' + localUser.value.id;
|
||||||
|
if (newUser.value) {
|
||||||
|
query = 'users/add';
|
||||||
|
localUser.value.settings = DefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
localUser.value.role = { role: role.value || '', permissions: [] };
|
||||||
|
appApi
|
||||||
|
.post(query, JSON.stringify(localUser.value))
|
||||||
|
.then(() => {
|
||||||
|
emit('update');
|
||||||
|
dialog.value.close();
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.required .q-field__label::after {
|
||||||
|
content: ' *';
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
92
src/components/dialog/OkDialog.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog ref="dialog">
|
||||||
|
<q-card :style="'width:' + props.width">
|
||||||
|
<q-card-section
|
||||||
|
v-if="props.dialogLabel"
|
||||||
|
class="text-h6 text-center"
|
||||||
|
:class="'text-' + props.labelColor"
|
||||||
|
>{{ props.dialogLabel }}</q-card-section
|
||||||
|
>
|
||||||
|
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
|
||||||
|
props.text
|
||||||
|
}}</q-card-section>
|
||||||
|
<q-card-actions align="right" class="text-primary">
|
||||||
|
<q-btn
|
||||||
|
v-if="props.buttonCancelLabel"
|
||||||
|
:flat="props.buttonCancelFlat"
|
||||||
|
:label="props.buttonCancelLabel"
|
||||||
|
v-close-popup
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="props.buttonOkLabel"
|
||||||
|
:flat="props.buttonOkFlat"
|
||||||
|
:label="props.buttonOkLabel"
|
||||||
|
:color="props.buttonOkColor"
|
||||||
|
v-close-popup
|
||||||
|
@click="confirm"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
buttonOkLabel: {
|
||||||
|
type: String,
|
||||||
|
default: 'OK',
|
||||||
|
},
|
||||||
|
buttonOkColor: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonOkFlat: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
labelColor: {
|
||||||
|
type: String,
|
||||||
|
default: 'primary',
|
||||||
|
},
|
||||||
|
dialogLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonCancelLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
buttonCancelFlat: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: '300px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-confirm']);
|
||||||
|
|
||||||
|
const dialog = ref();
|
||||||
|
const localInput = ref();
|
||||||
|
|
||||||
|
const open = (input?: unknown) => {
|
||||||
|
localInput.value = input;
|
||||||
|
dialog.value?.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm = () => {
|
||||||
|
emit('update-confirm', localInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
1
src/css/app.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// app global css in SCSS form
|
||||||
40
src/css/quasar.variables.scss
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Quasar SCSS (& Sass) Variables
|
||||||
|
// --------------------------------------------------
|
||||||
|
// To customize the look and feel of this app, you can override
|
||||||
|
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
||||||
|
|
||||||
|
// Check documentation for full list of Quasar variables
|
||||||
|
|
||||||
|
// Your own variables (that are declared here) and Quasar's own
|
||||||
|
// ones will be available out of the box in your .vue/.scss/.sass files
|
||||||
|
|
||||||
|
// It's highly recommended to change the default colors
|
||||||
|
// to match your app's branding.
|
||||||
|
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||||
|
|
||||||
|
$primary: #1976d2;
|
||||||
|
$primary-text: #ffffff;
|
||||||
|
$secondary: #26a69a;
|
||||||
|
$secondary-text: #ffffff;
|
||||||
|
$accent: #9c27b0;
|
||||||
|
|
||||||
|
$dark: #1d1d1d;
|
||||||
|
$dark-page: #121212;
|
||||||
|
|
||||||
|
$positive: #21ba45;
|
||||||
|
$negative: #c10015;
|
||||||
|
$info: #31ccec;
|
||||||
|
$warning: #f2c037;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--q-primary-text: #ffffff;
|
||||||
|
--q-secondary-text: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary-text {
|
||||||
|
color: var(--q-primary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-secondary-text {
|
||||||
|
color: var(--q-secondary-text) !important;
|
||||||
|
}
|
||||||
7
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
NODE_ENV: string;
|
||||||
|
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
|
||||||
|
VUE_ROUTER_BASE: string | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/layouts/MainLayout.vue
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<q-layout view="lHh Lpr lFf">
|
||||||
|
<q-header elevated>
|
||||||
|
<q-toolbar>
|
||||||
|
<q-img
|
||||||
|
:src="localLogo"
|
||||||
|
alt="Logo"
|
||||||
|
style="width: 40px; height: 40px; background-color: var(--q-primary)"
|
||||||
|
class="q-mr-sm"
|
||||||
|
/>
|
||||||
|
<q-btn flat dense round icon="menu" aria-label="Menu" @click="toggleLeftDrawer" />
|
||||||
|
|
||||||
|
<q-toolbar-title class="text-primary-text"> {{ $t(appName) }} </q-toolbar-title>
|
||||||
|
|
||||||
|
<div>Version {{ version }}</div>
|
||||||
|
<q-btn dense icon="refresh" square class="q-px-md q-ml-md" @click="refresh" />
|
||||||
|
<LoginMenu />
|
||||||
|
</q-toolbar>
|
||||||
|
</q-header>
|
||||||
|
|
||||||
|
<q-drawer v-model="leftDrawerOpen" bordered :width="drawerWidth" :overlay="$q.screen.lt.sm">
|
||||||
|
<q-list>
|
||||||
|
<q-item v-if="!autorized" to="/login" exact clickable v-ripple @click="closeDrawer">
|
||||||
|
<q-item-section>{{ $t('login') }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="autorized || user.isPermittedTo('members', 'read')"
|
||||||
|
to="/members"
|
||||||
|
exact
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
<q-item-section> {{ $t('members') }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="autorized || user.isPermittedTo('events', 'read')"
|
||||||
|
to="/events"
|
||||||
|
exact
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
<q-item-section>{{ $t('events') }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="autorized || user.isPermittedTo('responsible', 'read')"
|
||||||
|
to="/responsible"
|
||||||
|
exact
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
<q-item-section>{{ $t('responsible') }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="autorized || user.isPermittedTo('group', 'read')"
|
||||||
|
to="/group"
|
||||||
|
exact
|
||||||
|
clickable
|
||||||
|
v-ripple
|
||||||
|
@click="closeDrawer"
|
||||||
|
>
|
||||||
|
<q-item-section>{{ $t('groups') }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-drawer>
|
||||||
|
<q-page-container>
|
||||||
|
<router-view />
|
||||||
|
</q-page-container>
|
||||||
|
</q-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { version } from '../../package.json';
|
||||||
|
import LoginMenu from 'src/vueLib/login/LoginMenu.vue';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import { logo, appName } from 'src/vueLib/models/settings';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
const localLogo = ref(logo);
|
||||||
|
|
||||||
|
const leftDrawerOpen = ref(false);
|
||||||
|
const user = useUserStore();
|
||||||
|
const autorized = computed(
|
||||||
|
() => user.isAuthorizedAs(['admin']) || user.isPermittedTo('settings', 'read'),
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleLeftDrawer() {
|
||||||
|
leftDrawerOpen.value = !leftDrawerOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDrawer() {
|
||||||
|
leftDrawerOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change width based on screen size
|
||||||
|
const drawerWidth = computed(() => {
|
||||||
|
const $q = useQuasar();
|
||||||
|
return $q.screen.lt.sm ? 220 : 300; // phone: 220px, desktop: 300px
|
||||||
|
});
|
||||||
|
</script>
|
||||||
93
src/localstorage/localStorage.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Dark } from 'quasar';
|
||||||
|
import { appName, databaseName, type Settings } from 'src/vueLib/models/settings';
|
||||||
|
import { updateOrAddObject } from 'src/vueLib/utils/utils';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export function setLocalSettings(settings: Settings) {
|
||||||
|
localStorage.setItem('icon', settings.icon);
|
||||||
|
localStorage.setItem('appName', settings.appName);
|
||||||
|
localStorage.setItem('databaseName', settings.databaseName);
|
||||||
|
localStorage.setItem('primaryColor', settings.primaryColor);
|
||||||
|
localStorage.setItem('primaryColorText', settings.primaryColorText);
|
||||||
|
localStorage.setItem('secondaryColor', settings.secondaryColor);
|
||||||
|
localStorage.setItem('secondaryColorText', settings.secondaryColorText);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalSettings(): Settings {
|
||||||
|
return <Settings>{
|
||||||
|
icon: localStorage.getItem('icon'),
|
||||||
|
appName: localStorage.getItem('appName') || appName.value,
|
||||||
|
databaseName: localStorage.getItem('databaseName') || databaseName.value,
|
||||||
|
primaryColor: localStorage.getItem('primaryColor'),
|
||||||
|
primaryColorText: localStorage.getItem('primaryColorText'),
|
||||||
|
secondaryColor: localStorage.getItem('secondaryColor'),
|
||||||
|
secondaryColorText: localStorage.getItem('secondaryColorText'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLocalStorage() {
|
||||||
|
localStorage.removeItem('icon');
|
||||||
|
localStorage.removeItem('appName');
|
||||||
|
localStorage.removeItem('databaseName');
|
||||||
|
localStorage.removeItem('primaryColor');
|
||||||
|
localStorage.removeItem('primaryColorText');
|
||||||
|
localStorage.removeItem('secondaryColor');
|
||||||
|
localStorage.removeItem('secondaryColorText');
|
||||||
|
localStorage.removeItem('lastRoute');
|
||||||
|
localStorage.removeItem('mode');
|
||||||
|
localStorage.removeItem('lang');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalDarkMode() {
|
||||||
|
localStorage.setItem('mode', String(Dark.mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalDarkMode(): boolean {
|
||||||
|
return localStorage.getItem('mode') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalLastRoute(route: string) {
|
||||||
|
localStorage.setItem('lastRoute', route);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalLastRoute(): string {
|
||||||
|
return localStorage.getItem('lastRoute') || '/members';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalLanguage(language: string) {
|
||||||
|
localStorage.setItem('lang', language);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalLanguage(): string | null {
|
||||||
|
return localStorage.getItem('lang');
|
||||||
|
}
|
||||||
|
|
||||||
|
type pageDefault = {
|
||||||
|
page: string;
|
||||||
|
filteredColumn: string;
|
||||||
|
filteredValue: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type pageDefaults = pageDefault[];
|
||||||
|
|
||||||
|
const pageDefaults = ref<pageDefaults>([]);
|
||||||
|
|
||||||
|
export function setLocalPageDefaults(
|
||||||
|
page: string,
|
||||||
|
filteredColumn?: string,
|
||||||
|
filteredValue?: string[],
|
||||||
|
) {
|
||||||
|
updateOrAddObject(
|
||||||
|
pageDefaults.value,
|
||||||
|
{ page: page, filteredColumn: filteredColumn, filteredValue: filteredValue },
|
||||||
|
'page',
|
||||||
|
);
|
||||||
|
localStorage.setItem('pageDefaults', JSON.stringify(pageDefaults.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalPageDefaults(page: string): pageDefault | null {
|
||||||
|
const defaults = localStorage.getItem('pageDefaults');
|
||||||
|
if (!defaults) return null;
|
||||||
|
pageDefaults.value = JSON.parse(defaults);
|
||||||
|
return pageDefaults.value.find((e) => e.page === page) || null;
|
||||||
|
}
|
||||||
27
src/pages/ErrorNotFound.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size: 30vh">
|
||||||
|
404
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-h2" style="opacity:.4">
|
||||||
|
Oops. Nothing here...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
class="q-mt-xl"
|
||||||
|
color="white"
|
||||||
|
text-color="blue"
|
||||||
|
unelevated
|
||||||
|
to="/"
|
||||||
|
label="Go Home"
|
||||||
|
no-caps
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
||||||
11
src/pages/EventsTable.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<SiteTitle :title="$t('events')" />
|
||||||
|
<EventsTable />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
import EventsTable from 'src/vueLib/tables/events/EventsTable.vue';
|
||||||
|
</script>
|
||||||
11
src/pages/GroupTable.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<SiteTitle :title="$t('groups')" />
|
||||||
|
<GroupTable />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
import GroupTable from 'src/vueLib/tables/group/GroupTable.vue';
|
||||||
|
</script>
|
||||||
27
src/pages/LoginPage.vue
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-center">
|
||||||
|
<LoginForm v-on:update-close="forwardToPage" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getLocalLastRoute } from 'src/localstorage/localStorage';
|
||||||
|
import LoginForm from 'src/vueLib/login/LoginForm.vue';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import { nextTick, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (userStore.user?.username !== '' && userStore.user?.role.role !== '') {
|
||||||
|
forwardToPage().catch((err) => console.error(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const forwardToPage = async () => {
|
||||||
|
await nextTick();
|
||||||
|
await router.push(getLocalLastRoute());
|
||||||
|
};
|
||||||
|
</script>
|
||||||
13
src/pages/MembersTable.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<SiteTitle :title="$t('members')" />
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<MembersTable />
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
import MembersTable from 'src/vueLib/tables/members/MembersTable.vue';
|
||||||
|
</script>
|
||||||
11
src/pages/ResponsibleTable.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<q-page>
|
||||||
|
<SiteTitle :title="$t('responsibles')" />
|
||||||
|
<ResponsibleTable />
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
import ResponsibleTable from 'src/vueLib/tables/responsible/ResponsibleTable.vue';
|
||||||
|
</script>
|
||||||
206
src/pages/SettingsPage.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<SiteTitle :title="$t('userSettings')" />
|
||||||
|
<div class="text-h2 flex flex-center">
|
||||||
|
<q-card class="q-gutter-md">
|
||||||
|
<div>
|
||||||
|
<q-card class="q-ma-lg">
|
||||||
|
<p class="text-bold text-h6 text-primary q-pa-md">{{ $t('general') }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<q-input
|
||||||
|
:readonly="!user.isPermittedTo('settings', 'write')"
|
||||||
|
:class="[
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-3',
|
||||||
|
colorGroup ? 'col-md-6' : 'col-md-6',
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-12',
|
||||||
|
'q-pa-md',
|
||||||
|
]"
|
||||||
|
filled
|
||||||
|
:label="$t('appName')"
|
||||||
|
v-model="settings.appName"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
:readonly="!user.isPermittedTo('settings', 'write')"
|
||||||
|
:class="[
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-3',
|
||||||
|
colorGroup ? 'col-md-6' : 'col-md-6',
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-12',
|
||||||
|
'q-pa-md',
|
||||||
|
]"
|
||||||
|
filled
|
||||||
|
:label="$t('icon')"
|
||||||
|
v-model="settings.icon"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-ma-lg">
|
||||||
|
<p class="text-bold text-h6 text-primary q-pa-md">{{ $t('database') }}</p>
|
||||||
|
<div class="row">
|
||||||
|
<q-input
|
||||||
|
:readonly="!user.isPermittedTo('settings', 'write')"
|
||||||
|
:class="[
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-3',
|
||||||
|
colorGroup ? 'col-md-6' : 'col-md-6',
|
||||||
|
colorGroup ? 'col-md-4' : 'col-md-12',
|
||||||
|
'q-pa-md',
|
||||||
|
]"
|
||||||
|
filled
|
||||||
|
:label="$t('databaseName')"
|
||||||
|
v-model="settings.databaseName"
|
||||||
|
></q-input>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
<q-card class="q-ma-lg">
|
||||||
|
<q-btn
|
||||||
|
class="q-pa-md"
|
||||||
|
:icon="colorGroup ? 'remove' : 'add'"
|
||||||
|
size="md"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
@click="colorGroup = !colorGroup"
|
||||||
|
>{{ $t('colors') }}</q-btn
|
||||||
|
>
|
||||||
|
<div v-if="colorGroup" class="colum">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-sm-6 col-md-3 q-px-md">
|
||||||
|
<p class="text-center text-bold text-h6 text-primary">{{ $t('primaryColor') }}</p>
|
||||||
|
<q-color
|
||||||
|
:disable="!user.isPermittedTo('settings', 'write')"
|
||||||
|
bordered
|
||||||
|
class="q-mx-md"
|
||||||
|
v-model="settings.primaryColor"
|
||||||
|
></q-color>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-md-3 q-px-md">
|
||||||
|
<p
|
||||||
|
:class="[
|
||||||
|
'text-center',
|
||||||
|
'text-bold',
|
||||||
|
'text-h6',
|
||||||
|
'text-primary-text',
|
||||||
|
settings.primaryColorText === '#ffffff' ? 'bg-black' : '',
|
||||||
|
]"
|
||||||
|
:style="settings.primaryColorText === '#ffffff' ? 'opacity: 0.2;' : ''"
|
||||||
|
>
|
||||||
|
{{ $t('primaryColorText') }}
|
||||||
|
</p>
|
||||||
|
<q-color
|
||||||
|
:disable="!user.isPermittedTo('settings', 'write')"
|
||||||
|
bordered
|
||||||
|
class="q-mx-md"
|
||||||
|
v-model="settings.primaryColorText"
|
||||||
|
></q-color>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-md-3 q-px-md">
|
||||||
|
<p class="text-center text-bold text-h6 text-secondary">
|
||||||
|
{{ $t('secondaryColor') }}
|
||||||
|
</p>
|
||||||
|
<q-color
|
||||||
|
:disable="!user.isPermittedTo('settings', 'write')"
|
||||||
|
class="q-mx-md"
|
||||||
|
v-model="settings.secondaryColor"
|
||||||
|
></q-color>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 col-md-3 q-px-md">
|
||||||
|
<p
|
||||||
|
:class="[
|
||||||
|
'text-center',
|
||||||
|
'text-bold',
|
||||||
|
'text-h6',
|
||||||
|
'text-secondary-text',
|
||||||
|
settings.secondaryColorText === '#ffffff' ? 'bg-black' : '',
|
||||||
|
]"
|
||||||
|
:style="settings.secondaryColorText === '#ffffff' ? 'opacity: 0.2;' : ''"
|
||||||
|
>
|
||||||
|
{{ $t('secondaryColorText') }}
|
||||||
|
</p>
|
||||||
|
<q-color
|
||||||
|
:disable="!user.isPermittedTo('settings', 'write')"
|
||||||
|
class="q-mx-md"
|
||||||
|
v-model="settings.secondaryColorText"
|
||||||
|
></q-color>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-btn
|
||||||
|
:disable="!user.isPermittedTo('settings', 'write')"
|
||||||
|
class="q-my-md q-mx-md"
|
||||||
|
color="secondary"
|
||||||
|
dense
|
||||||
|
no-caps
|
||||||
|
@click="resetColors"
|
||||||
|
>{{ $t('resetColors') }}</q-btn
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
<div class="row justify-end">
|
||||||
|
<div class="q-my-lg q-mr-xl">
|
||||||
|
<q-btn no-caps color="primary" @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { logo, appName, databaseName } from 'src/vueLib/models/settings';
|
||||||
|
import { reactive, ref, watch } from 'vue';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { type Settings } from 'src/vueLib/models/settings';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import { setLocalSettings } from 'src/localstorage/localStorage';
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const colorGroup = ref(false);
|
||||||
|
const user = useUserStore();
|
||||||
|
|
||||||
|
const settings = reactive<Settings>({
|
||||||
|
appName: appName.value,
|
||||||
|
icon: logo.value,
|
||||||
|
databaseName: databaseName.value,
|
||||||
|
primaryColor: document.documentElement.style.getPropertyValue('--q-primary'),
|
||||||
|
primaryColorText: document.documentElement.style.getPropertyValue('--q-primary-text'),
|
||||||
|
secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'),
|
||||||
|
secondaryColorText: document.documentElement.style.getPropertyValue('--q-secondary-text'),
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(settings, (newSettings) => {
|
||||||
|
logo.value = newSettings.icon;
|
||||||
|
appName.value = newSettings.appName;
|
||||||
|
databaseName.value = newSettings.databaseName;
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetColors() {
|
||||||
|
document.documentElement.style.setProperty('--q-primary', '#1976d2');
|
||||||
|
settings.primaryColor = '#1976d2';
|
||||||
|
document.documentElement.style.setProperty('--q-primary-text', '#ffffff');
|
||||||
|
settings.primaryColorText = '#ffffff';
|
||||||
|
document.documentElement.style.setProperty('--q-secondary', '#26a69a');
|
||||||
|
settings.secondaryColor = '#26a69a';
|
||||||
|
document.documentElement.style.setProperty('--q-secondary-text', '#ffffff');
|
||||||
|
settings.secondaryColorText = '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
document.documentElement.style.setProperty('--q-primary', settings.primaryColor);
|
||||||
|
document.documentElement.style.setProperty('--q-primary-text', settings.primaryColorText);
|
||||||
|
document.documentElement.style.setProperty('--q-secondary', settings.secondaryColor);
|
||||||
|
document.documentElement.style.setProperty('--q-secondary-text', settings.secondaryColorText);
|
||||||
|
appName.value = settings.appName;
|
||||||
|
logo.value = settings.icon;
|
||||||
|
setLocalSettings(settings);
|
||||||
|
|
||||||
|
const tempuser = user.user;
|
||||||
|
|
||||||
|
if (tempuser) {
|
||||||
|
tempuser.settings = settings;
|
||||||
|
}
|
||||||
|
appApi
|
||||||
|
.post('users/update', tempuser)
|
||||||
|
.then((resp) => NotifyResponse(resp.data.message))
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
38
src/pages/UserSettings.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<SiteTitle :title="$t('userSettings')" />
|
||||||
|
<div class="q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
dense
|
||||||
|
class="taxt-grey"
|
||||||
|
active-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
>
|
||||||
|
<q-tab name="users" no-caps :label="$t('users')" />
|
||||||
|
<q-tab name="roles" no-caps :label="$t('roles')" />
|
||||||
|
</q-tabs>
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel name="users" style="padding: 0px">
|
||||||
|
<UserTable />
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="roles" style="padding: 0px">
|
||||||
|
<RoleTable />
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import UserTable from 'src/vueLib/tables/users/UserTable.vue';
|
||||||
|
import RoleTable from 'src/vueLib/tables/roles/RoleTable.vue';
|
||||||
|
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
|
||||||
|
|
||||||
|
const tab = ref('users');
|
||||||
|
</script>
|
||||||
53
src/router/index.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineRouter } from '#q-app/wrappers';
|
||||||
|
import {
|
||||||
|
createMemoryHistory,
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory,
|
||||||
|
} from 'vue-router';
|
||||||
|
import routes from './routes';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If not building with SSR mode, you can
|
||||||
|
* directly export the Router instantiation;
|
||||||
|
*
|
||||||
|
* The function below can be async too; either use
|
||||||
|
* async/await or return a Promise which resolves
|
||||||
|
* with the Router instance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default defineRouter(function (/* { store, ssrContext } */) {
|
||||||
|
const createHistory = process.env.SERVER
|
||||||
|
? createMemoryHistory
|
||||||
|
: process.env.VUE_ROUTER_MODE === 'history'
|
||||||
|
? createWebHistory
|
||||||
|
: createWebHashHistory;
|
||||||
|
|
||||||
|
const Router = createRouter({
|
||||||
|
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||||
|
routes,
|
||||||
|
|
||||||
|
// Leave this as is and make changes in quasar.conf.js instead!
|
||||||
|
// quasar.conf.js -> build -> vueRouterMode
|
||||||
|
// quasar.conf.js -> build -> publicPath
|
||||||
|
history: createHistory(process.env.VUE_ROUTER_BASE),
|
||||||
|
});
|
||||||
|
|
||||||
|
Router.beforeEach((to, from, next) => {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const isLoggedIn = userStore.isAuthenticated;
|
||||||
|
if (to.meta.requiresAuth && !isLoggedIn) {
|
||||||
|
next('/login');
|
||||||
|
} else if (
|
||||||
|
to.meta.requiresAdmin &&
|
||||||
|
!userStore.isPermittedTo(to.path.replace('/', ''), 'read')
|
||||||
|
) {
|
||||||
|
next('/');
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Router;
|
||||||
|
});
|
||||||
57
src/router/routes.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: () => import('layouts/MainLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: () => import('pages/LoginPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'login',
|
||||||
|
component: () => import('pages/LoginPage.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'members',
|
||||||
|
component: () => import('pages/MembersTable.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'events',
|
||||||
|
component: () => import('pages/EventsTable.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'responsible',
|
||||||
|
component: () => import('src/pages/ResponsibleTable.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'group',
|
||||||
|
component: () => import('src/pages/GroupTable.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
component: () => import('pages/SettingsPage.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'userSettings',
|
||||||
|
component: () => import('pages/UserSettings.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Always leave this as last one,
|
||||||
|
// but you can also remove it
|
||||||
|
{
|
||||||
|
path: '/:catchAll(.*)*',
|
||||||
|
component: () => import('pages/ErrorNotFound.vue'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
82
src/vueLib/checkboxes/CheckBoxGroupPermissions.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<q-card>
|
||||||
|
<q-card bordered v-for="(permission, index) in localPermission" v-bind:key="index">
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<div class="text-h7 text-bold text-primary">{{ $t(permission.name) }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<q-checkbox
|
||||||
|
class="q-mx-md"
|
||||||
|
:model-value="isFlagSet(permission.permission, 1 << 0)"
|
||||||
|
@update:model-value="(val) => toggleBit(index, 0, val)"
|
||||||
|
>{{ i18n.global.t('read') }}</q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-mx-md"
|
||||||
|
:model-value="isFlagSet(permission.permission, 1 << 1)"
|
||||||
|
@update:model-value="(val) => toggleBit(index, 1, val)"
|
||||||
|
>{{ i18n.global.t('write') }}</q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
class="q-mx-md"
|
||||||
|
:model-value="isFlagSet(permission.permission, 1 << 2)"
|
||||||
|
@update:model-value="(val) => toggleBit(index, 2, val)"
|
||||||
|
>{{ i18n.global.t('delete') }}</q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-if="permission.permissionNumber > 3"
|
||||||
|
class="q-mx-md"
|
||||||
|
:model-value="isFlagSet(permission.permission, 1 << 3)"
|
||||||
|
@update:model-value="(val) => toggleBit(index, 3, val)"
|
||||||
|
>{{ i18n.global.t('import') }}</q-checkbox
|
||||||
|
>
|
||||||
|
<q-checkbox
|
||||||
|
v-if="permission.permissionNumber > 4"
|
||||||
|
class="q-mx-md"
|
||||||
|
:model-value="isFlagSet(permission.permission, 1 << 4)"
|
||||||
|
@update:model-value="(val) => toggleBit(index, 4, val)"
|
||||||
|
>{{ i18n.global.t('export') }}</q-checkbox
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, type PropType } from 'vue';
|
||||||
|
import type { Permissions } from './permissions';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
permissions: {
|
||||||
|
type: Object as PropType<Permissions>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
const localPermission = ref(
|
||||||
|
props.permissions.map((e) => ({
|
||||||
|
name: e.name,
|
||||||
|
permission: e.permission ?? 0,
|
||||||
|
permissionNumber: e.name === 'members' ? 5 : 3,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function isFlagSet(mask: number, flag: number) {
|
||||||
|
return (mask & flag) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBit(index: number, bit: number, value: boolean) {
|
||||||
|
const item = localPermission.value[index];
|
||||||
|
if (!item) return; // guard against undefined index
|
||||||
|
|
||||||
|
const mask = 1 << bit;
|
||||||
|
const current = item.permission ?? 0;
|
||||||
|
|
||||||
|
item.permission = value ? current | mask : current & ~mask;
|
||||||
|
emit('update', localPermission.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
50
src/vueLib/checkboxes/permissions.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export interface Permission {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
permission: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Permissions = Permission[];
|
||||||
|
|
||||||
|
export const defaultPermissions = [
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
label: i18n.global.t('settings'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userSettings',
|
||||||
|
label: i18n.global.t('userSettings'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'members',
|
||||||
|
label: i18n.global.t('members'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'events',
|
||||||
|
label: i18n.global.t('events'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'responsible',
|
||||||
|
label: i18n.global.t('responsible'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'group',
|
||||||
|
label: i18n.global.t('group'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'excursionTable',
|
||||||
|
label: i18n.global.t('excursionTable'),
|
||||||
|
permission: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const permissions = ref<Permissions>(defaultPermissions);
|
||||||
170
src/vueLib/dialog/DialogFrame.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<q-dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
:maximized="minMaxState"
|
||||||
|
:full-width="minMaxState"
|
||||||
|
:no-focus="!minMaxState"
|
||||||
|
:no-refocus="!minMaxState"
|
||||||
|
:seamless="!minMaxState"
|
||||||
|
>
|
||||||
|
<q-card class="layout bg-surface text-on-surface" :style="cardStyle">
|
||||||
|
<!-- Draggable Header -->
|
||||||
|
<div
|
||||||
|
class="dialog-header row items-center justify-between bg-transparent"
|
||||||
|
v-touch-pan.mouse.prevent.stop="handlePan"
|
||||||
|
>
|
||||||
|
<div v-if="headerTitle" class="text-left text-bold text-caption q-mx-sm">
|
||||||
|
{{ headerTitle }}
|
||||||
|
</div>
|
||||||
|
<div class="row justify-end q-mx-sm">
|
||||||
|
<q-btn dense flat :icon="minMaxIcon" size="md" @click="minMax" />
|
||||||
|
<q-btn dense flat icon="close" size="md" v-close-popup />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-separator color="black" />
|
||||||
|
|
||||||
|
<!-- Content Slot -->
|
||||||
|
<div class="scrollArea" :style="'padding: ' + props.innerPadding + 'px'">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize Handle -->
|
||||||
|
<div v-if="!minMaxState" class="resize-handle" @mousedown.prevent="startResizing" />
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from 'vue';
|
||||||
|
import type { TouchPanValue } from 'quasar';
|
||||||
|
|
||||||
|
const dialogRef = ref();
|
||||||
|
const open = () => dialogRef.value?.show();
|
||||||
|
const close = () => dialogRef.value?.hide();
|
||||||
|
defineExpose({ open, close });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
headerTitle: { type: String, default: '' },
|
||||||
|
width: { type: Number, default: 400 },
|
||||||
|
height: { type: Number, default: 250 },
|
||||||
|
innerPadding: { type: Number, default: 16 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fullscreen toggle
|
||||||
|
const minMaxIcon = ref('fullscreen');
|
||||||
|
const minMaxState = ref(false);
|
||||||
|
function minMax() {
|
||||||
|
minMaxState.value = !minMaxState.value;
|
||||||
|
minMaxIcon.value = minMaxState.value ? 'fullscreen_exit' : 'fullscreen';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position and Size
|
||||||
|
const position = ref({ x: 0, y: 0 });
|
||||||
|
const width = ref(props.width || 400);
|
||||||
|
const height = ref(props.height || 250);
|
||||||
|
|
||||||
|
// Watch prop changes and sync local ref
|
||||||
|
watch(
|
||||||
|
() => props.width,
|
||||||
|
(newWidth) => {
|
||||||
|
if (newWidth !== undefined && newWidth !== width.value) {
|
||||||
|
width.value = newWidth;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.height,
|
||||||
|
(newHeight) => {
|
||||||
|
if (newHeight !== undefined && newHeight !== height.value) {
|
||||||
|
height.value = newHeight;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dragging (only from header)
|
||||||
|
const handlePan: TouchPanValue = (details) => {
|
||||||
|
if (!minMaxState.value && details.delta) {
|
||||||
|
position.value.x += details.delta.x || 0;
|
||||||
|
position.value.y += details.delta.y || 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resizing
|
||||||
|
const isResizing = ref(false);
|
||||||
|
function startResizing(e: MouseEvent) {
|
||||||
|
isResizing.value = true;
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startY = e.clientY;
|
||||||
|
const startWidth = width.value;
|
||||||
|
const startHeight = height.value;
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
width.value = Math.max(200, startWidth + e.clientX - startX);
|
||||||
|
height.value = Math.max(200, startHeight + e.clientY - startY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
isResizing.value = false;
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
window.removeEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
window.addEventListener('mouseup', onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
const cardStyle = computed(() => {
|
||||||
|
if (minMaxState.value) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
width:
|
||||||
|
typeof width.value === 'number' || /^\d+$/.test(width.value)
|
||||||
|
? `${width.value}px`
|
||||||
|
: width.value,
|
||||||
|
height:
|
||||||
|
typeof height.value === 'number' || /^\d+$/.test(height.value)
|
||||||
|
? `${height.value}px`
|
||||||
|
: height.value,
|
||||||
|
transform: `translate(${position.value.x}px, ${position.value.y}px)`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Draggable header */
|
||||||
|
.dialog-header {
|
||||||
|
padding: 8px 0;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollable content */
|
||||||
|
.scrollArea {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize handle in bottom right */
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
cursor: nwse-resize;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
115
src/vueLib/general/SearchableSelect .vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<q-select
|
||||||
|
ref="selectRef"
|
||||||
|
v-model="modelValueLocal"
|
||||||
|
:options="filteredOptions"
|
||||||
|
:option-label="optionLabel"
|
||||||
|
:option-value="optionValue"
|
||||||
|
:use-input="search"
|
||||||
|
input-debounce="200"
|
||||||
|
@filter="filterFn"
|
||||||
|
@update:model-value="emitValue"
|
||||||
|
v-bind="$attrs"
|
||||||
|
><template v-slot:append
|
||||||
|
><q-btn
|
||||||
|
size="xs"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
round
|
||||||
|
icon="search"
|
||||||
|
:color="search ? 'primary' : 'grey'"
|
||||||
|
@click="searchBox"
|
||||||
|
>
|
||||||
|
</q-btn
|
||||||
|
></template>
|
||||||
|
</q-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts" generic="T extends Record<string, string | number | object>">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const search = ref(false);
|
||||||
|
const selectRef = ref();
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number, Object] as PropType<string | number | object | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array as PropType<T[]>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
optionLabel: {
|
||||||
|
type: [Function, String] as PropType<((option: T) => string) | string | undefined>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
optionValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | number | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const optionLabel = props.optionLabel;
|
||||||
|
const optionValue = props.optionValue;
|
||||||
|
|
||||||
|
const modelValueLocal = ref<string | number | object | null | undefined>(props.modelValue);
|
||||||
|
const filteredOptions = ref<T[]>([...(props.options || [])]);
|
||||||
|
|
||||||
|
function searchBox() {
|
||||||
|
if (search.value) {
|
||||||
|
selectRef.value.updateInputValue('');
|
||||||
|
}
|
||||||
|
search.value = !search.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(v) => (modelValueLocal.value = v),
|
||||||
|
);
|
||||||
|
watch(
|
||||||
|
() => props.options,
|
||||||
|
(o) => (filteredOptions.value = [...o]),
|
||||||
|
);
|
||||||
|
|
||||||
|
function filterFn(val: string, update: (fn: () => void) => void) {
|
||||||
|
update(() => {
|
||||||
|
if (!val) {
|
||||||
|
filteredOptions.value = [...props.options];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const needle = val.toLowerCase();
|
||||||
|
filteredOptions.value = props.options.filter((opt) => {
|
||||||
|
let field = null;
|
||||||
|
if (typeof optionLabel === 'function') {
|
||||||
|
field = optionLabel(opt);
|
||||||
|
} else {
|
||||||
|
field = opt[String(optionLabel)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof field !== 'string' && typeof field !== 'number') return false;
|
||||||
|
return String(field).toLowerCase().includes(needle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove duplicates by optionValue
|
||||||
|
const seen = new Set();
|
||||||
|
filteredOptions.value = filteredOptions.value.filter((opt) => {
|
||||||
|
const value = opt[optionValue];
|
||||||
|
if (seen.has(value)) return false;
|
||||||
|
seen.add(value);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitValue(val: string | number | null) {
|
||||||
|
emit('update:modelValue', val);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
12
src/vueLib/general/SiteTitle.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<h4 class="text-primary text-bold text-center" :style="{ margin: props.marging + 'px' }">
|
||||||
|
{{ props.title }}
|
||||||
|
</h4>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
title: { type: String },
|
||||||
|
marging: { type: Number, default: 28 },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
94
src/vueLib/general/useNotify.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { getCurrentInstance } from 'vue';
|
||||||
|
import type { Response } from '../models/Response';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
export function useNotify() {
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
const $q = instance?.appContext.config.globalProperties.$q;
|
||||||
|
|
||||||
|
function NotifyResponse(
|
||||||
|
response: Response | string | AxiosError | undefined,
|
||||||
|
type?: 'warning' | 'error',
|
||||||
|
timeout: number = 5000,
|
||||||
|
) {
|
||||||
|
let color = 'green';
|
||||||
|
let icon = 'check_circle';
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'warning':
|
||||||
|
color = 'orange';
|
||||||
|
icon = 'warning';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
color = 'red';
|
||||||
|
icon = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
let message = '';
|
||||||
|
if (response instanceof AxiosError && response.response) {
|
||||||
|
if (response.response.data) {
|
||||||
|
const data = response.response.data as Response;
|
||||||
|
message = data.message;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = typeof response === 'string' ? response : (response.message ?? '');
|
||||||
|
}
|
||||||
|
if (message === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = typeof response === 'string' ? color : type === 'error' ? 'red' : color;
|
||||||
|
icon = typeof response === 'string' ? icon : type === 'error' ? 'error' : icon;
|
||||||
|
if (!$q) {
|
||||||
|
console.error(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$q?.notify({
|
||||||
|
message: message,
|
||||||
|
color: color,
|
||||||
|
position: 'top',
|
||||||
|
icon: icon,
|
||||||
|
timeout: timeout,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
dense: true,
|
||||||
|
round: true,
|
||||||
|
handler: () => {
|
||||||
|
/* just closes */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotifyDialog(title: string, text: string, okText?: string, cancelText?: string) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
$q
|
||||||
|
?.dialog({
|
||||||
|
title: title,
|
||||||
|
message: text,
|
||||||
|
persistent: true,
|
||||||
|
ok: okText ?? 'OK',
|
||||||
|
cancel: cancelText ?? 'CANCEL',
|
||||||
|
})
|
||||||
|
.onOk(() => {
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.onCancel(() => {
|
||||||
|
resolve(false);
|
||||||
|
})
|
||||||
|
.onDismiss(() => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
NotifyDialog,
|
||||||
|
NotifyResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
85
src/vueLib/login/ChangePassword.vue
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="$t('changePassword') + ' ' + $t('user') + ' ' + localUser.user"
|
||||||
|
:height="510"
|
||||||
|
:width="500"
|
||||||
|
:inner-padding="48"
|
||||||
|
>
|
||||||
|
<q-form ref="form">
|
||||||
|
<div class="row justify-center q-gutter-md">
|
||||||
|
<q-input
|
||||||
|
class="col-6 required"
|
||||||
|
filled
|
||||||
|
autocomplete="current-password"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
:label="$t('currentPassword')"
|
||||||
|
v-model="localUser.password"
|
||||||
|
:rules="[(val) => !!val || $t('currentPassword') + ' ' + $t('isRequired')]"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="showPassword ? 'visibility_off' : 'visibility'"
|
||||||
|
@mousedown.prevent="showPassword = true"
|
||||||
|
@mouseup.prevent="showPassword = false"
|
||||||
|
@mouseleave.prevent="showPassword = false"
|
||||||
|
@touchstart.prevent="showPassword = true"
|
||||||
|
@touchend.prevent="showPassword = false"
|
||||||
|
@touchcancel.prevent="showPassword = false"
|
||||||
|
></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<div class="col-6">
|
||||||
|
<EnterNewPassword v-model:password="localUser.newPassword!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<q-btn class="q-ma-md" color="primary" no-caps @click="save">{{ $t('save') }}</q-btn>
|
||||||
|
</div>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import DialogFrame from '../dialog/DialogFrame.vue';
|
||||||
|
import type { User } from 'src/vueLib/models/users';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import { validateQForm } from '../utils/validation';
|
||||||
|
import EnterNewPassword from './EnterNewPassword.vue';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const dialog = ref();
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const form = ref();
|
||||||
|
const localUser = ref<User>({
|
||||||
|
user: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const open = (user: User) => {
|
||||||
|
localUser.value = user;
|
||||||
|
localUser.value.password = '';
|
||||||
|
localUser.value.newPassword = '';
|
||||||
|
dialog.value?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
dialog.value?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:password']);
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!(await validateQForm(form.value))) {
|
||||||
|
NotifyResponse(i18n.global.t('notAllRequiredFieldsFilled'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit('update:password', localUser.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open, close });
|
||||||
|
</script>
|
||||||
129
src/vueLib/login/EnterNewPassword.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-input
|
||||||
|
class="col-5 required"
|
||||||
|
:label="$t('password')"
|
||||||
|
autocomplete="new-password"
|
||||||
|
filled
|
||||||
|
:lazy-rules="false"
|
||||||
|
:rules="[validatePassword]"
|
||||||
|
v-model="password"
|
||||||
|
@update:model-value="checkStrength"
|
||||||
|
:type="showPassword1 ? 'text' : 'password'"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="showPassword1 ? 'visibility_off' : 'visibility'"
|
||||||
|
@mousedown.prevent="showPassword1 = true"
|
||||||
|
@mouseup.prevent="showPassword1 = false"
|
||||||
|
@mouseleave.prevent="showPassword1 = false"
|
||||||
|
@touchstart.prevent="showPassword1 = true"
|
||||||
|
@touchend.prevent="showPassword1 = false"
|
||||||
|
@touchcancel.prevent="showPassword1 = false"
|
||||||
|
></q-btn>
|
||||||
|
<q-icon :name="strengthIcon" :color="strengthColor"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
|
||||||
|
<div class="q-mt-md q-px-xl">
|
||||||
|
<q-linear-progress :value="strengthValue" :color="strengthColor" size="8px" rounded />
|
||||||
|
<div class="text-caption text-center q-mt-xs">
|
||||||
|
{{ strengthLabel }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-input
|
||||||
|
ref="pwdForm2"
|
||||||
|
class="col-5 required"
|
||||||
|
:label="$t('password')"
|
||||||
|
autocomplete="new-password"
|
||||||
|
filled
|
||||||
|
:type="showPassword2 ? 'text' : 'password'"
|
||||||
|
:rules="[checkSamePassword]"
|
||||||
|
v-model="passwordCheck"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="showPassword2 ? 'visibility_off' : 'visibility'"
|
||||||
|
@mousedown.prevent="showPassword2 = true"
|
||||||
|
@mouseup.prevent="showPassword2 = false"
|
||||||
|
@mouseleave.prevent="showPassword2 = false"
|
||||||
|
@touchstart.prevent="showPassword2 = true"
|
||||||
|
@touchend.prevent="showPassword2 = false"
|
||||||
|
@touchcancel.prevent="showPassword2 = false"
|
||||||
|
></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import zxcvbn from 'zxcvbn';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
password: { type: String },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:password']);
|
||||||
|
|
||||||
|
const pwdForm2 = ref();
|
||||||
|
const showPassword1 = ref(false);
|
||||||
|
const showPassword2 = ref(false);
|
||||||
|
const passwordCheck = ref('');
|
||||||
|
const strengthValue = ref(0);
|
||||||
|
const strengthLabel = ref('Enter a password');
|
||||||
|
const strengthColor = ref('grey');
|
||||||
|
const strengthIcon = ref('lock');
|
||||||
|
|
||||||
|
function checkStrength() {
|
||||||
|
const result = zxcvbn(password.value || '');
|
||||||
|
strengthValue.value = (result.score + 1) / 5;
|
||||||
|
const levels = [
|
||||||
|
i18n.global.t('veryWeak'),
|
||||||
|
i18n.global.t('weak'),
|
||||||
|
i18n.global.t('fair'),
|
||||||
|
i18n.global.t('good'),
|
||||||
|
i18n.global.t('strong'),
|
||||||
|
];
|
||||||
|
const colors = ['red', 'orange', 'yellow', 'light-green', 'green'];
|
||||||
|
const icon = ['lock', 'warning', 'error_outline', 'check_circle_outline', 'verified_user'];
|
||||||
|
|
||||||
|
strengthLabel.value = levels[result.score] || i18n.global.t('veryWeak');
|
||||||
|
strengthColor.value = colors[result.score] || 'grey';
|
||||||
|
strengthIcon.value = icon[result.score] || 'lock';
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePassword(): string | boolean {
|
||||||
|
pwdForm2.value?.validate();
|
||||||
|
|
||||||
|
if (!password.value) return i18n.global.t('passwordIsRequired');
|
||||||
|
|
||||||
|
if (password.value.length < 8) {
|
||||||
|
return i18n.global.t('passwordTooShort');
|
||||||
|
} else if (!/[A-Z]/.test(password.value)) {
|
||||||
|
return i18n.global.t('passwordNeedsUppercase');
|
||||||
|
} else if (!/[a-z]/.test(password.value)) {
|
||||||
|
return i18n.global.t('passwordNeedsLowercase');
|
||||||
|
} else if (!/[0-9]/.test(password.value)) {
|
||||||
|
return i18n.global.t('passwordNeedsNumber');
|
||||||
|
} else if (!/[!@#$%^&*(),.?":{}|<>]/.test(password.value)) {
|
||||||
|
return i18n.global.t('passwordNeedsSpecial');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSamePassword(): string | boolean {
|
||||||
|
if (password.value === passwordCheck.value) return true;
|
||||||
|
return i18n.global.t('passwordDoNotMatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = computed({
|
||||||
|
get: () => props.password,
|
||||||
|
set: (v) => emit('update:password', v),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
57
src/vueLib/login/LoginDialog.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame ref="Dialog" :width="300" :height="380" header-title="Login">
|
||||||
|
<LoginForm v-on:update-close="close" />
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, nextTick } from 'vue';
|
||||||
|
import DialogFrame from '../dialog/DialogFrame.vue';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import LoginForm from './LoginForm.vue';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const Dialog = ref();
|
||||||
|
const refUserInput = ref();
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
Dialog.value?.open();
|
||||||
|
nextTick(() => {
|
||||||
|
refUserInput.value?.focus();
|
||||||
|
}).catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
Dialog.value.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes shake {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateX(-6px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateX(6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.4s ease;
|
||||||
|
border: 2px solid #f44336;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
114
src/vueLib/login/LoginForm.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-black"></div>
|
||||||
|
<q-form ref="refForm">
|
||||||
|
<q-item-section class="q-gutter-md q-pa-md">
|
||||||
|
<q-card :class="['q-gutter-xs q-items-center q-pa-lg', { shake: shake }]">
|
||||||
|
<div class="text-h5 text-primary text-center">{{ $t(appName) }}</div>
|
||||||
|
<q-input
|
||||||
|
ref="refUserInput"
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autocomplete="username"
|
||||||
|
type="text"
|
||||||
|
:label="$t('user')"
|
||||||
|
v-model="user"
|
||||||
|
:rules="[(val) => !!val || $t('user') + ' ' + $t('isRequired')]"
|
||||||
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
dense
|
||||||
|
filled
|
||||||
|
autocomplete="current-password"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
:label="$t('password')"
|
||||||
|
v-model="password"
|
||||||
|
@keyup.enter="onSubmit"
|
||||||
|
:rules="[(val) => !!val || $t('password') + ' ' + $t('isRequired')]"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:icon="showPassword ? 'visibility_off' : 'visibility'"
|
||||||
|
@mousedown.prevent="showPassword = true"
|
||||||
|
@mouseup.prevent="showPassword = false"
|
||||||
|
@mouseleave.prevent="showPassword = false"
|
||||||
|
@touchstart.prevent="showPassword = true"
|
||||||
|
@touchend.prevent="showPassword = false"
|
||||||
|
@touchcancel.prevent="showPassword = false"
|
||||||
|
></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
<div class="q-pt-sm q-mr-md row justify-end">
|
||||||
|
<q-btn no-caps color="primary" :label="$t('login')" @click="onSubmit"></q-btn>
|
||||||
|
</div>
|
||||||
|
</q-card>
|
||||||
|
</q-item-section>
|
||||||
|
</q-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { appName } from '../models/settings';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import { useLogin } from './useLogin';
|
||||||
|
|
||||||
|
const refForm = ref();
|
||||||
|
const refUserInput = ref();
|
||||||
|
const user = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const showPassword = ref(false);
|
||||||
|
const shake = ref(false);
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const { login } = useLogin();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update-close']);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const valid = refForm.value?.validate();
|
||||||
|
if (!valid) {
|
||||||
|
NotifyResponse('error submitting login form', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await login(user.value, password.value)
|
||||||
|
.then(() => {
|
||||||
|
NotifyResponse("logged in as '" + user.value + "'");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
shake.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
shake.value = false;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
emit('update-close');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes shake {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateX(8px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateX(-6px);
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
transform: translateX(6px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shake {
|
||||||
|
animation: shake 0.4s ease;
|
||||||
|
border: 2px solid #f44336;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
src/vueLib/login/LoginMenu.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="q-gutter-md">
|
||||||
|
<q-btn dense flat round icon="person" :color="currentUser ? 'green' : ''">
|
||||||
|
<q-menu ref="refLoginMenu">
|
||||||
|
<q-list style="min-width: 120px">
|
||||||
|
<q-item v-if="user.user" class="text-primary">{{ currentUser?.username }}</q-item>
|
||||||
|
<q-item v-if="showLogin" clickable v-close-popup @click="openLogin">
|
||||||
|
<q-item-section class="text-primary">{{ loginText }}</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-btn flat :icon="darkMode" @click="toggleDarkMode"
|
||||||
|
><q-tooltip>{{ $t(darkMode) }}</q-tooltip></q-btn
|
||||||
|
>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-select
|
||||||
|
:label="$t('language')"
|
||||||
|
borderless
|
||||||
|
color="primary"
|
||||||
|
dense
|
||||||
|
v-model="langSelected"
|
||||||
|
:options="langSelection"
|
||||||
|
><q-tooltip>{{ $t('language') }}</q-tooltip></q-select
|
||||||
|
>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="
|
||||||
|
(autorized || user.isPermittedTo('settings', 'read')) && route.path !== '/settings'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-btn flat color="secondary" icon="settings" to="/settings"></q-btn>
|
||||||
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="
|
||||||
|
(autorized || user.isPermittedTo('userSettings', 'read')) &&
|
||||||
|
route.path !== '/userSettings'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<q-btn flat color="secondary" icon="group" to="/userSettings"></q-btn>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
<LoginDialog ref="refLoginDialog"></LoginDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LoginDialog from './LoginDialog.vue';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import { lang, i18n } from 'src/boot/lang';
|
||||||
|
import { useUserStore } from './userStore';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { Dark } from 'quasar';
|
||||||
|
import { useLogin } from './useLogin';
|
||||||
|
import { setLocalDarkMode, setLocalLanguage } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
const userLogin = useLogin();
|
||||||
|
const route = useRoute();
|
||||||
|
const refLoginDialog = ref();
|
||||||
|
const user = useUserStore();
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const currentUser = computed(() => user.user);
|
||||||
|
const darkMode = computed(() => {
|
||||||
|
if (Dark.mode) {
|
||||||
|
return 'light_mode';
|
||||||
|
}
|
||||||
|
return 'dark_mode';
|
||||||
|
});
|
||||||
|
const showLogin = computed(
|
||||||
|
() => (route.path !== '/' && route.path !== '/login') || currentUser.value?.username === '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const autorized = computed(() => !!user.isAuthorizedAs(['admin']));
|
||||||
|
|
||||||
|
// switch between logged in or logged out text
|
||||||
|
const loginText = computed(() => {
|
||||||
|
return currentUser.value ? i18n.global.t('logout') : i18n.global.t('login');
|
||||||
|
});
|
||||||
|
|
||||||
|
//switch between dark and light mode and save it in localStorage
|
||||||
|
function toggleDarkMode() {
|
||||||
|
Dark.toggle();
|
||||||
|
setLocalDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// opens login page if no user is logged in otherwise it serves as logout
|
||||||
|
function openLogin() {
|
||||||
|
if (currentUser.value) {
|
||||||
|
userLogin.logout().catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refLoginDialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
const langSelected = ref(i18n.global.locale);
|
||||||
|
const langSelection = ref(lang);
|
||||||
|
|
||||||
|
// Watch for changes and update i18n locale
|
||||||
|
watch(langSelected, (newLang) => {
|
||||||
|
i18n.global.locale = newLang;
|
||||||
|
|
||||||
|
setLocalLanguage(newLang);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
101
src/vueLib/login/useLogin.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { useUserStore } from './userStore';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import type { Settings } from '../models/settings';
|
||||||
|
import { appName, logo } from '../models/settings';
|
||||||
|
import { clearLocalStorage, setLocalSettings } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
const refreshTime = 10000;
|
||||||
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
export function useLogin() {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
async function login(user: string, password: string) {
|
||||||
|
try {
|
||||||
|
await appApi.post('/login', { user, password }).then((resp) => {
|
||||||
|
const sets = resp.data.settings as Settings;
|
||||||
|
|
||||||
|
if (sets.icon) {
|
||||||
|
logo.value = sets.icon;
|
||||||
|
}
|
||||||
|
if (sets.appName) {
|
||||||
|
appName.value = sets.appName;
|
||||||
|
}
|
||||||
|
document.documentElement.style.setProperty('--q-primary', sets.primaryColor);
|
||||||
|
document.documentElement.style.setProperty('--q-primary-text', sets.primaryColorText);
|
||||||
|
document.documentElement.style.setProperty('--q-secondary', sets.secondaryColor);
|
||||||
|
document.documentElement.style.setProperty('--q-secondary-text', sets.secondaryColorText);
|
||||||
|
setLocalSettings(sets);
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await appApi.get('/login/me');
|
||||||
|
await userStore
|
||||||
|
.setUser({
|
||||||
|
id: resp.data.id,
|
||||||
|
username: resp.data.user,
|
||||||
|
role: { role: resp.data.role, permissions: [] },
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
|
||||||
|
startRefreshInterval();
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
await appApi.get('/logout').catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
});
|
||||||
|
|
||||||
|
userStore.clearUser();
|
||||||
|
clearLocalStorage();
|
||||||
|
stopRefreshInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
await appApi
|
||||||
|
.post('login/refresh', {}, { withCredentials: true })
|
||||||
|
.then(() => {
|
||||||
|
appApi
|
||||||
|
.get('/login/me')
|
||||||
|
.then((resp) => {
|
||||||
|
userStore
|
||||||
|
.setUser({
|
||||||
|
id: resp.data.id,
|
||||||
|
username: resp.data.user,
|
||||||
|
role: { role: resp.data.role, permissions: [] },
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
if (!intervalId) {
|
||||||
|
startRefreshInterval();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
userStore.clearUser();
|
||||||
|
});
|
||||||
|
stopRefreshInterval();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRefreshInterval() {
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
refresh().catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
}, refreshTime);
|
||||||
|
}
|
||||||
|
function stopRefreshInterval() {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { login, logout, refresh };
|
||||||
|
}
|
||||||
112
src/vueLib/login/userStore.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { useGlobalRouter } from 'src/vueLib/utils/globalRouter';
|
||||||
|
import { useGlobalQ } from 'src/vueLib/utils/globalQ';
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { useNotify } from '../general/useNotify';
|
||||||
|
import type { Role } from '../models/roles';
|
||||||
|
import type { UserState, User } from '../models/user';
|
||||||
|
import type { Permission } from '../checkboxes/permissions';
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: (): UserState => ({
|
||||||
|
user: null,
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isAuthenticated: (state: UserState): boolean => {
|
||||||
|
return !!state.user;
|
||||||
|
},
|
||||||
|
|
||||||
|
isAuthorizedAs: (state: UserState) => {
|
||||||
|
return (roles: string[]) => {
|
||||||
|
return state.user !== null && roles.includes(state.user.role?.role);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
isPermittedTo: (state: UserState) => {
|
||||||
|
return (name: string, type: 'read' | 'write' | 'delete' | 'import' | 'export'): boolean => {
|
||||||
|
const permission = state.user?.permissions?.find((r: Permission) => r.name === name);
|
||||||
|
switch (type) {
|
||||||
|
case 'read':
|
||||||
|
return permission?.permission ? (permission.permission & (1 << 0)) === 1 : false;
|
||||||
|
case 'write':
|
||||||
|
return permission?.permission ? (permission.permission & (1 << 1)) === 2 : false;
|
||||||
|
case 'delete':
|
||||||
|
return permission?.permission ? (permission.permission & (1 << 2)) === 4 : false;
|
||||||
|
case 'import':
|
||||||
|
return permission?.permission ? (permission.permission & (1 << 3)) === 8 : false;
|
||||||
|
case 'export':
|
||||||
|
return permission?.permission ? (permission.permission & (1 << 4)) === 16 : false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async setUser(user: User) {
|
||||||
|
await appApi
|
||||||
|
.get('roles?role=' + user.role?.role)
|
||||||
|
.then((resp) => {
|
||||||
|
const roleData = resp.data.find((role: Role) => role.role === user.role?.role);
|
||||||
|
user.permissions = roleData?.permissions || [];
|
||||||
|
this.user = user;
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'));
|
||||||
|
},
|
||||||
|
clearUser() {
|
||||||
|
const $q = useGlobalQ();
|
||||||
|
|
||||||
|
if (!this.user) return;
|
||||||
|
if ($q) {
|
||||||
|
$q?.notify({
|
||||||
|
message: "user '" + this.user?.username + "' logged out",
|
||||||
|
color: 'orange',
|
||||||
|
position: 'top',
|
||||||
|
icon: 'warning',
|
||||||
|
timeout: 5000,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
dense: true,
|
||||||
|
round: true,
|
||||||
|
handler: () => {
|
||||||
|
/* just closes */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("user '" + this.user?.username + "' logged out");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.user = null;
|
||||||
|
|
||||||
|
const router = useGlobalRouter();
|
||||||
|
|
||||||
|
router?.push('/').catch((err) => {
|
||||||
|
if ($q) {
|
||||||
|
$q?.notify({
|
||||||
|
message: err,
|
||||||
|
color: 'orange',
|
||||||
|
position: 'top',
|
||||||
|
icon: 'warning',
|
||||||
|
timeout: 5000,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'close',
|
||||||
|
color: 'white',
|
||||||
|
dense: true,
|
||||||
|
round: true,
|
||||||
|
handler: () => {
|
||||||
|
/* just closes */
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("user '" + this.user?.username + "' logged out");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
3
src/vueLib/models/Response.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface Response {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
11
src/vueLib/models/event.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Members } from './member';
|
||||||
|
|
||||||
|
export interface Event {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
date?: string;
|
||||||
|
day?: string;
|
||||||
|
attendees: Members;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Events = Event[];
|
||||||
6
src/vueLib/models/group.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface Group {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Groups = Group[];
|
||||||
24
src/vueLib/models/member.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import type { Group } from './group';
|
||||||
|
import type { Responsible } from './responsible';
|
||||||
|
|
||||||
|
export interface Member {
|
||||||
|
id: number;
|
||||||
|
memberId?: number;
|
||||||
|
responsibleId?: number | undefined;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
birthday?: string;
|
||||||
|
age?: string;
|
||||||
|
comment?: string;
|
||||||
|
address?: string;
|
||||||
|
town?: string;
|
||||||
|
zip?: string;
|
||||||
|
phone?: string;
|
||||||
|
email?: string;
|
||||||
|
group?: Group;
|
||||||
|
responsible?: Responsible;
|
||||||
|
firstVisit?: string;
|
||||||
|
lastVisit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Members = Member[];
|
||||||
12
src/vueLib/models/metaData.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export interface MetaData {
|
||||||
|
rowIndex: number;
|
||||||
|
seperator: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
birthday: string;
|
||||||
|
address: string;
|
||||||
|
town: string;
|
||||||
|
zip: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
9
src/vueLib/models/responsible.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Member } from './member';
|
||||||
|
|
||||||
|
export interface Responsible {
|
||||||
|
id: number;
|
||||||
|
memberId: number;
|
||||||
|
member: Member;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Responsibles = Responsible[];
|
||||||
9
src/vueLib/models/roles.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Permissions } from '../checkboxes/permissions';
|
||||||
|
|
||||||
|
export interface Role {
|
||||||
|
id?: number;
|
||||||
|
role: string;
|
||||||
|
permissions: Permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Roles = Role[];
|
||||||
27
src/vueLib/models/settings.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const logo = ref('');
|
||||||
|
export const appName = ref('Attendance Records');
|
||||||
|
export const databaseName = ref('members.dba');
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
appName: string;
|
||||||
|
icon: string;
|
||||||
|
databaseName: string;
|
||||||
|
primaryColor: string;
|
||||||
|
primaryColorText: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
secondaryColorText: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DefaultSettings(): Settings {
|
||||||
|
return {
|
||||||
|
appName: 'Attendance Records',
|
||||||
|
icon: '',
|
||||||
|
databaseName: 'members.dba',
|
||||||
|
primaryColor: document.documentElement.style.getPropertyValue('--q-primary-text'),
|
||||||
|
primaryColorText: document.documentElement.style.getPropertyValue('--q-primary'),
|
||||||
|
secondaryColor: document.documentElement.style.getPropertyValue('--q-secondary'),
|
||||||
|
secondaryColorText: document.documentElement.style.getPropertyValue('--q-secondary-text'),
|
||||||
|
};
|
||||||
|
}
|
||||||
15
src/vueLib/models/user.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Permissions } from '../checkboxes/permissions';
|
||||||
|
import type { Role } from './roles';
|
||||||
|
import type { Settings } from './settings';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
role: Role;
|
||||||
|
permissions?: Permissions;
|
||||||
|
settings?: Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserState {
|
||||||
|
user: User | null;
|
||||||
|
}
|
||||||
15
src/vueLib/models/users.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Role } from './roles';
|
||||||
|
import type { Settings } from './settings';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id?: number;
|
||||||
|
user: string;
|
||||||
|
email: string;
|
||||||
|
role?: Role;
|
||||||
|
expiration?: string;
|
||||||
|
password?: string;
|
||||||
|
newPassword?: string;
|
||||||
|
settings?: Settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Users = User[];
|
||||||
82
src/vueLib/tables/attendees/AttendeesTable.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import type { Members } from 'src/vueLib/models/member';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { i18n } from 'boot/lang';
|
||||||
|
import type { Events } from 'src/vueLib/models/event';
|
||||||
|
|
||||||
|
export function useAttendeesTable() {
|
||||||
|
const attendees = ref<Members>([]);
|
||||||
|
|
||||||
|
const pagination = ref({
|
||||||
|
sortBy: 'firstName',
|
||||||
|
descending: false,
|
||||||
|
page: 1,
|
||||||
|
rowsPerPage: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
{
|
||||||
|
name: 'firstName',
|
||||||
|
align: 'left' as const,
|
||||||
|
label: i18n.global.t('prename'),
|
||||||
|
field: 'firstName',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastName',
|
||||||
|
align: 'left' as const,
|
||||||
|
label: i18n.global.t('lastName'),
|
||||||
|
field: 'lastName',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
//updates Attendees list from database
|
||||||
|
async function updateAttendees(eventArray: number) {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
let events: Events | undefined;
|
||||||
|
|
||||||
|
await appApi
|
||||||
|
.get('events')
|
||||||
|
.then((resp) => {
|
||||||
|
if (resp.data === null) {
|
||||||
|
attendees.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
events = resp.data as Events;
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch((err) => {
|
||||||
|
NotifyResponse(err, 'error');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!events ||
|
||||||
|
events.length === 0 ||
|
||||||
|
!events[eventArray]?.attendees ||
|
||||||
|
events[eventArray].attendees === null
|
||||||
|
) {
|
||||||
|
attendees.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attendees.value = events[eventArray].attendees ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
attendees,
|
||||||
|
pagination,
|
||||||
|
columns,
|
||||||
|
loading,
|
||||||
|
updateAttendees,
|
||||||
|
};
|
||||||
|
}
|
||||||
229
src/vueLib/tables/attendees/AttendeesTable.vue
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
bordered
|
||||||
|
ref="tableRef"
|
||||||
|
:title="$t('attendees')"
|
||||||
|
title-class="text-bold text-blue-9"
|
||||||
|
:no-data-label="$t('noDataAvailable')"
|
||||||
|
:loading-label="$t('loading')"
|
||||||
|
:rows-per-page-label="$t('recordsPerPage')"
|
||||||
|
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
|
||||||
|
:rows="attendees"
|
||||||
|
:columns="columns"
|
||||||
|
row-key="id"
|
||||||
|
v-model:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
:filter="filter"
|
||||||
|
:selection="selectOption ? 'multiple' : 'none'"
|
||||||
|
v-model:selected="selected"
|
||||||
|
binary-state-sort
|
||||||
|
dense
|
||||||
|
class="bigger-table-text"
|
||||||
|
>
|
||||||
|
<!--top left of event table-->
|
||||||
|
<template v-slot:top-left>
|
||||||
|
<q-btn-group push flat style="color: grey">
|
||||||
|
<q-btn
|
||||||
|
v-if="user.isPermittedTo('events', 'write')"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="person"
|
||||||
|
@click="openAttendanceDialog"
|
||||||
|
><q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
|
||||||
|
<q-tooltip>{{ $t('addNewAttendees') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="user.isPermittedTo('events', 'write')"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
style="color: grey"
|
||||||
|
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
|
||||||
|
@click="selectOption = !selectOption"
|
||||||
|
>
|
||||||
|
<q-tooltip>{{ $t('selectAttendeesOptions') }}</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</q-btn-group>
|
||||||
|
<div v-if="selectOption && selected.length > 0">
|
||||||
|
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
|
||||||
|
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
v-close-popup
|
||||||
|
@click="openRemoveDialog(...selected)"
|
||||||
|
class="text-negative"
|
||||||
|
>{{ $t('delete') }}</q-item
|
||||||
|
>
|
||||||
|
</q-menu>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectOption && selected.length > 0" class="q-ml-md text-weight-bold">
|
||||||
|
{{ $t('selected') }}: {{ selected.length }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!--top right of event table-->
|
||||||
|
<template v-slot:top-right>
|
||||||
|
<q-input filled dense debounce="300" v-model="filter" :placeholder="$t('search')">
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<!--body of event table-->
|
||||||
|
<template v-slot:body-cell="props">
|
||||||
|
<q-td v-if="props.col.field === 'attendees'" :props="props">
|
||||||
|
<q-btn v-if="props.value !== null && props.value.length > 0" dense flat icon="people"
|
||||||
|
><q-badge color="primary" text-color="primary-text" floating transparent>{{
|
||||||
|
props.row.attendees.length
|
||||||
|
}}</q-badge></q-btn
|
||||||
|
>
|
||||||
|
</q-td>
|
||||||
|
<q-td v-else :props="props">
|
||||||
|
{{ props.value }}
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body-cell-option="props">
|
||||||
|
<q-td :props="props">
|
||||||
|
<q-btn
|
||||||
|
v-if="user.isPermittedTo('events', 'delete')"
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
@click="openRemoveDialog(props.row)"
|
||||||
|
color="negative"
|
||||||
|
icon="delete"
|
||||||
|
/>
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
<DialogFrame ref="attendanceDialog" :header-title="$t('members')" :width="700" :height="500">
|
||||||
|
<MembersTable
|
||||||
|
add-attendees
|
||||||
|
:compare-members="attendees"
|
||||||
|
v-on:update-event="(val) => updateTable(val)"
|
||||||
|
:event-id="localEvent?.id ?? 0"
|
||||||
|
/>
|
||||||
|
</DialogFrame>
|
||||||
|
|
||||||
|
<OkDialog
|
||||||
|
ref="okDialog"
|
||||||
|
:dialog-label="$t('delete')"
|
||||||
|
:text="$t('doYouWantToDelete') + ' ' + deleteText"
|
||||||
|
label-color="red"
|
||||||
|
:button-cancel-label="$t('cancel')"
|
||||||
|
:button-ok-label="$t('confirm')"
|
||||||
|
:button-ok-flat="false"
|
||||||
|
button-ok-color="red"
|
||||||
|
v-on:update-confirm="(val) => removeAttendees(...val)"
|
||||||
|
></OkDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
//imports
|
||||||
|
import { appApi } from 'src/boot/axios';
|
||||||
|
import { onMounted, type PropType, ref } from 'vue';
|
||||||
|
import type { Members } from 'src/vueLib/models/member';
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import OkDialog from 'src/components/dialog/OkDialog.vue';
|
||||||
|
import { useNotify } from 'src/vueLib/general/useNotify';
|
||||||
|
import { useAttendeesTable } from '../attendees/AttendeesTable';
|
||||||
|
import { useUserStore } from 'src/vueLib/login/userStore';
|
||||||
|
import type { Event } from 'src/vueLib/models/event';
|
||||||
|
import MembersTable from '../members/MembersTable.vue';
|
||||||
|
import { i18n } from 'src/boot/lang';
|
||||||
|
|
||||||
|
//use constants and function of imports
|
||||||
|
const { attendees, pagination, loading, columns, updateAttendees } = useAttendeesTable();
|
||||||
|
|
||||||
|
//define given properties
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object as PropType<Event>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
eventArray: { type: Number, default: 0, required: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// define emits
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
// definitions
|
||||||
|
const { NotifyResponse } = useNotify();
|
||||||
|
const attendanceDialog = ref();
|
||||||
|
const okDialog = ref();
|
||||||
|
const deleteText = ref('');
|
||||||
|
const selectOption = ref(false);
|
||||||
|
const localEvent = ref<Event>();
|
||||||
|
const selected = ref<Members>([]);
|
||||||
|
const openSubmenu = ref(false);
|
||||||
|
const filter = ref('');
|
||||||
|
const user = useUserStore();
|
||||||
|
|
||||||
|
// load at page mount
|
||||||
|
onMounted(() => {
|
||||||
|
localEvent.value = props.event;
|
||||||
|
attendees.value = props.event.attendees ?? [];
|
||||||
|
});
|
||||||
|
|
||||||
|
//opens attendance dialog
|
||||||
|
function openAttendanceDialog() {
|
||||||
|
attendanceDialog.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
//opens remove dialog
|
||||||
|
function openRemoveDialog(...attendees: Members) {
|
||||||
|
if (attendees.length === 1) {
|
||||||
|
deleteText.value = "'";
|
||||||
|
if (attendees[0]?.firstName && attendees[0]?.lastName) {
|
||||||
|
deleteText.value += attendees[0]?.firstName + ' ' + attendees[0]?.lastName;
|
||||||
|
}
|
||||||
|
deleteText.value += "'";
|
||||||
|
} else {
|
||||||
|
deleteText.value = String(attendees.length) + ' ' + i18n.global.t('attendees');
|
||||||
|
}
|
||||||
|
|
||||||
|
okDialog.value?.open(attendees);
|
||||||
|
}
|
||||||
|
|
||||||
|
//remove attendees from database
|
||||||
|
async function removeAttendees(...removeAttendees: Members) {
|
||||||
|
if (!localEvent.value) {
|
||||||
|
NotifyResponse('event is empty', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localEvent.value.attendees = removeAttendees;
|
||||||
|
|
||||||
|
await appApi
|
||||||
|
.post('events/delete/attendees', localEvent.value)
|
||||||
|
.then(() => {
|
||||||
|
selected.value = [];
|
||||||
|
if (localEvent.value?.attendees !== undefined && localEvent.value?.attendees.length > 1) {
|
||||||
|
NotifyResponse(i18n.global.t('deleteAttendees'), 'warning');
|
||||||
|
} else {
|
||||||
|
NotifyResponse(i18n.global.t('deleteAttendee'), 'warning');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => NotifyResponse(err, 'error'))
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
await updateAttendees(props.eventArray);
|
||||||
|
emit('update', -removeAttendees.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTable(amount: number) {
|
||||||
|
await updateAttendees(props.eventArray);
|
||||||
|
emit('update', amount);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bigger-table-text .q-table__middle td {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bigger-table-text .q-table__top,
|
||||||
|
.bigger-table-text .q-table__bottom,
|
||||||
|
.bigger-table-text th {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
src/vueLib/tables/attendees/AttendeesTableDialog.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<DialogFrame
|
||||||
|
ref="dialog"
|
||||||
|
:header-title="$t('attendees')"
|
||||||
|
:width="700"
|
||||||
|
:height="600"
|
||||||
|
:inner-padding="0"
|
||||||
|
>
|
||||||
|
<q-card>
|
||||||
|
<!-- tabs declaration in header-->
|
||||||
|
<q-tabs
|
||||||
|
v-model="tab"
|
||||||
|
dense
|
||||||
|
class="text-grey"
|
||||||
|
active-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
>
|
||||||
|
<q-tab no-caps name="attendance" :label="$t('attendees')"
|
||||||
|
><q-badge color="primary" text-color="primary-text" floating transparent>{{
|
||||||
|
attendees.length
|
||||||
|
}}</q-badge></q-tab
|
||||||
|
>
|
||||||
|
|
||||||
|
<q-tab no-caps name="noneAttendees" :label="$t('noneAttendees')"
|
||||||
|
><q-badge color="primary" text-color="primary-text" floating transparent>{{
|
||||||
|
missingAttendanceAmount
|
||||||
|
}}</q-badge></q-tab
|
||||||
|
>
|
||||||
|
</q-tabs>
|
||||||
|
|
||||||
|
<q-separator />
|
||||||
|
|
||||||
|
<!--tab of attenting members-->
|
||||||
|
|
||||||
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel name="attendance" style="padding: 0px">
|
||||||
|
<AttendeesTable
|
||||||
|
:event="localEvent!"
|
||||||
|
:event-array="eA"
|
||||||
|
v-on:update="(val) => updateTable(val)"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
<!--tab of missing attenting members-->
|
||||||
|
<q-tab-panel name="noneAttendees" style="padding: 0px">
|
||||||
|
<MembersTable
|
||||||
|
add-attendees
|
||||||
|
:compare-members="attendees"
|
||||||
|
v-on:update-event="(val) => updateTable(val)"
|
||||||
|
:event-id="localEvent?.id ?? 0"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
</q-card>
|
||||||
|
</DialogFrame>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// imports
|
||||||
|
import AttendeesTable from './AttendeesTable.vue';
|
||||||
|
import MembersTable from '../members/MembersTable.vue';
|
||||||
|
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
|
||||||
|
import type { Event } from 'src/vueLib/models/event';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useAttendeesTable } from './AttendeesTable';
|
||||||
|
import { useMemberTable } from '../members/MembersTable';
|
||||||
|
import { getLocalPageDefaults } from 'src/localstorage/localStorage';
|
||||||
|
|
||||||
|
//use constants and function of imports
|
||||||
|
const { attendees, updateAttendees } = useAttendeesTable();
|
||||||
|
const { setNewFilter, filteredMembers, updateMembers } = useMemberTable();
|
||||||
|
|
||||||
|
// declare emits
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
// declarations
|
||||||
|
const dialog = ref();
|
||||||
|
const localEvent = ref<Event>();
|
||||||
|
const missingAttendanceAmount = ref(0);
|
||||||
|
const attendanceAmount = ref(0);
|
||||||
|
const eA = ref(0);
|
||||||
|
const tab = ref('attendance');
|
||||||
|
|
||||||
|
// opens attendace dialog of event
|
||||||
|
const open = async (eventArray: number, event: Event) => {
|
||||||
|
// local declarations
|
||||||
|
localEvent.value = event;
|
||||||
|
attendanceAmount.value = event.attendees.length ?? [];
|
||||||
|
eA.value = eventArray;
|
||||||
|
|
||||||
|
// get attendance
|
||||||
|
await updateAttendees(eventArray);
|
||||||
|
//get missing attendance from memer updateTable
|
||||||
|
await updateMembers(event.attendees);
|
||||||
|
|
||||||
|
// set custom filter
|
||||||
|
const defaults = getLocalPageDefaults('attendance');
|
||||||
|
setNewFilter(defaults?.filteredColumn || '', ...(defaults?.filteredValue ?? []));
|
||||||
|
|
||||||
|
// set amount of missing attendace
|
||||||
|
missingAttendanceAmount.value = filteredMembers.value.length;
|
||||||
|
|
||||||
|
//open atttendance dialog
|
||||||
|
dialog.value.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
//updates amount of table display and both tables
|
||||||
|
async function updateTable(amount?: number) {
|
||||||
|
// check wether amount is given and if it adds or substracts amount
|
||||||
|
if (amount && amount < 0) {
|
||||||
|
missingAttendanceAmount.value -= amount;
|
||||||
|
} else if (amount) {
|
||||||
|
missingAttendanceAmount.value = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update attendace table
|
||||||
|
await updateAttendees(eA.value);
|
||||||
|
// update for amoutn display in tab
|
||||||
|
if (localEvent.value) localEvent.value.attendees = attendees.value;
|
||||||
|
//emit to update event table
|
||||||
|
emit('update');
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
31
src/vueLib/tables/components/ClickableComponent.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<q-td
|
||||||
|
:props="props.tdProps"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:class="props.inClass"
|
||||||
|
:style="props.permitted ? 'cursor: pointer' : ''"
|
||||||
|
@click="props.permitted && emit('onClick')"
|
||||||
|
>
|
||||||
|
{{ props.value ?? '' }}
|
||||||
|
</q-td>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['onClick']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
tdProps: { type: Object },
|
||||||
|
inClass: String,
|
||||||
|
style: String,
|
||||||
|
permitted: { type: Boolean, default: false },
|
||||||
|
value: {
|
||||||
|
type: null as unknown as PropType<unknown>,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
49
src/vueLib/tables/components/FilterSelect.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<q-card flat class="q-pa-sm">
|
||||||
|
<q-select
|
||||||
|
:label="$t('filterByColumn')"
|
||||||
|
dense
|
||||||
|
v-model="columnFilter"
|
||||||
|
option-label="label"
|
||||||
|
option-value="name"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
clearable
|
||||||
|
:options="props.filterOptions"
|
||||||
|
v-on:clear="emit('clear')"
|
||||||
|
class="q-mt-xs"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
v-if="columnFilter"
|
||||||
|
:label="$t('filterByColumnValue')"
|
||||||
|
dense
|
||||||
|
v-model="columnOption"
|
||||||
|
:options="props.columnOptions"
|
||||||
|
class="q-mt-xs"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
columnFilter: String,
|
||||||
|
columnOption: Array,
|
||||||
|
filterOptions: Array,
|
||||||
|
columnOptions: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:columnFilter', 'update:columnOption', 'clear']);
|
||||||
|
|
||||||
|
const columnFilter = computed({
|
||||||
|
get: () => props.columnFilter,
|
||||||
|
set: (v) => emit('update:columnFilter', v),
|
||||||
|
});
|
||||||
|
|
||||||
|
const columnOption = computed({
|
||||||
|
get: () => props.columnOption,
|
||||||
|
set: (v) => emit('update:columnOption', v),
|
||||||
|
});
|
||||||
|
</script>
|
||||||