41 Commits

Author SHA1 Message Date
Adrian Zürcher
9e460db854 new release
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 8m12s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 3m53s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 3m37s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 3m35s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 3m39s
2026-02-23 21:23:49 +01:00
Adrian Zürcher
fa58872840 optimize open database calls 2026-02-23 21:23:14 +01:00
Adrian Zürcher
ec5893db57 add new workspace settings for users 2026-02-23 21:21:52 +01:00
Adrian Zürcher
21bbfd617e Merge branch 'main' of https://gitea.tecamino.com/paadi/memberApp 2026-02-19 21:48:47 +01:00
Adrian Zürcher
d154041df0 fix not able to add members to event close #56 2026-02-19 21:48:16 +01:00
41e0b4d060 README.MD aktualisiert 2026-02-19 21:03:56 +01:00
2f67c02be1 README.MD aktualisiert 2026-02-19 21:03:25 +01:00
Adrian Zürcher
15f9026a5f new release
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 8m11s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 7m27s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 7m42s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 6m45s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 6m59s
2026-02-19 18:20:43 +01:00
Adrian Zürcher
e25bac1a1e fix widget size in phone 2026-02-19 18:19:38 +01:00
Adrian Zürcher
548cd9d622 fix pdf size in phone and on desktop close #52 2026-02-19 18:18:58 +01:00
Adrian Zürcher
8e3e8f8bc7 chnage role table so own user can not change his own rules 2026-02-19 10:51:32 +01:00
Adrian Zürcher
e686a27bf1 change username to user 2026-02-19 10:51:09 +01:00
Adrian Zürcher
ab88acd740 add new openDatabse function 2026-02-19 10:50:40 +01:00
Adrian Zürcher
6392877dc1 add new workspaces for users 2026-02-19 10:49:12 +01:00
Adrian Zürcher
b726eb42dc add filter so user can not change to admin if the have the right to change user rights 2026-02-14 13:43:21 +01:00
Adrian Zürcher
8963cba016 fix update role not working 2026-02-14 13:42:54 +01:00
Adrian Zürcher
73901335a3 new release
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 3m5s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 6m38s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 6m47s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 6m46s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 6m41s
2026-02-13 20:40:21 +01:00
Adrian Zürcher
62aed501f3 fix not updating and hiding added memebers close #49 2026-02-13 20:39:16 +01:00
Adrian Zürcher
43d81dd27a commit forgotten file 2026-02-13 20:17:57 +01:00
Adrian Zürcher
ce654bbb6a fix language switching close #51 2026-02-13 20:16:17 +01:00
Adrian Zürcher
9b2b1d3ef7 fix responsible not showing close #50 2026-02-13 15:26:59 +01:00
Adrian Zürcher
f59443ce5a lift version 2026-02-13 13:10:25 +01:00
Adrian Zürcher
c60907257e minor fixes
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 3m19s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 3m59s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 3m45s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 3m41s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 3m38s
2026-02-13 13:08:48 +01:00
Adrian Zürcher
227c57ed41 add label 2026-02-13 13:08:39 +01:00
Adrian Zürcher
c20ce31f04 add save user day settings close #46 2026-02-13 12:28:54 +01:00
Adrian Zürcher
55b6305a5e fix translation switching close #47 close #46 2026-02-13 12:28:26 +01:00
Adrian Zürcher
0026f68320 modify localstorage close #48 2026-02-13 12:27:30 +01:00
Adrian Zürcher
3f02afde85 fix double menu point close #45 2026-02-12 21:12:05 +01:00
Adrian Zürcher
2143ff1683 add print and export pdf function close #22 2026-02-12 17:25:17 +01:00
Adrian Zürcher
1fe2bd24b0 fix only one event per day close #44 2026-02-12 16:25:56 +01:00
Adrian Zürcher
35fcd31a20 amount card hide when empty data 2026-02-12 16:25:44 +01:00
Adrian Zürcher
d2d7c83684 fix selected memebers after adding close #41 2026-02-12 15:54:04 +01:00
Adrian Zürcher
31929ea366 lift version
All checks were successful
Build Quasar SPA and Go Backend for memberApp / build-spa (push) Successful in 3m19s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, .exe, windows) (push) Successful in 4m20s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm, 6, , linux) (push) Successful in 4m12s
Build Quasar SPA and Go Backend for memberApp / build-backend (arm64, , linux) (push) Successful in 4m14s
Build Quasar SPA and Go Backend for memberApp / build-backend (amd64, , linux) (push) Successful in 4m31s
2026-02-12 12:10:25 +01:00
Adrian Zürcher
095d6411ba add label prop 2026-02-12 12:09:55 +01:00
Adrian Zürcher
031b4d84ec change page localstorage names for new report page 2026-02-12 12:09:44 +01:00
Adrian Zürcher
1657123cc1 add new feature report page 2026-02-12 12:08:55 +01:00
Adrian Zürcher
9a55d4c2f0 fix source string undefined in logo image 2026-02-09 07:38:47 +01:00
Adrian Zürcher
bf481da21e hide image if undefined 2026-02-09 07:36:32 +01:00
Adrian Zürcher
b0d225f7b8 fix add member with with group close #38 2026-02-09 07:28:20 +01:00
Adrian Zürcher
5324787f23 fix language duplicate error 2026-02-09 07:16:11 +01:00
Adrian Zürcher
bb626bf6b5 fix cookie error close #40 2026-02-09 07:15:28 +01:00
52 changed files with 1972 additions and 339 deletions

View File

@@ -1,10 +1,10 @@
# Memer App
# Member App
A full-stack Member Management platform designed for organizations to manage their members, events, excursions, and Impact Team activities efficiently.
## 🧩 Overview
**Memer App** provides a unified interface for handling:
**Member App** provides a unified interface for handling:
- Member registration and profiles
- Event creation, attendance tracking, and reporting
- Excursion & Impact Team management

BIN
backend/gagag.dbaa Normal file

Binary file not shown.

View File

@@ -3,9 +3,9 @@ module backend
go 1.25.4
require (
gitea.tecamino.com/paadi/access-handler v1.0.34
gitea.tecamino.com/paadi/memberDB v1.1.19
gitea.tecamino.com/paadi/tecamino-dbm v0.1.1
gitea.tecamino.com/paadi/access-handler v1.0.51
gitea.tecamino.com/paadi/memberDB v1.1.30
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0
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
@@ -14,7 +14,7 @@ require (
)
require (
gitea.tecamino.com/paadi/dbHandler v1.1.10 // indirect
gitea.tecamino.com/paadi/dbHandler v1.1.12 // 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

View File

@@ -1,11 +1,11 @@
gitea.tecamino.com/paadi/access-handler v1.0.34 h1:6P65HiusSfvgv/ezOvxSahqyRJMK9UrxtGsz6loLoUk=
gitea.tecamino.com/paadi/access-handler v1.0.34/go.mod h1:HyMp1WvzmqLw8Ljt3r1qlF8fY+T5WFXr9Da/CTIM0H8=
gitea.tecamino.com/paadi/dbHandler v1.1.10 h1:zZQbDTJ0bu6CIW90Zms8yYIzTLHtWPNhVKRxLUXEDuE=
gitea.tecamino.com/paadi/dbHandler v1.1.10/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.1.19 h1:khbhtqS7rXTuOsWwxTO6rm13mIDjnBmJaTcJY4jmpQw=
gitea.tecamino.com/paadi/memberDB v1.1.19/go.mod h1:VBsORoIIhh0/RM5AvmaAjMEM2/cNaIT2TqDL1VDcov4=
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/access-handler v1.0.51 h1:kTPwN+0Zw/Uyfo6el1jl5ORzIrjQdC8PUlczoN9mBS4=
gitea.tecamino.com/paadi/access-handler v1.0.51/go.mod h1:0kUGU4Jw2jSvopCCwecuX/2QnVKS09Ec1KQNrBXvsFs=
gitea.tecamino.com/paadi/dbHandler v1.1.12 h1:F1ARSTUm0MZmF84FfD/g5RQNMYyDYXHYrB3cXPSi4qw=
gitea.tecamino.com/paadi/dbHandler v1.1.12/go.mod h1:y/xn/POJg1DO++67uKvnO23lJQgh+XFQq7HZCS9Getw=
gitea.tecamino.com/paadi/memberDB v1.1.30 h1:N+3V9A/+OAGIoJeUNVHj1qUuBcy6ADLYFIgCnp2Ggk4=
gitea.tecamino.com/paadi/memberDB v1.1.30/go.mod h1:Q4NO1cdBm/6RLF+bP2NEzBPJURKjyIr4u3dElDXmHWI=
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0 h1:xFgcpIiQMyqbglScZBAbdOQyM+yOJ3GHMK2iX5Ep3Gg=
gitea.tecamino.com/paadi/tecamino-dbm v1.0.0/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=

View File

@@ -145,6 +145,7 @@ func main() {
auth.GET("/users", accessHandler.GetUser)
auth.GET("/roles", accessHandler.GetRole)
auth.GET("/workspaces", accessHandler.GetWorkspace)
auth.POST("database/open", dbHandler.OpenDatabase)
auth.POST("/members/add", dbHandler.AddNewMember)
@@ -158,6 +159,8 @@ func main() {
auth.POST("/events/delete/attendees", dbHandler.DeleteAttendee)
auth.POST("/events/delete", dbHandler.DeleteEvent)
auth.POST("/report", dbHandler.GetReport)
auth.POST("/groups/add", dbHandler.NewGroup)
auth.POST("/groups/edit", dbHandler.UpdateGroup)
auth.POST("/groups/delete", dbHandler.DeleteGroup)
@@ -175,6 +178,11 @@ func main() {
auth.POST("/users/new/password", accessHandler.ChangePassword)
auth.POST("/users/delete", accessHandler.DeleteUser)
auth.POST("/workspaces/add", accessHandler.AddWorkspace)
auth.POST("/workspaces/update", accessHandler.UpdateWorkspace)
auth.POST("/workspaces/data", accessHandler.ReadWorkspaceData)
auth.POST("/workspaces/delete", accessHandler.DeleteWorkspace)
api.POST("/login/refresh", accessHandler.Refresh)
// Serve static files
@@ -226,4 +234,8 @@ func main() {
if err := s.ServeHttp(env.HostUrl.GetValue(), env.HostPort.GetUIntValue()); err != nil {
logger.Error("main", "error http server "+err.Error())
}
if err := s.ServeHttp(env.HostUrl.GetValue(), env.HostPort.GetUIntValue()); err != nil {
logger.Error("main", "error http server "+err.Error())
}
}

235
package-lock.json generated
View File

@@ -1,17 +1,18 @@
{
"name": "lightcontrol",
"version": "1.2.1",
"version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lightcontrol",
"version": "1.2.1",
"version": "1.3.0",
"hasInstallScript": true,
"dependencies": {
"@capacitor-community/sqlite": "^7.0.1",
"@quasar/extras": "^1.17.0",
"axios": "^1.10.0",
"html2pdf.js": "^0.14.0",
"js-yaml": "^4.1.0",
"jwt-decode": "^4.0.0",
"pinia": "^3.0.3",
@@ -93,6 +94,15 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
@@ -1849,6 +1859,12 @@
"@types/node": "*"
}
},
"node_modules/@types/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
"license": "MIT"
},
"node_modules/@types/qs": {
"version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -1856,6 +1872,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
@@ -1886,6 +1909,13 @@
"@types/send": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/zxcvbn": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz",
@@ -2636,6 +2666,15 @@
"license": "Apache-2.0",
"optional": true
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -2964,6 +3003,26 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3320,6 +3379,18 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -3368,6 +3439,15 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -3505,6 +3585,15 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dot-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
@@ -4281,6 +4370,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-png": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
"integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
"license": "MIT",
"dependencies": {
"@types/pako": "^2.0.3",
"iobuffer": "^5.3.2",
"pako": "^2.1.0"
}
},
"node_modules/fast-png/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -4291,6 +4397,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4734,6 +4846,30 @@
"node": "^14.13.1 || >=16.0.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/html2pdf.js": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/html2pdf.js/-/html2pdf.js-0.14.0.tgz",
"integrity": "sha512-yvNJgE/8yru2UeGflkPdjW8YEY+nDH5X7/2WG4uiuSCwYiCp8PZ8EKNiTAa6HxJ1NjC51fZSIEq6xld5CADKBQ==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0",
"jspdf": "^4.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -4865,6 +5001,12 @@
"node": ">=18"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
"integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
"license": "MIT"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -5161,6 +5303,23 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jspdf": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz",
"integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.3.1",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -5988,6 +6147,13 @@
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6235,6 +6401,16 @@
],
"license": "MIT"
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -6349,6 +6525,13 @@
"node": ">=8.10.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
@@ -6417,6 +6600,16 @@
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.40.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
@@ -7365,6 +7558,16 @@
"node": ">=16"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -7495,6 +7698,16 @@
"node": ">=8"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/sync-child-process": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz",
@@ -7583,6 +7796,15 @@
"b4a": "^1.6.4"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -7873,6 +8095,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "lightcontrol",
"version": "1.2.4",
"version": "1.4.1",
"description": "A Tecamino App",
"productName": "Attendence Records",
"author": "A. Zuercher",
@@ -18,6 +18,7 @@
"@capacitor-community/sqlite": "^7.0.1",
"@quasar/extras": "^1.17.0",
"axios": "^1.10.0",
"html2pdf.js": "^0.14.0",
"js-yaml": "^4.1.0",
"jwt-decode": "^4.0.0",
"pinia": "^3.0.3",

View File

@@ -112,7 +112,7 @@ export default defineConfig((/* ctx */) => {
//directives: [],
// Quasar plugins
plugins: ['Notify', 'Dialog'],
plugins: ['Cookies', 'Notify', 'Dialog'],
},
// animations: 'all', // --- includes all animations

View File

@@ -132,8 +132,8 @@ filterByColumn: Spaltenfilter
filterByColumnValue: Spaltenwerte
saveAsDefault: Aus Standard spichere
day: Tag
MondayShort: Mäntig
Monday: Mo
MondayShort: Mo
Monday: Mäntig
Tuesday: Zistig
TuesdayShort: Di
Wednesday: Mittwuch
@@ -156,3 +156,52 @@ numberOfEvents: Anzau Veranstautige
numberOfResponsibles: Anzau Veratwortläche
numberOfGroups: Anzau Gruppe
selectDates: Datumuswauh
apply: Awende
minimal: Minimal
maximal: Maximal
average: Durchschnitt
filterEventName: Verastautig filtere
hintFilterEventName: "*'IIgabe'* * oder % als filler vorher oder nächer"
total: Gesamt
exportPdf: PDF exportiere
print: Drucke
today: Hüt
week: Wuche
month: Monat
year: Jahr
appName: Applikationsname
calendar:
days:
- 'Suntig'
- 'Mäntig'
- 'Zistig'
- 'Mittwuch'
- 'Donstig'
- 'Fritig'
- 'Samstig'
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
months:
- 'Januar'
- 'Februar'
- 'März'
- 'April'
- 'Mai'
- 'Juni'
- 'Juli'
- 'Ougust'
- 'Septämber'
- 'Oktober'
- 'Novämber'
- 'Dezämber'
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
firstDayOfWeek: 1
format24h: true
pluralDay: 'Täg'
description: Beschribig
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Füeg neuis Workspace hinzue
saved: gspicheret
noWorkspaceFound: Kes Workspace gfunge
addNewDatabase: Nei Datenbank
fileNeedsToEndWith: Dateiname mues fougendi endig ha

View File

@@ -65,8 +65,8 @@ 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
addNewRole: Neue Rolle hinzufügen
addNewEvent: Neue Veranstaltung hinzufügen
veryWeak: sehr Schwach
weak: Schwach
fair: Ausreichend
@@ -156,3 +156,52 @@ numberOfEvents: Anzahl Veranstaltungen
numberOfResponsibles: Anzahl Verantwortliche
numberOfGroups: Anzahl Gruppe
selectDates: Datumauswahl
apply: Anwenden
minimal: Minimal
maximal: Maximal
average: Durchschnitt
filterEventName: Veranstaltung filtern
hintFilterEventName: "*'Eingabe'* * oder % als Filler vorher oder nachher"
total: Gesamt
exportPdf: PDF exportieren
print: Drucken
today: Heute
week: Woche
month: Monat
year: Jahr
appName: Applikationsname
calendar:
days:
- 'Sonntag'
- 'Montag'
- 'Dienstag'
- 'Mittwoch'
- 'Donnerstag'
- 'Freitag'
- 'Samstag'
daysShort: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
months:
- 'Januar'
- 'Februar'
- 'März'
- 'April'
- 'Mai'
- 'Juni'
- 'Juli'
- 'August'
- 'September'
- 'Oktober'
- 'November'
- 'Dezember'
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
firstDayOfWeek: 1
format24h: true
pluralDay: 'Tage'
description: Beschreibung
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Neues Workspace hinzufügen
saved: gespeichert
noWorkspaceFound: Kein Workspace gefunden
addNewDatabase: Neue Datenbank hinzufügen
fileNeedsToEndWith: Dateiname muss folgende Endung haben

View File

@@ -156,3 +156,52 @@ numberOfEvents: Amount of Events
numberOfResponsibles: Amount of Responsibles
numberOfGroups: Amount of Groups
selectDates: Dateselection
apply: Apply
minimal: Minimal
maximal: Maximal
average: Average
filterEventName: filter Events
hintFilterEventName: "*'Input'* * or % as filler before oder after"
total: Total
exportPdf: export PDF
print: Print
today: Today
week: Week
month: Month
year: Year
appName: Applicationname
calendar:
days:
- 'Sunday'
- 'Monday'
- 'Tuesday'
- 'Wednesday'
- 'Thursday'
- 'Friday'
- 'Saturday'
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thur', 'Fri', 'Sat']
months:
- 'January'
- 'February'
- 'March'
- 'April'
- 'May'
- 'June'
- 'July'
- 'August'
- 'September'
- 'October'
- 'November'
- 'December'
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
firstDayOfWeek: 0
format24h: false
pluralDay: 'Days'
description: Description
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Add new Workspace
saved: saved
noWorkspaceFound: No Workspace found
addNewDatabase: Add new database
fileNeedsToEndWith: Filename must end with

View File

@@ -37,7 +37,7 @@ login: Iniciar sesión
logout: Cerrar sesión
user: Usuario
password: Contraseña
isRequired: Es obligatorio
isRequired: Obligatorio
colors: Colores
primaryColor: Color principal
primaryColorText: Color del texto principal
@@ -108,7 +108,7 @@ attendeeAdded: Asistente añadido
attendeesAdded: Asistentes añadidos
eventAdded: Evento añadido
userUpdated: Usuario actualizado
selectResponsibleOptions: Seleccionar opciones responsables
selectResponsibleOptions: Seleccionar opciones de responsables
addNewResponsible: Añadir responsable
responsibleAdded: Responsable añadido
responsiblesAdded: Responsables añadidos
@@ -145,7 +145,7 @@ FridayShort: Vi
Saturday: Sábado
SaturdayShort:
Sunday: Domingo
Sunday: Do
SundayShort: Do
currentPassword: Contraseña actual
addFirstUser: Añadir primer usuario administrador
report: Informe
@@ -156,3 +156,52 @@ numberOfEvents: Cantidad de eventos
numberOfResponsibles: Número de responsables
numberOfGroups: Número de grupos
selectDates: Selección de fecha
apply: Aplicar
minimal: Mínimo
maximal: Máximo
average: Promedio
filterEventName: filtrar eventos
hintFilterEventName: "*''Entrada'* * o % como relleno antes o después"
total: Total
exportPdf: exportar PDF
print: Imprimir
today: Hoy
week: Semana
month: Mes
year: Año
appName: Nombre de la aplicación
calendar:
days:
- 'Domingo'
- 'Lunes'
- 'Martes'
- 'Miércoles'
- 'Jueves'
- 'Viernes'
- 'Sábado'
daysShort: ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb']
months:
- 'Enero'
- 'Febrero'
- 'Marzo'
- 'Abril'
- 'Mayo'
- 'Junio'
- 'Julio'
- 'Agosto'
- 'Septiembre'
- 'Octubre'
- 'Noviembre'
- 'Diciembre'
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic']
firstDayOfWeek: 1
format24h: true
pluralDay: 'dias'
description: Descripción
workspace: Workspace
workspaces: Workspaces
addNewWorkspace: Añadir nuevo Workspace
saved: guardado
noWorkspaceFound: No se encontró Workspace
addNewDatabase: Agregar nueva base de datos
fileNeedsToEndWith: El nombre del archivo debe terminar con

View File

@@ -1,29 +1,35 @@
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';
import { Me, openDatabase } from 'src/vueLib/components/DatabaseCall';
const pinia = createPinia();
export default boot(async ({ app }) => {
app.use(pinia);
const useStore = useUserStore();
const userStore = 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));
const resp = await Me().catch(() =>
login.logout().catch((err) => {
console.error(err);
return;
}),
);
if (!resp) return;
await userStore
.setUser({
id: resp.data.id,
user: resp.data.username,
role: { role: resp.data.role, permissions: [] },
workspaceId: resp.data.workspaceId,
settings: resp.data.settings,
})
.catch(() => {
login.logout().catch((err) => console.error(err));
});
.catch((err) => console.error(err));
login.refresh().catch((err) => console.error(err));
await openDatabase().catch((err) => console.error(err));
});

View File

@@ -1,7 +1,7 @@
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 { logo, appName } from 'src/vueLib/models/settings';
import { Dark } from 'quasar';
import { getLocalDarkMode, getLocalSettings } from 'src/localstorage/localStorage';
@@ -20,7 +20,6 @@ export default boot(({ app, router }) => {
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(

View File

@@ -1,15 +1,14 @@
import { boot } from 'quasar/wrappers';
import { useUserStore } from 'src/vueLib/login/userStore';
import { appApi } from './axios';
import { getLocalLastRoute, setLocalLastRoute } from 'src/localstorage/localStorage';
import { Me } from 'src/vueLib/components/DatabaseCall';
export default boot(async ({ router }) => {
const userStore = useUserStore();
// load user
try {
const { data } = await appApi.get('/login/me');
const data = await Me();
userStore.setFirstLogin(data.newDatabase);
data.role.role = data.role;
await userStore.setUser(data);
} catch {

View File

@@ -74,7 +74,9 @@ function open(title: string, members: Members) {
appApi
.get('events')
.then((resp) => {
events.value.push(...resp.data.map((e: Event) => (e.name = e.name + ' (' + e.date + ')')));
events.value.push(
...resp.data.map((e: Event) => ({ ...e, name: e.name + ' (' + e.date + ')' })),
);
})
.catch((err) => {
NotifyResponse(err, 'error');
@@ -109,7 +111,7 @@ async function addAttendees() {
});
await updateAttendees(0);
updateEvents();
await updateEvents();
}
let resolveNewEvent!: (value: Event) => void;

View File

@@ -1,5 +1,8 @@
<template>
<div class="q-gutter-sm">
<div
class="q-ma-sm q-gutter-sm"
:style="{ height: props.height + 'px', width: props.width + 'px' }"
>
<h6 class="text-center text-bold q-ma-md text-primary">{{ props.title }}</h6>
<div class="row">
<q-checkbox
@@ -11,33 +14,54 @@
:val="opt.value"
:label="opt.label"
/>
<q-tabs
v-model="activeTab"
dense
class="text-primary"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
@update:model-value="onTabChange"
>
<q-tab no-caps name="today" :label="$t('today')" />
<q-tab no-caps name="week" :label="$t('week')" />
<q-tab no-caps name="month" :label="$t('month')" />
<q-tab no-caps name="year" :label="$t('year')" />
</q-tabs>
</div>
<div class="row">
<q-date v-model="dateRange" range flat />
<div>
<q-badge color="secondary"> Total dates selected: {{ filteredDates.length }} </q-badge>
</div>
<q-date :locale="calendarLanguage" v-model="dateRange" range flat />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, onMounted, watch, type PropType, computed } from 'vue';
import { date } from 'quasar';
import { i18n } from 'src/boot/lang';
import type { QDateLocale } from 'src/vueLib/models/qDateLocale';
const props = defineProps({
title: String,
height: { type: Number, default: 400 },
width: { type: Number, default: 300 },
});
const weekdays = defineModel('weekdays', {
type: Array as PropType<number[]>,
default: () => [0, 3],
});
const startDate = new Date();
// Initial range (format: YYYY/MM/DD)
const dateRange = ref();
const selectedWeekdays = ref([0, 3]); // Default to weekdays
const activeTab = ref('');
// Initial range (format: YYYY-MM-DD)
const dateRange = ref<string | { to: string; from: string }>('');
const selectedWeekdays = ref(weekdays); // Default to weekdays
const emit = defineEmits(['update:dates']);
onMounted(() => {
dateRange.value = date.formatDate(startDate, 'YYYY/MM/DD');
dateRange.value = date.formatDate(startDate, 'YYYY-MM-DD');
});
const weekdayOptions = [
@@ -50,14 +74,37 @@ const weekdayOptions = [
{ label: i18n.global.t('SundayShort'), value: 0 },
];
const calendarLanguage = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const localeData = i18n.global.tm('calendar') as unknown as QDateLocale;
return {
days: localeData.days,
daysShort: localeData.daysShort,
months: localeData.months,
monthsShort: localeData.monthsShort,
firstDayOfWeek: localeData.firstDayOfWeek,
format24h: localeData.format24h,
pluralDay: localeData.pluralDay,
};
});
const onTabChange = (val: 'today' | 'week' | 'month' | 'year') => {
if (val) setRange(val);
// Optional: Reset tab to empty so user can click the same tab again later
setTimeout(() => {
activeTab.value = '';
}, 500);
};
// The Logic: Calculate all specific dates within the range that match weekdays
const filteredDates = computed(() => {
watch(dateRange, () => {
if (!dateRange.value) {
return [];
} else if (typeof dateRange.value === 'string') {
const current = new Date(dateRange.value);
if (current !== undefined && selectedWeekdays.value.includes(current.getDay())) {
return [date.formatDate(current, 'YYYY/MM/DD')];
emit('update:dates', [date.formatDate(current, 'YYYY-MM-DD')]);
return [date.formatDate(current, 'YYYY-MM-DD')];
}
return [];
}
@@ -69,10 +116,89 @@ const filteredDates = computed(() => {
while (current <= end) {
if (selectedWeekdays.value.includes(current.getDay())) {
result.push(date.formatDate(current, 'YYYY/MM/DD'));
result.push(date.formatDate(current, 'YYYY-MM-DD'));
}
current = date.addToDate(current, { days: 1 });
}
emit('update:dates', result);
return result;
});
/**
* Logic for 1 Week / 1 Month / 1 Year
*/
const setRange = (type: 'today' | 'week' | 'month' | 'year') => {
const anchor =
typeof dateRange.value === 'string'
? new Date(dateRange.value)
: new Date(dateRange.value.from);
let from: Date;
let to: Date;
if (type === 'today') {
const now = new Date();
dateRange.value = date.formatDate(date.startOfDate(now, 'day'), 'YYYY-MM-DD');
return;
} else if (type === 'week') {
// getDay() returns 0 for Sunday.
// We calculate how many days to subtract to get to Monday (1).
const day = anchor.getDay();
const diffToMonday = day === 0 ? -6 : 1 - day;
from = date.addToDate(anchor, { days: diffToMonday });
from = date.startOfDate(from, 'day'); // Reset time to 00:00
to = date.addToDate(from, { days: 6 });
to = date.endOfDate(to, 'day'); // Set time to 23:59
} else if (type === 'month') {
// 'month' is a valid unit for startOfDate
from = date.startOfDate(anchor, 'month');
to = date.endOfDate(anchor, 'month');
} else {
// 'year' is a valid unit for startOfDate
from = date.startOfDate(anchor, 'year');
to = date.endOfDate(anchor, 'year');
}
dateRange.value = {
from: date.formatDate(from, 'YYYY/MM/DD'),
to: date.formatDate(to, 'YYYY/MM/DD'),
};
};
watch(
[dateRange, selectedWeekdays],
() => {
if (!dateRange.value) {
emit('update:dates', []);
return;
}
let start: Date, end: Date;
if (typeof dateRange.value === 'string') {
start = new Date(dateRange.value);
end = new Date(dateRange.value);
} else {
start = new Date(dateRange.value.from);
end = new Date(dateRange.value.to);
}
const result = [];
let current = start;
while (current <= end) {
if (selectedWeekdays.value.includes(current.getDay())) {
result.push(date.formatDate(current, 'YYYY-MM-DD'));
}
current = date.addToDate(current, { days: 1 });
}
emit('update:dates', result);
},
{ deep: true },
);
</script>

View File

@@ -69,7 +69,6 @@
filled
emit-value
map-options
option-value="name"
option-label="name"
v-model="localMember.group"
></q-select>
@@ -78,7 +77,7 @@
:label="$t('responsible')"
filled
:options="props.responsibles"
:option-label="(opt) => opt.firstName + ' ' + opt.lastName"
:option-label="(opt) => opt.member.firstName + ' ' + opt.member.lastName"
v-model="localMember.responsible"
></q-select>
<q-input
@@ -107,7 +106,7 @@
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 type { Member, Members } 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';
@@ -127,7 +126,7 @@ const props = defineProps({
type: Object as PropType<Responsibles>,
},
group: {
type: Array,
type: Object as PropType<Members>,
},
});

View File

@@ -0,0 +1,53 @@
<template>
<q-card class="col-auto">
<div class="row q-col-gutter-xs">
<div v-for="opt in props.amounts" :key="opt.name">
<q-card class="q-ma-xs" flat bordered>
<q-item dense>
<q-item-section>
<q-item-label class="text-bold text-primary text-center">
{{ $t(opt.name) }}
</q-item-label>
</q-item-section>
</q-item>
<q-separator />
<q-card-section>
<div class="column justify-between items-center q-mb-xs">
<span class="text-bold text-grey-7 text-caption">{{ $t('events') }}</span>
<q-badge color="black" outline class="text-bold">{{ opt.events }}</q-badge>
</div>
<div class="column justify-between items-center q-mb-xs">
<span class="text-bold text-grey-7 text-caption">{{ $t('minimal') }}</span>
<q-badge color="secondary" outline class="text-bold">{{ opt.minimal }}</q-badge>
</div>
<div class="column justify-between items-center q-mb-xs">
<span class="text-bold text-grey-7 text-caption">{{ $t('average') }}</span>
<q-badge color="secondary" outline class="text-bold">{{ opt.average }}</q-badge>
</div>
<div class="column justify-between items-center q-mb-xs">
<span class="text-bold text-grey-7 text-caption">{{ $t('maximal') }}</span>
<q-badge color="primary" outline class="text-bold">{{ opt.maximal }}</q-badge>
</div>
</q-card-section>
</q-card>
</div>
</div>
</q-card>
</template>
<script setup lang="ts">
import type { Amount } from 'src/vueLib/models/report';
import type { PropType } from 'vue';
const props = defineProps({
amounts: {
type: Array as PropType<Amount[]>,
required: true,
},
});
</script>

View File

@@ -30,6 +30,9 @@
class="col-5 required"
:label="$t('role')"
filled
option-label="role"
option-value="role"
emit-value
:options="props.roles"
:rules="[(val) => !!val || $t('roleIsRequired')]"
v-model="role"
@@ -66,6 +69,7 @@ const dialog = ref();
const form = ref();
const newUser = ref(false);
const role = ref('');
const localUser = ref<User>({
user: '',
email: '',

View File

@@ -0,0 +1,105 @@
<template>
<DialogFrame
ref="dialog"
:header-title="newWorkspace ? $t('addNewWorkspace') : $t('edit') + ' ' + localWorkspace.name"
:height="300"
:width="600"
>
<div class="column">
<div class="row justify-center">
<q-input
class="col-5 required"
:label="$t('workspace')"
filled
:rules="[(val) => !!val || $t('workspaceIsRequired')]"
v-model="localWorkspace.name"
autofocus
></q-input>
</div>
<div class="row justify-center">
<q-input
dense
class="col-5 required"
:label="$t('description')"
filled
v-model="localWorkspace.description"
></q-input>
</div>
</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 { Workspace } from 'src/vueLib/models/workspaces';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'src/boot/lang';
const { NotifyResponse } = useNotify();
const dialog = ref();
const newWorkspace = ref(false);
const localWorkspace = ref<Workspace>({
name: '',
description: '',
});
const emit = defineEmits(['update']);
function open(workspace: Workspace | null) {
if (workspace === undefined) {
return;
}
if (workspace !== null) {
localWorkspace.value = { ...workspace };
localWorkspace.value.description = workspace.description;
newWorkspace.value = false;
} else {
localWorkspace.value = {
name: '',
description: '',
};
newWorkspace.value = true;
}
dialog.value?.open();
}
async function save() {
let query = 'workspaces/update?id=' + localWorkspace.value.id;
let update = true;
if (newWorkspace.value) {
query = 'workspaces/add';
update = false;
}
await appApi
.post(query, JSON.stringify(localWorkspace.value))
.then(() => {
if (update) {
NotifyResponse(
i18n.global.t('workspace') +
" '" +
localWorkspace.value.name +
"' " +
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>

View File

@@ -3,11 +3,13 @@
<q-header elevated>
<q-toolbar>
<q-img
v-if="localLogo !== undefined && localLogo !== ''"
: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>
@@ -63,10 +65,7 @@
>
<q-item-section>{{ $t('groups') }}</q-item-section>
</q-item>
<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
<q-item
v-if="autorized || user.isPermittedTo('members', 'read')"
to="/report"
exact
@@ -75,7 +74,7 @@
@click="closeDrawer"
>
<q-item-section> {{ $t('report') }}</q-item-section>
</q-item> -->
</q-item>
<q-item v-if="autorized" to="/stats" exact clickable v-ripple @click="closeDrawer">
<q-item-section> {{ $t('stats') }}</q-item-section>
</q-item>

View File

@@ -1,12 +1,10 @@
import { Dark } from 'quasar';
import { appName, databaseName, type Settings } from 'src/vueLib/models/settings';
import { updateOrAddObject } from 'src/vueLib/utils/utils';
import { appName, type Settings } from 'src/vueLib/models/settings';
import { ref } from 'vue';
export function setLocalSettings(settings: Settings) {
localStorage.setItem('icon', settings.icon);
localStorage.setItem('appName', settings.appName);
localStorage.setItem('databaseName', settings.databaseName);
if (settings.icon !== '') localStorage.setItem('icon', settings.icon);
if (settings.appName !== '') localStorage.setItem('appName', settings.appName);
localStorage.setItem('primaryColor', settings.primaryColor);
localStorage.setItem('primaryColorText', settings.primaryColorText);
localStorage.setItem('secondaryColor', settings.secondaryColor);
@@ -18,16 +16,14 @@ export function getLocalSettings(): Settings {
if (name === undefined || name === 'undefined') {
name = appName.value;
}
let db = localStorage.getItem('databaseName');
if (db === undefined || db === 'undefined') {
db = databaseName.value;
let iconName = localStorage.getItem('icon');
if (iconName === undefined || iconName === 'undefined') {
iconName = '';
}
return <Settings>{
icon: localStorage.getItem('icon'),
icon: iconName,
appName: name,
databaseName: db,
primaryColor: localStorage.getItem('primaryColor'),
primaryColorText: localStorage.getItem('primaryColorText'),
secondaryColor: localStorage.getItem('secondaryColor'),
@@ -38,7 +34,6 @@ export function getLocalSettings(): Settings {
export function clearLocalStorage() {
localStorage.removeItem('icon');
localStorage.removeItem('appName');
localStorage.removeItem('databaseName');
localStorage.removeItem('primaryColor');
localStorage.removeItem('primaryColorText');
localStorage.removeItem('secondaryColor');
@@ -74,30 +69,19 @@ export function getLocalLanguage(): string | null {
type pageDefault = {
page: string;
filteredColumn: string;
filteredValue: string[];
data: unknown;
};
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 setLocalPageDefaults(page: string, data: unknown) {
localStorage.setItem(page + 'Defaults', JSON.stringify(data));
}
export function getLocalPageDefaults(page: string): pageDefault | null {
const defaults = localStorage.getItem('pageDefaults');
export function getLocalPageDefaults(page: string): unknown {
const defaults = localStorage.getItem(page + 'Defaults');
if (!defaults) return null;
pageDefaults.value = JSON.parse(defaults);
return pageDefaults.value.find((e) => e.page === page) || null;
return JSON.parse(defaults);
}

View File

@@ -15,7 +15,7 @@ const router = useRouter();
const userStore = useUserStore();
onMounted(() => {
if (userStore.user?.username !== '' && userStore.user?.role.role !== '') {
if (userStore.user?.user !== '' && userStore.user?.role.role !== '') {
forwardToPage().catch((err) => console.error(err));
}
});

View File

@@ -1,36 +1,373 @@
<template>
<div class="rows">
<q-inner-loading
:showing="loading"
label="Please wait..."
label-class="text-teal"
label-style="font-size: 1.1em"
/>
<div class="colums">
<div class="row justify-end">
<div class="column">
<DateDaySelect title="hjgjh" />
<q-btn-dropdown
no-caps
ref="dropdownRef"
class="q-ma-sm"
color="primary"
:label="$t('selectDates')"
@show="loadSettings"
>
<DateDaySelect @update:dates="updateReport" v-model:weekdays="weekdays" />
<div class="column justify-end q-pa-md">
<div class="row q-ma-md">
<q-input
class="col-7"
label-color="primary"
:label="$t('filterEventName')"
:hint="$t('hintFilterEventName')"
type="text"
v-model:model-value="filter"
></q-input>
</div>
<div class="row q-ma-md">
<q-select
class="col-7"
:label="$t('filterByColumnValue')"
dense
v-model="group"
:options="groups"
option-label="name"
option-value="id"
multiple
clearable
/>
</div>
<div class="row justify-end">
<q-btn dense class="q-ma-md" color="primary" no-caps @click="applyDateChoice">{{
$t('apply')
}}</q-btn>
</div>
</div>
</q-btn-dropdown>
<div class="column q-ma-sm" v-if="amounts.length">
<q-btn
dense
no-caps
class="q-ma-sm"
color="grey-9"
icon="print"
:label="$t('print')"
@click="printReport"
/>
<q-btn
dense
no-caps
color="secondary"
icon="picture_as_pdf"
:label="$t('exportPdf')"
@click="downloadPDF"
/>
</div>
</div>
</div>
</div>
<div id="report-content" ref="reportExportRef">
<div class="row justify-center q-ma-xs">
<h3 class="col-12 text-center text-primary text-bold">{{ $t('report') }}</h3>
<ReportStat :amounts="amounts" />
</div>
<div class="row justify-center">
<div
v-if="attendees !== undefined"
:class="
nonAttendees !== undefined
? printing
? 'col-5 q-pa-md'
: 'col-12 col-sm-5 col-md-5 q-pa-md'
: printing
? 'col-5'
: 'col-12 col-md-8 col-lg-5'
"
>
<q-table
flat
dense
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:rows-per-page-options="[0]"
:title="$t('attendees')"
title-class="text-bold text-primary"
:rows="attendees"
:columns="columns"
>
</q-table>
</div>
<div
v-if="nonAttendees !== undefined"
:class="
attendees !== undefined
? printing
? 'col-5 q-pa-md'
: 'col-12 col-sm-5 col-md-5 q-pa-md'
: printing
? 'col-5'
: 'col-12 col-md-8 col-lg-5'
"
>
<q-table
flat
dense
:title="$t('noneAttendees')"
:no-data-label="$t('noDataAvailable')"
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:rows-per-page-options="[0]"
title-class="text-bold text-primary"
:rows="nonAttendees"
:columns="columns"
>
</q-table>
</div>
</div>
</div>
<q-btn @click="openDateSelect">Hallo</q-btn>
<DialogFrame :header-title="$t('selectDates')" :width="350" :height="500" ref="dateSelect">
<DateDaySelect :title="$t('selectDates')" />
<q-btn color="primary" no-caps>{{ $t('apply') }}</q-btn>
</DialogFrame>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import DateDaySelect from 'src/components/DateDaySelect.vue';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import { i18n } from 'src/boot/lang';
import { appName } from 'src/vueLib/models/settings';
import type { Amount } from 'src/vueLib/models/report';
import ReportStat from 'src/components/ReportStat.vue';
import type { Group, Groups } from 'src/vueLib/models/group';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import html2pdf from 'html2pdf.js';
import type { PageDefault } from 'src/vueLib/models/pageDefaults';
const dateSelect = ref();
const filter = ref<string>('');
const group = ref<Group[]>([]);
const groups = ref<Groups>([]);
const allDates = ref<string[]>([]);
const attendees = ref();
const nonAttendees = ref();
const { NotifyResponse } = useNotify();
const dropdownRef = ref();
const loading = ref(false);
const amounts = ref<Amount[]>([]);
const reportExportRef = ref<HTMLElement | null>(null);
const weekdays = ref<number[]>([0, 3]);
const printing = ref<boolean>(false);
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,
},
]);
onMounted(() => {
loading.value = true;
appApi
.get('events')
.then((resp) => console.log(1, resp))
.catch((err) => NotifyResponse(err, 'error'));
.get('/groups')
.then((resp) => (groups.value = resp.data))
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
});
function openDateSelect() {
dateSelect.value?.open();
function loadSettings() {
const settings = getLocalPageDefaults('report') as PageDefault;
if (!settings) return;
if (settings.groups) {
group.value = settings.groups;
}
if (settings.weekdays) {
weekdays.value = settings.weekdays;
}
}
function applyDateChoice() {
amounts.value = [];
loading.value = true;
dropdownRef.value.hide();
const payload: { name: null | string[]; date: string[]; groupIds: number[] | null } = {
name: null,
date: allDates.value,
groupIds: null,
};
if (filter.value) {
//name is a array of string and search works as example Test*, Ge*, *st
payload.name = [filter.value];
}
if (group.value) {
// group has to be array of id numbers
payload.groupIds = group.value.map((g) => g.id);
}
payload.date = allDates.value;
setLocalPageDefaults('report', <PageDefault>{ groups: group.value, weekdays: weekdays.value });
appApi
.post('report', payload)
.then((resp) => {
attendees.value = [];
nonAttendees.value = [];
if (!resp.data) return;
if (resp.data.data === undefined) return;
const data = resp.data.data;
if (data.data === undefined) return;
if (data.attendees) {
attendees.value = data.attendees;
}
if (data.attendees) {
nonAttendees.value = data.nonAttendees;
}
const days = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
'total',
];
amounts.value = days
.filter((day) => data.data[day]) // Only include days that exist in the response
.map((day) => ({
...data.data[day],
name: day, // Dynamically translate the name
}));
if (amounts.value.length == 2) {
amounts.value.splice(1);
}
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => (loading.value = false));
}
function updateReport(dates: string[]) {
allDates.value = dates;
}
function printReport() {
printing.value = true;
window.print();
printing.value = false;
}
async function downloadPDF() {
printing.value = true;
const element = reportExportRef.value;
if (!element) return;
// Generate date string (YYYY-MM-DD)
const today = new Date().toISOString().split('T')[0];
// Optionally, add time for more precision (HH-mm)
const time = new Date()
.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
.replace(':', '-');
const options = {
margin: [10, 10, 10, 10] as [number, number, number, number],
filename: appName.value + `${today}_${time}.pdf`,
image: {
type: 'jpeg' as const,
quality: 1.0, // Set quality to 100%
},
html2canvas: {
scale: 4, // Increase this (2 is standard, 4 is crisp/retina)
useCORS: true,
letterRendering: true, // Improves text spacing
dpi: 300, // Standard print resolution
},
jsPDF: {
unit: 'mm' as const,
format: 'a4' as const,
orientation: 'portrait' as const,
compress: true, // Keeps file size manageable despite high scale
},
};
await html2pdf()
.set(options)
.from(element)
.save()
.catch((error) => {
console.error('PDF Generation failed:', error);
})
.finally(() => (printing.value = false));
}
</script>
<style>
@media print {
/* 1. Hide the URL, Date, and Page Title */
@page {
margin: 0; /* This is what removes the URL and headers/footers */
}
body {
padding: 1.5cm; /* Add padding here so the content isn't at the very edge */
}
/* 2. Hide UI elements */
.q-btn,
.q-btn-dropdown,
.q-header,
.q-drawer,
.q-footer,
.q-notifications {
display: none !important;
}
/* 3. Ensure the layout uses full width */
.q-page-container {
padding: 0 !important;
}
/* Remove shadows for cleaner printing */
.q-card,
.q-table__card {
box-shadow: none !important;
border: 1px solid #ddd !important;
}
}
/* This ensures the PDF version has a white background and visible text */
#report-content {
background: white;
color: black;
padding: 20px;
}
/* Force tables to expand to full width in the PDF */
#report-content .q-table__container {
width: 100% !important;
}
/* If you want to force a page break before the tables */
.pdf-page-break {
page-break-before: always;
}
</style>

View File

@@ -7,25 +7,17 @@
<p class="text-bold text-h6 text-primary q-pa-md">{{ $t('general') }}</p>
<div class="row">
<q-input
dense
: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',
]"
:class="[colorGroup ? ' col-md-4' : 'col-md-12', 'q-pa-md']"
filled
:label="$t('appName')"
v-model="settings.appName"
></q-input>
<q-input
dense
: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',
]"
:class="[colorGroup ? 'col-md-4' : 'col-md-12', 'q-pa-md']"
filled
:label="$t('icon')"
v-model="settings.icon"
@@ -34,19 +26,57 @@
</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
<div
v-if="localUser?.workspaces !== undefined && localUser?.workspaces.length > 0"
class="row"
>
<q-select
dense
: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',
]"
:class="[colorGroup ? 'col-md-4' : 'col-xs-12 col-sm-6 col-md-12', 'q-pa-md']"
filled
:label="$t('workspaces')"
:options="localUser?.workspaces"
option-label="name"
v-model="localWorkspace"
@update:model-value="changeWorkspace"
></q-select>
<q-select
dense
:readonly="!user.isPermittedTo('settings', 'write')"
:class="[colorGroup ? 'col-md-4' : 'col-xs-12 col-sm-6 col-md-12', 'q-pa-md']"
filled
:label="$t('databaseName')"
:options="databases"
v-model="settings.databaseName"
></q-input>
@update:model-value="changeDatabase"
>
<template v-slot:option="scope">
<q-item v-bind="scope.itemProps">
<q-item-section>
<q-item-label>{{ scope.opt }}</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn
v-if="scope.index > foundDatabases"
flat
round
size="sm"
color="negative"
icon="cancel"
@click.stop="removeItem(scope.index)"
/>
</q-item-section>
</q-item>
</template>
</q-select>
</div>
<div v-else class="column items-center q-pa-lg text-center">
<q-icon name="workspaces" size="64px" color="grey-5" />
<div class="text-h6 q-mt-md text-grey-8">
{{ $t('noWorkspaceFound') }}
</div>
</div>
</q-card>
<q-card class="q-ma-lg">
@@ -141,36 +171,88 @@
</div>
</q-card>
</div>
<DialogFrame :width="300" :header-title="$t('addNewDatabase')" ref="addDatabaseRef">
<q-input
class="q-ma-md"
autofocus
filled
:label="$t('databaseName')"
v-model:model-value="newDatabase"
@keyup.enter.stop.prevent="addNewDatabase"
/>
<div class="row justify-center">
<q-btn class="q-a-md" color="primary" :label="$t('save')" @click="addNewDatabase"></q-btn>
</div>
</DialogFrame>
</template>
<script setup lang="ts">
import { logo, appName, databaseName } from 'src/vueLib/models/settings';
import { reactive, ref, watch } from 'vue';
import { logo, appName } from 'src/vueLib/models/settings';
import { onMounted, 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 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';
import { type User } from 'src/vueLib/models/user';
import { i18n } from 'src/boot/lang';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import type { Workspace } from 'src/vueLib/models/workspaces';
import { openDatabase } from '../vueLib/components/DatabaseCall';
const { NotifyResponse } = useNotify();
const colorGroup = ref(false);
const user = useUserStore();
const addDatabaseRef = ref();
const newDatabase = ref<string>('');
const foundDatabases = ref<number>(0);
const localUser = ref<User>();
const databases = ref<string[]>([]);
const localWorkspace = ref<Workspace>();
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'),
});
onMounted(async () => {
await appApi
.get('users?id=' + user.user?.id)
.then((resp) => {
if (!resp.data) return;
localUser.value = resp.data[0];
localWorkspace.value = localUser.value?.workspaces?.find(
(w) => w.id === localUser.value?.workspaceId,
);
if (localUser.value) {
settings.databaseName = localUser.value.settings?.databaseName || '';
appApi
.post('workspaces/data', localWorkspace.value)
.then((resp) => {
if (!resp.data) {
settings.databaseName = '';
return;
}
databases.value = [i18n.global.t('addNewDatabase')];
databases.value.push(...resp.data.data);
foundDatabases.value = resp.data.data.length;
})
.catch((err) => NotifyResponse(err, 'error'));
}
})
.catch((err) => NotifyResponse(err, 'error'));
});
watch(settings, (newSettings) => {
logo.value = newSettings.icon;
appName.value = newSettings.appName;
databaseName.value = newSettings.databaseName;
});
function resetColors() {
@@ -184,23 +266,74 @@ function resetColors() {
settings.secondaryColorText = '#ffffff';
}
function save() {
function changeWorkspace() {
appApi
.post('workspaces/data', localWorkspace.value)
.then((resp) => {
if (resp.data) {
databases.value = [i18n.global.t('addNewDatabase')];
databases.value.push(...resp.data.data);
foundDatabases.value = resp.data.data.length;
}
})
.catch((err) => NotifyResponse(err, 'error'));
}
function changeDatabase() {
if (settings.databaseName) {
if (databases.value.indexOf(settings.databaseName) === 0) {
addDatabaseRef.value?.open();
}
}
}
function removeItem(index: number) {
if (index > 0) {
databases.value.splice(index, 1);
}
}
function addNewDatabase() {
if (!newDatabase.value.includes('.db')) {
NotifyResponse(i18n.global.t('fileNeedsToEndWith') + ' .db', 'error');
return;
}
databases.value.push(newDatabase.value);
settings.databaseName = newDatabase.value;
addDatabaseRef.value.close();
}
async 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;
if (localUser.value?.settings) {
localUser.value.settings = settings;
}
appApi
.post('users/update', tempuser)
.then((resp) => NotifyResponse(resp.data.message))
setLocalSettings(settings);
if (localUser.value) {
await user.setUser(localUser.value);
localUser.value.workspaceId = localWorkspace.value?.id;
}
await appApi
.post('users/update', localUser.value)
.then(() =>
NotifyResponse(
i18n.global.t('user') +
' ' +
localUser.value?.user +
' ' +
i18n.global.t('settings') +
' ' +
i18n.global.t('saved'),
),
)
.catch((err) => NotifyResponse(err, 'error'));
await openDatabase().catch((err) => console.error(err));
}
</script>

View File

@@ -52,7 +52,7 @@ import { appApi } from 'src/boot/axios';
import { i18n } from 'src/boot/lang';
import SiteTitle from 'src/vueLib/general/SiteTitle.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { onMounted, ref } from 'vue';
const stats = ref();
@@ -71,8 +71,13 @@ const amounts = ref<{
const { NotifyResponse } = useNotify();
onMounted(async () => {
const user = useUserStore().user;
const workspaceUuid = useUserStore().getWorkspaceUuid;
let path = user?.settings?.databaseName;
if (workspaceUuid !== undefined) path = workspaceUuid + '/' + path;
stats.value = await appApi
.post('/stats', { database: databaseName.value })
.post('/stats', { database: path })
.then((resp) => {
if ((resp.data.databaseSize as number) >= 1000000000) {
return (resp.data.data.databaseSize / 1000000000).toFixed(2) + ' GB';

View File

@@ -11,8 +11,15 @@
align="justify"
narrow-indicator
>
<q-tab name="users" no-caps :label="$t('users')" />
<q-tab name="roles" no-caps :label="$t('roles')" />
<q-tab name="users" icon="people" no-caps :label="$t('users')" />
<q-tab name="roles" icon="rule" no-caps :label="$t('roles')" />
<q-tab
v-if="user?.user?.role.role.includes('admin')"
name="workspaces"
icon="workspaces"
no-caps
:label="$t('workspaces')"
/>
</q-tabs>
<q-separator />
@@ -23,16 +30,24 @@
<q-tab-panel name="roles" style="padding: 0px">
<RoleTable />
</q-tab-panel>
<q-tab-panel name="workspaces" style="padding: 0px">
<WorkspaceTable />
</q-tab-panel>
</q-tab-panels>
</q-card>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onMounted, 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';
import { useUserStore } from 'src/vueLib/login/userStore';
import type { UserState } from 'src/vueLib/models/user';
import WorkspaceTable from 'src/vueLib/tables/workspaces/WorkspaceTable.vue';
const tab = ref('users');
const user = ref<UserState>();
onMounted(() => (user.value = useUserStore()));
</script>

View File

@@ -0,0 +1,43 @@
import { appApi } from 'src/boot/axios';
import { useUserStore } from '../login/userStore';
import type { User } from '../models/users';
import { useNotify } from '../general/useNotify';
export async function openDatabase() {
const user = useUserStore().user;
if (!user) return;
const tempUser = (await getUser(user?.id)) as User | void;
if (!tempUser) return;
let path = '';
path = tempUser.settings?.databaseName || '';
if (tempUser.workspaces) {
path = tempUser.workspaces.find((w) => w.id === tempUser.workspaceId)?.uuid || '';
path = path + '/' + tempUser.settings?.databaseName;
}
return appApi.post('database/open', {
dbPath: path,
create: true,
});
}
async function getUser(id: number): Promise<User | null> {
const { NotifyResponse } = useNotify();
return appApi
.get(`/users?id=${id}`)
.then((resp) => {
return resp.data[0];
})
.catch((err) => {
NotifyResponse(err, 'error');
return null;
});
}
export async function Me() {
return appApi.get('/login/me');
}

View File

@@ -1,6 +1,7 @@
<template>
<q-select
ref="selectRef"
:label="props.label || ''"
v-model="modelValueLocal"
:options="filteredOptions"
:option-label="optionLabel"
@@ -43,6 +44,9 @@ const props = defineProps({
type: Array as PropType<T[]>,
required: true,
},
label: {
type: String,
},
optionLabel: {
type: [Function, String] as PropType<((option: T) => string) | string | undefined>,
required: true,

View File

@@ -3,7 +3,7 @@
<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="user.user" class="text-primary">{{ currentUser?.user }}</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>
@@ -69,7 +69,7 @@ const darkMode = computed(() => {
return 'dark_mode';
});
const showLogin = computed(
() => (route.path !== '/' && route.path !== '/login') || currentUser.value?.username === '',
() => (route.path !== '/' && route.path !== '/login') || currentUser.value?.user === '',
);
const autorized = computed(() => !!user.isAuthorizedAs(['admin']));

View File

@@ -5,6 +5,7 @@ import type { Settings } from '../models/settings';
import { appName, logo } from '../models/settings';
import { clearLocalStorage, setLocalSettings } from 'src/localstorage/localStorage';
import { routerInstance } from 'src/router';
import { Me } from '../components/DatabaseCall';
const refreshTime = 10000;
let intervalId: ReturnType<typeof setInterval> | null = null;
@@ -31,12 +32,14 @@ export function useLogin() {
setLocalSettings(sets);
});
const resp = await appApi.get('/login/me');
const resp = await Me();
await userStore
.setUser({
id: resp.data.id,
username: resp.data.user,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
workspaceId: resp.data.workspace,
})
.catch((err) => NotifyResponse(err, 'error'));
@@ -60,31 +63,31 @@ export function useLogin() {
}
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(() => {});
await appApi.post('login/refresh', {}, { withCredentials: true }).catch(() => {
userStore.clearUser();
return;
});
const resp = await Me();
if (!resp) {
stopRefreshInterval();
return false;
}
userStore
.setUser({
id: resp.data.id,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
workspaceId: resp.data.workspace,
settings: resp.data.settings,
})
.catch(() => {
userStore.clearUser();
});
stopRefreshInterval();
return false;
.catch((err) => NotifyResponse(err, 'error'));
if (!intervalId) {
startRefreshInterval();
}
return true;
}
function startRefreshInterval() {

View File

@@ -25,8 +25,17 @@ export const useUserStore = defineStore('user', {
};
},
isPermittedTo: (state: UserState) => {
return (name: string, type: 'read' | 'write' | 'delete' | 'import' | 'export'): boolean => {
return (
name: string,
type: 'read' | 'write' | 'delete' | 'import' | 'export',
compareRole?: Role,
): boolean => {
const permission = state.user?.permissions?.find((r: Permission) => r.name === name);
if (compareRole && permission) {
const rolePermission = compareRole.permissions?.find((r: Permission) => r.name === name);
if (rolePermission && rolePermission?.permission > permission?.permission) return false;
}
switch (type) {
case 'read':
return permission?.permission ? (permission.permission & (1 << 0)) === 1 : false;
@@ -41,6 +50,9 @@ export const useUserStore = defineStore('user', {
}
};
},
getWorkspaceUuid: (state: UserState) => {
return state.user?.workspaces?.find((w) => w.id === state.user?.workspaceId)?.uuid;
},
},
actions: {
setFirstLogin(b: boolean) {
@@ -62,7 +74,7 @@ export const useUserStore = defineStore('user', {
if (!this.user) return;
if ($q) {
$q?.notify({
message: "user '" + this.user?.username + "' logged out",
message: "user '" + this.user?.user + "' logged out",
color: 'orange',
position: 'top',
icon: 'warning',
@@ -80,7 +92,7 @@ export const useUserStore = defineStore('user', {
],
});
} else {
console.error("user '" + this.user?.username + "' logged out");
console.error("user '" + this.user?.user + "' logged out");
}
this.user = null;
@@ -108,9 +120,12 @@ export const useUserStore = defineStore('user', {
],
});
} else {
console.error("user '" + this.user?.username + "' logged out");
console.error("user '" + this.user?.user + "' logged out");
}
});
},
isAdmin() {
return this.user?.role.role.includes('admin');
},
},
});

View File

@@ -0,0 +1,8 @@
import type { Group } from './group';
export type PageDefault = {
groups?: Group[];
selectedColumnFilter?: string;
selectedColumnOptions?: string[];
weekdays?: number[];
};

View File

@@ -0,0 +1,9 @@
export interface QDateLocale {
days: string[];
daysShort: string[];
months: string[];
monthsShort: string[];
firstDayOfWeek: number;
format24h: boolean;
pluralDay: string;
}

View File

@@ -0,0 +1,7 @@
export type Amount = {
name: string;
events: number;
minimal: number;
average: number;
maximal: number;
};

View File

@@ -1,13 +1,12 @@
import { ref } from 'vue';
export const logo = ref('');
export const appName = ref('Attendance Records');
export const databaseName = ref('members.dba');
export const logo = ref<string>('');
export const appName = ref<string>('Attendance Records');
export type Settings = {
appName: string;
icon: string;
databaseName: string;
databaseName?: string;
primaryColor: string;
primaryColorText: string;
secondaryColor: string;
@@ -18,7 +17,6 @@ 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'),

View File

@@ -1,14 +1,17 @@
import type { Permissions } from '../checkboxes/permissions';
import type { Role } from './roles';
import type { Settings } from './settings';
import type { Workspaces } from './workspaces';
export interface User {
id: number;
username: string;
user: string;
role: Role;
permissions?: Permissions;
settings?: Settings;
newDatabase?: boolean;
workspaceId?: number | undefined;
workspaces?: Workspaces;
}
export interface UserState {

View File

@@ -1,15 +1,19 @@
import type { Role } from './roles';
import type { Settings } from './settings';
import type { Workspace } from './workspaces';
export interface User {
id?: number;
user: string;
email: string;
role?: Role;
roleId?: number;
expiration?: string;
password?: string;
newPassword?: string;
settings?: Settings;
workspaceId?: number;
workspaces?: Workspace[];
}
export type Users = User[];

View File

@@ -0,0 +1,8 @@
export interface Workspace {
id?: number;
name: string;
uuid?: string;
description: string;
}
export type Workspaces = Workspace[];

View File

@@ -66,6 +66,7 @@ import { ref } from 'vue';
import { useAttendeesTable } from './AttendeesTable';
import { useMemberTable } from '../members/MembersTable';
import { getLocalPageDefaults } from 'src/localstorage/localStorage';
import type { PageDefault } from 'src/vueLib/models/pageDefaults';
//use constants and function of imports
const { attendees, updateAttendees } = useAttendeesTable();
@@ -95,8 +96,10 @@ const open = async (eventArray: number, event: Event) => {
await updateMembers(event.attendees);
// set custom filter
const defaults = getLocalPageDefaults('attendance');
setNewFilter(defaults?.filteredColumn || '', ...(defaults?.filteredValue ?? []));
const settings = getLocalPageDefaults('attendance') as PageDefault;
if (settings) {
setNewFilter(settings.selectedColumnFilter || '', ...(settings.selectedColumnOptions ?? []));
}
// set amount of missing attendace
missingAttendanceAmount.value = filteredMembers.value.length;

View File

@@ -63,10 +63,10 @@ export function useEventTable() {
const loading = ref(false);
//updates Event list from database
function updateEvents() {
async function updateEvents() {
loading.value = true;
appApi
await appApi
.get('events')
.then((resp) => {
if (resp.data === null) {

View File

@@ -116,14 +116,13 @@
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import { onMounted, ref } from 'vue';
import type { Event, Events } from 'src/vueLib/models/event';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/EventEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useEventTable } from './EventsTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import AttendeesTableDialog from '../attendees/AttendeesTableDialog.vue';
import type { Members } from 'src/vueLib/models/member';
@@ -149,20 +148,8 @@ const user = useUserStore();
const { Events, pagination, loading, columns, updateEvents } = useEventTable();
//load on mounting page
onMounted(() => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateEvents();
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
onMounted(async () => {
await updateEvents();
});
// opens dialog for all Event values
@@ -194,17 +181,17 @@ function openAttendees(eventArray: number, attendees: Members | null) {
}
//remove Event from database
function removeEvent(...removeEvents: Events) {
async function removeEvent(...removeEvents: Events) {
const EventIds: number[] = [];
removeEvents.forEach((Event: Event) => {
EventIds.push(Event.id);
});
appApi
await appApi
.post('events/delete', { ids: EventIds })
.then(() => {
updateEvents();
updateEvents().catch((err) => NotifyResponse(err, 'error'));
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))

View File

@@ -116,7 +116,6 @@ import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useGroupTable } from './GroupTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Group, Groups } from 'src/vueLib/models/group';
@@ -146,19 +145,8 @@ onMounted(async () => {
loading.value = true;
members.value = await getAllMembers();
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateGroups().catch((err) => {
NotifyResponse(err, 'error');
});
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
await updateGroups();
loading.value = false;
});
//opens dialog for one value

View File

@@ -206,15 +206,15 @@ import { useNotify } from 'src/vueLib/general/useNotify';
import { useMemberTable } from './MembersTable';
import UploadDialog from 'src/components/UploadDialog.vue';
import AddToEvent from 'src/components/AddToEvent.vue';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import type { Responsible } from 'src/vueLib/models/responsible';
import type { QTableColumn } from 'quasar';
import SearchableInput from '../components/SearchableInput.vue';
import FilterSelect from '../components/FilterSelect.vue';
import TopButtonGroup from '../components/TopButtonGroup.vue';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import type { PageDefault } from 'src/vueLib/models/pageDefaults';
const inProps = defineProps({
addAttendees: { type: Boolean },
@@ -262,7 +262,7 @@ const {
} = useMemberTable();
//load on mounting page
onMounted(() => {
onMounted(async () => {
page.value = 'members';
if (inProps.addAttendees || inProps.addResponsible) {
selectOption.value = true;
@@ -285,27 +285,26 @@ onMounted(() => {
loading.value = true;
localCompareMembers.value = inProps.compareMembers;
const defaults = getLocalPageDefaults(page.value);
selectedColumnFilter.value = defaults?.filteredColumn || '';
selectedColumnOptions.value = defaults?.filteredValue ?? [];
const settings = getLocalPageDefaults(page.value) as PageDefault;
if (settings) {
selectedColumnFilter.value = settings.selectedColumnFilter || '';
selectedColumnOptions.value = settings.selectedColumnOptions || [];
}
// set custom filter
setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value);
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateTable().catch((err) => NotifyResponse(err, 'error'));
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
await updateMembers(localCompareMembers.value, inProps.addResponsible).catch((err) =>
NotifyResponse(err, 'error'),
);
loading.value = false;
});
async function updateTable() {
async function updateTable(add?: Members) {
localCompareMembers.value = inProps.compareMembers;
if (add) {
localCompareMembers.value?.push(...add);
}
await updateMembers(localCompareMembers.value, inProps.addResponsible).catch((err) =>
NotifyResponse(err, 'error'),
);
@@ -369,7 +368,11 @@ function setColumnOptions(columnName: string) {
async function filterMembers() {
setNewFilter(selectedColumnFilter.value, ...(selectedColumnOptions.value || []));
setLocalPageDefaults(page.value, selectedColumnFilter.value, selectedColumnOptions.value || []);
const settings = <PageDefault>{
selectedColumnFilter: selectedColumnFilter.value,
selectedColumnOptions: selectedColumnOptions.value,
};
setLocalPageDefaults(page.value, settings);
await updateTable();
}
@@ -450,9 +453,10 @@ async function addMemberTo() {
if (inProps.addAttendees) {
await updateMemberLastVisit(selected.value);
} else {
await updateTable();
await updateTable(selected.value);
emit('update-event', filteredMembers.value.length);
}
selected.value = [];
}
async function updateMemberLastVisit(members: Members) {

View File

@@ -101,14 +101,13 @@
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import { onMounted, ref } from 'vue';
import DialogFrame from 'src/vueLib/dialog/DialogFrame.vue';
import MembersTable from '../members/MembersTable.vue';
import type { Members } from 'src/vueLib/models/member';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useResponsibleTable } from './ResponsibleTable';
import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang';
import type { Responsible, Responsibles } from 'src/vueLib/models/responsible';
@@ -128,22 +127,8 @@ const user = useUserStore();
const { responsibleMember, pagination, loading, columns, updateResponsibles } =
useResponsibleTable();
//load on mounting page
onMounted(() => {
loading.value = true;
appApi
.post('database/open', { dbPath: databaseName.value, create: true })
.then(() => {
updateResponsibles().catch((err) => {
NotifyResponse(err, 'error');
});
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
});
onMounted(async () => {
await updateResponsibles();
});
//opens dialog for one value

View File

@@ -5,6 +5,7 @@ import { i18n } from 'boot/lang';
import type { Roles } from 'src/vueLib/models/roles';
import { useUserStore } from 'src/vueLib/login/userStore';
import { useLogin } from 'src/vueLib/login/useLogin';
import { Me } from 'src/vueLib/components/DatabaseCall';
export const roles = ref<Roles>([]);
@@ -64,17 +65,27 @@ export function useRoleTable() {
.finally(() => {
loading.value = false;
});
await appApi
.get('/login/me')
.then((resp) => {
userStore
.setUser({ id: resp.data.id, username: resp.data.username, role: resp.data.role })
.catch((err) => NotifyResponse(err, 'error'));
login.refresh().catch((err) => NotifyResponse(err, 'error'));
})
.catch(() => {
login.logout().catch((err) => NotifyResponse(err, 'error'));
const resp = await Me().catch(() => {
login.logout().catch((err) => {
NotifyResponse(err, 'error');
return;
});
});
if (!resp) return;
await userStore
.setUser({
id: resp.data.id,
user: resp.data.user,
role: { role: resp.data.role, permissions: [] },
workspaceId: resp.data.workspaceId,
settings: resp.data.settings,
})
.catch((err) => NotifyResponse(err, 'error'));
login.refresh().catch((err) => NotifyResponse(err, 'error'));
}
return {
roles,

View File

@@ -23,17 +23,11 @@
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-btn v-if="writePermisssion" dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewRole') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
v-if="writePermisssion"
dense
flat
style="color: grey"
@@ -72,13 +66,9 @@
<q-td
:props="props"
:disable="!autorized(props.row)"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermisssion ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermisssion
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -89,19 +79,18 @@
<template v-slot:body-cell-permissions="props">
<q-td :props="props">
<q-btn
:disable="!autorized(props.row) || !user.isPermittedTo('userSettings', 'write')"
:disable="
!autorized(props.row) || !writePermisssion || user.user?.role.role === props.row.role
"
flat
dense
icon="rule"
:color="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermisssion && user.user?.role.role !== props.row.role
? 'secondary'
: 'grey'
"
@click="
user.isPermittedTo('userSettings', 'write') &&
openAllValueDialog(props.row, 'permissions')
"
@click="writePermisssion && openAllValueDialog(props.row, 'permissions')"
>
<q-tooltip> {{ $t('permissions') }} </q-tooltip>
</q-btn>
@@ -145,7 +134,7 @@
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import { ref, onMounted, computed } from 'vue';
import type { Roles, Role } from 'src/vueLib/models/roles';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/RoleEditAllDialog.vue';
@@ -158,6 +147,8 @@ import { useUserStore } from 'src/vueLib/login/userStore';
import SearchableInput from '../components/SearchableInput.vue';
const { NotifyResponse } = useNotify();
const { roles, pagination, loading, columns, updateRoles } = useRoleTable();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
@@ -169,7 +160,7 @@ const currentUser = ref();
const filter = ref('');
const user = useUserStore();
const { roles, pagination, loading, columns, updateRoles } = useRoleTable();
const writePermisssion = computed(() => user.isPermittedTo('userSettings', 'write'));
//load on mounting page
onMounted(() => {
@@ -234,10 +225,6 @@ function removeRole(...removeRoles: Roles) {
</script>
<style>
.blink-yellow {
animation: blink-yellow 1.5s step-start 6 !important;
}
.bigger-table-text .q-table__middle td {
font-size: 14px;
}

View File

@@ -43,6 +43,14 @@ export function useUserTable() {
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'workspaces',
align: 'left' as const,
label: i18n.global.t('workspaces'),
field: 'workspaces',
sortable: true,
style: 'width: 120px; max-width: 120px;',
},
{
name: 'expiration',
align: 'left' as const,
@@ -59,9 +67,9 @@ export function useUserTable() {
const loading = ref(false);
//updates user list from database
function updateUsers() {
async function updateUsers() {
loading.value = true;
appApi
await appApi
.get('users')
.then((resp) => {
if (resp.data === null) {

View File

@@ -9,7 +9,7 @@
:loading-label="$t('loading')"
:rows-per-page-label="$t('recordsPerPage')"
:selected-rows-label="(val) => val + ' ' + $t('recordSelected')"
:rows="users"
:rows="localUsers"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
@@ -23,17 +23,11 @@
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-btn v-if="writePermission" dense flat icon="add" @click="openAllValueDialog(null)">
<q-tooltip>{{ $t('addNewUser') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
v-if="writePermission"
dense
flat
style="color: grey"
@@ -43,7 +37,9 @@
<q-tooltip>{{ $t('selectUserOptions') }}</q-tooltip>
</q-btn>
</q-btn-group>
<div v-if="selectOption && selected.length > 0">
<div
v-if="selectOption && selected.length > 0 && user.isPermittedTo('userSettings', 'delete')"
>
<q-btn flat dense icon="more_vert" @click="openSubmenu = true" />
<q-menu v-if="openSubmenu" anchor="bottom middle" self="top middle">
<q-item
@@ -71,13 +67,9 @@
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -101,7 +93,11 @@
<template v-slot:body-cell-role="props">
<q-td :props="props">
<q-select
:readonly="!user.isPermittedTo('userSettings', 'write') || !autorized(props.row)"
:readonly="
user.user?.id === props.row.id ||
!user.isPermittedTo('userSettings', 'write', props.row.role) ||
!autorized(props.row)
"
dense
v-model="props.row.role"
:options="localRoles"
@@ -110,16 +106,25 @@
></q-select>
</q-td>
</template>
<template v-slot:body-cell-workspaces="props">
<q-td :props="props">
<q-select
:readonly="props.row.id === user.user?.id || !autorized(props.row) || !writePermission"
dense
v-model="props.row.workspaces"
:options="localWorkspaces"
option-label="name"
multiple
@update:model-value="updateUser(props.row)"
></q-select>
</q-td>
</template>
<template v-slot:body-cell-expiration="props">
<q-td
:props="props"
:style="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
? 'cursor: pointer'
: ''
"
:style="autorized(props.row) && writePermission ? 'cursor: pointer' : ''"
@click="
autorized(props.row) && user.isPermittedTo('userSettings', 'write')
autorized(props.row) && writePermission
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
@@ -177,31 +182,73 @@ import { i18n } from 'src/boot/lang';
import { useUserStore } from 'src/vueLib/login/userStore';
import ChangePassword from 'src/vueLib/login/ChangePassword.vue';
import SearchableInput from '../components/SearchableInput.vue';
import { useWorkspaceTable } from '../workspaces/WorkspaceTable';
const { NotifyResponse } = useNotify();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const { updateWorkspaces } = useWorkspaceTable();
const user = useUserStore();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const localRoles = computed(() => {
return roles.value.map((role) => role.role);
const localUsers = computed(() => {
return users.value.filter((u) => {
if (user.isAdmin() || user.user?.id === u.id) return user;
const roleP = u.role?.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
const notInWorkspace = u.workspaces?.some((w) => currentUser.value.workspaces.includes(w.name));
if (
roleP === undefined ||
userP === undefined ||
(!notInWorkspace && u.workspaces?.length !== 0)
)
return;
if (userP > roleP) return user;
});
});
const localRoles = computed(() => {
return roles.value.filter((role) => {
const roleP = role.permissions.find((p) => p.name === 'userSettings')?.permission;
const userP = user.user?.permissions?.find((p) => p.name === 'userSettings')?.permission;
if (roleP === undefined || userP === undefined) return;
if (userP < roleP) return;
if (user.isAdmin() || !role.role.includes('admin')) return role;
});
});
const localWorkspaces = computed(() => {
return users.value.filter((u) => u.id === user.user?.id)[0]?.workspaces;
});
const selectOption = ref(false);
const selected = ref<Users>([]);
const openSubmenu = ref(false);
const filter = ref('');
const currentUser = ref();
const { users, pagination, loading, columns, updateUsers } = useUserTable();
const { updateRoles } = useRoleTable();
const user = useUserStore();
const changePwdDialog = ref();
const writePermission = computed(() => user.isPermittedTo('userSettings', 'write'));
//load on mounting page
onMounted(() => {
onMounted(async () => {
loading.value = true;
currentUser.value = user.user;
updateUsers();
updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateUsers().finally(() => {
const targetUser = users.value.find((u) => u.id === currentUser.value.id);
if (targetUser && targetUser.workspaces) {
currentUser.value.workspaces = targetUser.workspaces.map((ws) => ws.name);
}
});
await updateRoles().catch((err) => NotifyResponse(err, 'error'));
await updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
// get workspaces of current user
});
//check authorization
@@ -261,7 +308,7 @@ function removeUser(...removeUsers: Users) {
appApi
.post('users/delete?id=' + currentUser.value.id, { ids: userIds })
.then(() => {
updateUsers();
updateUsers().catch((err) => NotifyResponse(err, 'error'));
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
@@ -272,6 +319,10 @@ function removeUser(...removeUsers: Users) {
// update role select
function updateUser(user: User) {
if (user.role?.id) {
user.roleId = user.role?.id;
}
appApi
.post('/users/update', user)
.then(() => NotifyResponse(i18n.global.t('userUpdated')))

View File

@@ -0,0 +1,78 @@
import { appApi } from 'src/boot/axios';
import { ref, computed } from 'vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { i18n } from 'boot/lang';
import type { Workspaces } from 'src/vueLib/models/workspaces';
// import { useUserStore } from 'src/vueLib/login/userStore';
// import { useLogin } from 'src/vueLib/login/useLogin';
export const workspaces = ref<Workspaces>([]);
export function useWorkspaceTable() {
const pagination = ref({
sortBy: 'name',
descending: false,
page: 1,
rowsPerPage: 20,
});
const columns = computed(() => [
{
name: 'name',
align: 'left' as const,
label: i18n.global.t('name'),
field: 'name',
sortable: true,
},
{
name: 'description',
align: 'left' as const,
label: i18n.global.t('description'),
field: 'description',
style: 'width: 120px; max-width: 120px;',
},
{ name: 'option', align: 'center' as const, label: '', field: 'option', icon: 'option' },
]);
const { NotifyResponse } = useNotify();
const loading = ref(false);
// const userStore = useUserStore();
// const login = useLogin();
//updates user list from database
async function updateWorkspaces() {
loading.value = true;
await appApi
.get('workspaces?id=0')
.then((resp) => {
if (resp.data === null) {
workspaces.value = [];
return;
}
workspaces.value = resp.data as Workspaces;
if (workspaces.value === null) {
workspaces.value = [];
return;
}
})
.catch((err) => {
NotifyResponse(err, 'error');
})
.finally(() => {
loading.value = false;
});
await appApi
.post('users/update', { id: 1, workspaces: workspaces.value })
.catch((err) => NotifyResponse(err, 'error'));
}
return {
workspaces,
pagination,
columns,
loading,
updateWorkspaces,
};
}

View File

@@ -0,0 +1,223 @@
<template>
<q-table
flat
bordered
ref="tableRef"
title="Workspaces"
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="workspaces"
: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"
>
<template v-slot:top-left>
<q-btn-group push flat style="color: grey">
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
icon="add"
@click="openAllValueDialog(null)"
>
<q-tooltip>{{ $t('addNewWorkspace') }}</q-tooltip>
</q-btn>
<q-btn
v-if="user.isPermittedTo('userSettings', 'write')"
dense
flat
style="color: grey"
:icon="selectOption ? 'check_box' : 'check_box_outline_blank'"
@click="selectOption = !selectOption"
>
<q-tooltip>{{ $t('selectWorkspaceOptions') }}</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 table-->
<template v-slot:top-right>
<SearchableInput v-model="filter" :placeholder="$t('search')" />
</template>
<!-- table body content-->
<template v-slot:body-cell="props">
<q-td
:props="props"
:style="user.isPermittedTo('userSettings', 'write') ? 'cursor: pointer' : ''"
@click="
user.isPermittedTo('userSettings', 'write')
? openSingleValueDialog(props.col.label, props.col.name, props.row)
: ''
"
>
{{ props.value }}
</q-td>
</template>
<template v-slot:body-cell-option="props">
<q-td :props="props">
<q-btn
v-if="user.isPermittedTo('userSettings', 'delete')"
flat
dense
icon="delete"
color="negative"
@click="openRemoveDialog(props.row)"
>
<q-tooltip> {{ $t('delete') }} </q-tooltip>
</q-btn>
</q-td>
</template>
</q-table>
<EditOneDialog
ref="editOneDialog"
endpoint="workspaces/update"
query-id
v-on:update="updateWorkspaces"
></EditOneDialog>
<EditAllDialog ref="editAllDialog" v-on:update="updateWorkspaces"></EditAllDialog>
<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) => removeWorkspace(...val)"
></OkDialog>
</template>
<script setup lang="ts">
import { appApi } from 'src/boot/axios';
import { ref, onMounted } from 'vue';
import type { Workspaces, Workspace } from 'src/vueLib/models/workspaces';
import EditOneDialog from 'src/components/EditOneDialog.vue';
import EditAllDialog from 'src/components/WorkspaceEditAllDialog.vue';
import OkDialog from 'src/components/dialog/OkDialog.vue';
import { useNotify } from 'src/vueLib/general/useNotify';
import { useWorkspaceTable } from './WorkspaceTable';
import { i18n } from 'src/boot/lang';
import { QTable } from 'quasar';
import { useUserStore } from 'src/vueLib/login/userStore';
import SearchableInput from '../components/SearchableInput.vue';
import { type User } from 'src/vueLib/models/user';
const { NotifyResponse } = useNotify();
const editOneDialog = ref();
const editAllDialog = ref();
const okDialog = ref();
const deleteText = ref('');
const selectOption = ref(false);
const selected = ref<Workspaces>([]);
const openSubmenu = ref(false);
const currentUser = ref<User>();
const filter = ref('');
const user = useUserStore();
const { workspaces, pagination, loading, columns, updateWorkspaces } = useWorkspaceTable();
//load on mounting page
onMounted(() => {
loading.value = true;
if (user.user) {
currentUser.value = user.user;
}
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
});
// opens dialog for all workspace values
function openSingleValueDialog(label: string, field: string, workspace: Workspace) {
editOneDialog.value?.open(label, field, workspace);
}
//opens dialog for one value
function openAllValueDialog(workspace: Workspace | null, typ?: 'permissions') {
editAllDialog.value?.open(workspace, typ);
}
//opens remove dialog
function openRemoveDialog(...workspaces: Workspaces) {
if (workspaces.length === 1) {
deleteText.value = "'" + workspaces[0]?.name + "'";
} else {
deleteText.value = String(workspaces.length) + ' ' + i18n.global.t('workspaces');
}
okDialog.value?.open(workspaces);
}
//remove workspace from database
function removeWorkspace(...removeWorkspaces: Workspaces) {
const workspaces: Workspace[] = [];
const user = useUserStore().user;
const usedWorkspaceId = user?.workspaces?.find((w) => w.uuid === user.workspaceId)?.id;
removeWorkspaces.forEach((workspace: Workspace) => {
if (workspace.id === usedWorkspaceId) {
NotifyResponse(i18n.global.t('notPossibleToDeleteUsedWorkspace'), 'error');
} else if (workspace.id) {
workspaces.push(workspace);
}
});
appApi
.post('workspaces/delete', {
workspaces: workspaces,
})
.then(() => {
updateWorkspaces().catch((err) => NotifyResponse(err, 'error'));
if (workspaces.length === 1) {
NotifyResponse("'" + workspaces[0]?.name + "' " + i18n.global.t('deleted'), 'warning');
} else {
NotifyResponse(i18n.global.t('deleteWorkspaces'), 'warning');
}
selected.value = [];
})
.catch((err) => NotifyResponse(err, 'error'))
.finally(() => {
loading.value = false;
selectOption.value = false;
});
}
</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>