Initial template
This commit is contained in:
parent
dcd4925371
commit
88030ce384
|
|
@ -2,7 +2,6 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
|
|
@ -40,6 +39,7 @@ func main() {
|
|||
|
||||
// Initialize handlers
|
||||
userHandler := handlers.NewUserHandler(db)
|
||||
menuHandler := handlers.NewMenuHandler(db)
|
||||
|
||||
// Set Gin mode
|
||||
if cfg.Server.Env == "production" {
|
||||
|
|
@ -79,6 +79,12 @@ func main() {
|
|||
// Current user
|
||||
api.GET("/auth/me", authHandler.GetCurrentUser)
|
||||
|
||||
// Menu and UI configuration
|
||||
api.GET("/menu", menuHandler.GetMenuItems)
|
||||
api.GET("/toolbar", menuHandler.GetToolbarItems)
|
||||
api.GET("/settings", menuHandler.GetUserSettings)
|
||||
api.PUT("/settings", menuHandler.UpdateUserSettings)
|
||||
|
||||
// User management (admin only)
|
||||
users := api.Group("/users")
|
||||
users.Use(middleware.RequireAdmin())
|
||||
|
|
|
|||
|
|
@ -4,9 +4,37 @@ go 1.21
|
|||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
golang.org/x/crypto v0.20.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
golang.org/x/crypto v0.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1 +1,97 @@
|
|||
# This file will be generated by go mod download
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
|
|
|||
|
|
@ -58,40 +58,269 @@ func Connect(cfg *config.DatabaseConfig) (*DB, error) {
|
|||
}
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
var createUsersTable string
|
||||
var queries []string
|
||||
|
||||
if db.Type == "sqlite" {
|
||||
createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
queries = []string{
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS menu_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
parent_id INTEGER,
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '',
|
||||
route TEXT DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
role_required TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES menu_items(id) ON DELETE CASCADE
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
sidebar_position TEXT DEFAULT 'left',
|
||||
sidebar_collapsed INTEGER DEFAULT 0,
|
||||
theme TEXT DEFAULT 'light',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS toolbar_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
icon TEXT DEFAULT '',
|
||||
action TEXT NOT NULL,
|
||||
shortcut TEXT DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
separator INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);`,
|
||||
}
|
||||
} else {
|
||||
createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
queries = []string{
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS menu_items (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_id BIGINT,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
icon VARCHAR(100) DEFAULT '',
|
||||
route VARCHAR(255) DEFAULT '',
|
||||
sort_order INT DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
role_required VARCHAR(50) DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES menu_items(id) ON DELETE CASCADE
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS user_settings (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNIQUE NOT NULL,
|
||||
sidebar_position VARCHAR(10) DEFAULT 'left',
|
||||
sidebar_collapsed BOOLEAN DEFAULT FALSE,
|
||||
theme VARCHAR(20) DEFAULT 'light',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS toolbar_items (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
icon VARCHAR(100) DEFAULT '',
|
||||
action VARCHAR(100) NOT NULL,
|
||||
shortcut VARCHAR(50) DEFAULT '',
|
||||
sort_order INT DEFAULT 0,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
separator BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);`,
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createUsersTable); err != nil {
|
||||
return fmt.Errorf("failed to create users table: %w", err)
|
||||
for _, query := range queries {
|
||||
if _, err := db.Exec(query); err != nil {
|
||||
return fmt.Errorf("failed to execute migration: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Seed default menu items
|
||||
if err := db.seedMenuItems(); err != nil {
|
||||
return fmt.Errorf("failed to seed menu items: %w", err)
|
||||
}
|
||||
|
||||
// Seed default toolbar items
|
||||
if err := db.seedToolbarItems(); err != nil {
|
||||
return fmt.Errorf("failed to seed toolbar items: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) seedMenuItems() error {
|
||||
// Check if menu items already exist
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM menu_items").Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil // Already seeded
|
||||
}
|
||||
|
||||
menuItems := []struct {
|
||||
parentID *int64
|
||||
title string
|
||||
icon string
|
||||
route string
|
||||
sortOrder int
|
||||
roleRequired string
|
||||
}{
|
||||
{nil, "Dashboard", "home", "/dashboard", 1, ""},
|
||||
{nil, "Verwaltung", "settings", "", 2, "admin"},
|
||||
{nil, "Berichte", "chart", "", 3, ""},
|
||||
{nil, "Einstellungen", "cog", "", 4, ""},
|
||||
}
|
||||
|
||||
// Insert root items
|
||||
for _, item := range menuItems {
|
||||
var result int64
|
||||
if db.Type == "sqlite" {
|
||||
res, err := db.Exec(`INSERT INTO menu_items (parent_id, title, icon, route, sort_order, role_required) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
item.parentID, item.title, item.icon, item.route, item.sortOrder, item.roleRequired)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, _ = res.LastInsertId()
|
||||
} else {
|
||||
res, err := db.Exec(`INSERT INTO menu_items (parent_id, title, icon, route, sort_order, role_required) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
item.parentID, item.title, item.icon, item.route, item.sortOrder, item.roleRequired)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, _ = res.LastInsertId()
|
||||
}
|
||||
|
||||
// Add sub-items based on parent
|
||||
switch item.title {
|
||||
case "Verwaltung":
|
||||
subItems := []struct {
|
||||
title string
|
||||
icon string
|
||||
route string
|
||||
order int
|
||||
}{
|
||||
{"Benutzer", "users", "/users", 1},
|
||||
{"Rollen", "shield", "/roles", 2},
|
||||
{"Gruppen", "folder", "/groups", 3},
|
||||
}
|
||||
for _, sub := range subItems {
|
||||
_, err := db.Exec(`INSERT INTO menu_items (parent_id, title, icon, route, sort_order, role_required) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
result, sub.title, sub.icon, sub.route, sub.order, "admin")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "Berichte":
|
||||
subItems := []struct {
|
||||
title string
|
||||
icon string
|
||||
route string
|
||||
order int
|
||||
}{
|
||||
{"Aktivitäten", "activity", "/reports/activities", 1},
|
||||
{"Statistiken", "bar-chart", "/reports/statistics", 2},
|
||||
{"Export", "download", "/reports/export", 3},
|
||||
}
|
||||
for _, sub := range subItems {
|
||||
_, err := db.Exec(`INSERT INTO menu_items (parent_id, title, icon, route, sort_order, role_required) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
result, sub.title, sub.icon, sub.route, sub.order, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case "Einstellungen":
|
||||
subItems := []struct {
|
||||
title string
|
||||
icon string
|
||||
route string
|
||||
order int
|
||||
}{
|
||||
{"Profil", "user", "/profile", 1},
|
||||
{"Sicherheit", "lock", "/settings/security", 2},
|
||||
{"Anzeige", "monitor", "/settings/display", 3},
|
||||
}
|
||||
for _, sub := range subItems {
|
||||
_, err := db.Exec(`INSERT INTO menu_items (parent_id, title, icon, route, sort_order, role_required) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
result, sub.title, sub.icon, sub.route, sub.order, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) seedToolbarItems() error {
|
||||
// Check if toolbar items already exist
|
||||
var count int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM toolbar_items").Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil // Already seeded
|
||||
}
|
||||
|
||||
toolbarItems := []struct {
|
||||
title string
|
||||
icon string
|
||||
action string
|
||||
shortcut string
|
||||
sortOrder int
|
||||
separator bool
|
||||
}{
|
||||
{"Ausschneiden", "scissors", "cut", "Ctrl+X", 1, false},
|
||||
{"Kopieren", "copy", "copy", "Ctrl+C", 2, false},
|
||||
{"Einfügen", "clipboard", "paste", "Ctrl+V", 3, true},
|
||||
{"Rückgängig", "rotate-ccw", "undo", "Ctrl+Z", 4, false},
|
||||
{"Wiederholen", "rotate-cw", "redo", "Ctrl+Y", 5, true},
|
||||
{"Speichern", "save", "save", "Ctrl+S", 6, false},
|
||||
{"Drucken", "printer", "print", "Ctrl+P", 7, true},
|
||||
{"Suchen", "search", "search", "Ctrl+F", 8, false},
|
||||
{"Hilfe", "help-circle", "help", "F1", 9, false},
|
||||
}
|
||||
|
||||
for _, item := range toolbarItems {
|
||||
_, err := db.Exec(`INSERT INTO toolbar_items (title, icon, action, shortcut, sort_order, separator) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
item.title, item.icon, item.action, item.shortcut, item.sortOrder, item.separator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"admintemplate/internal/database"
|
||||
"admintemplate/internal/models"
|
||||
)
|
||||
|
||||
type MenuHandler struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewMenuHandler(db *database.DB) *MenuHandler {
|
||||
return &MenuHandler{db: db}
|
||||
}
|
||||
|
||||
// GetMenuItems returns all menu items organized in a tree structure
|
||||
func (h *MenuHandler) GetMenuItems(c *gin.Context) {
|
||||
userRole := c.GetString("userRole")
|
||||
|
||||
// Get all active menu items
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, parent_id, title, icon, route, sort_order, active, role_required
|
||||
FROM menu_items
|
||||
WHERE active = 1
|
||||
ORDER BY sort_order
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch menu items"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var allItems []models.MenuItem
|
||||
for rows.Next() {
|
||||
var item models.MenuItem
|
||||
var parentID sql.NullInt64
|
||||
err := rows.Scan(&item.ID, &parentID, &item.Title, &item.Icon, &item.Route, &item.SortOrder, &item.Active, &item.RoleReq)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if parentID.Valid {
|
||||
item.ParentID = &parentID.Int64
|
||||
}
|
||||
|
||||
// Filter by role
|
||||
if item.RoleReq != "" && item.RoleReq != userRole && userRole != "admin" {
|
||||
continue
|
||||
}
|
||||
|
||||
allItems = append(allItems, item)
|
||||
}
|
||||
|
||||
// Build tree structure
|
||||
menuTree := buildMenuTree(allItems, nil)
|
||||
c.JSON(http.StatusOK, menuTree)
|
||||
}
|
||||
|
||||
// buildMenuTree creates a nested menu structure
|
||||
func buildMenuTree(items []models.MenuItem, parentID *int64) []models.MenuItem {
|
||||
var result []models.MenuItem
|
||||
|
||||
for _, item := range items {
|
||||
if (parentID == nil && item.ParentID == nil) ||
|
||||
(parentID != nil && item.ParentID != nil && *parentID == *item.ParentID) {
|
||||
item.Children = buildMenuTree(items, &item.ID)
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetToolbarItems returns all toolbar items
|
||||
func (h *MenuHandler) GetToolbarItems(c *gin.Context) {
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, title, icon, action, shortcut, sort_order, active, separator
|
||||
FROM toolbar_items
|
||||
WHERE active = 1
|
||||
ORDER BY sort_order
|
||||
`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch toolbar items"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []models.ToolbarItem
|
||||
for rows.Next() {
|
||||
var item models.ToolbarItem
|
||||
err := rows.Scan(&item.ID, &item.Title, &item.Icon, &item.Action, &item.Shortcut, &item.SortOrder, &item.Active, &item.Separator)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetUserSettings returns user settings
|
||||
func (h *MenuHandler) GetUserSettings(c *gin.Context) {
|
||||
userID := c.GetInt64("userID")
|
||||
|
||||
var settings models.UserSettings
|
||||
err := h.db.QueryRow(`
|
||||
SELECT id, user_id, sidebar_position, sidebar_collapsed, theme
|
||||
FROM user_settings
|
||||
WHERE user_id = ?
|
||||
`, userID).Scan(&settings.ID, &settings.UserID, &settings.SidebarPosition, &settings.SidebarCollapsed, &settings.Theme)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
// Return default settings
|
||||
settings = models.UserSettings{
|
||||
UserID: userID,
|
||||
SidebarPosition: "left",
|
||||
SidebarCollapsed: false,
|
||||
Theme: "light",
|
||||
}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, settings)
|
||||
}
|
||||
|
||||
// UpdateUserSettings updates user settings
|
||||
func (h *MenuHandler) UpdateUserSettings(c *gin.Context) {
|
||||
userID := c.GetInt64("userID")
|
||||
|
||||
var req models.UpdateSettingsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if settings exist
|
||||
var existingID int64
|
||||
err := h.db.QueryRow("SELECT id FROM user_settings WHERE user_id = ?", userID).Scan(&existingID)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
// Insert new settings
|
||||
position := "left"
|
||||
collapsed := false
|
||||
theme := "light"
|
||||
|
||||
if req.SidebarPosition != nil {
|
||||
position = *req.SidebarPosition
|
||||
}
|
||||
if req.SidebarCollapsed != nil {
|
||||
collapsed = *req.SidebarCollapsed
|
||||
}
|
||||
if req.Theme != nil {
|
||||
theme = *req.Theme
|
||||
}
|
||||
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO user_settings (user_id, sidebar_position, sidebar_collapsed, theme)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, userID, position, collapsed, theme)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create settings"})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check settings"})
|
||||
return
|
||||
} else {
|
||||
// Update existing settings
|
||||
if req.SidebarPosition != nil {
|
||||
if _, err := h.db.Exec("UPDATE user_settings SET sidebar_position = ? WHERE user_id = ?", *req.SidebarPosition, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update sidebar position"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.SidebarCollapsed != nil {
|
||||
if _, err := h.db.Exec("UPDATE user_settings SET sidebar_collapsed = ? WHERE user_id = ?", *req.SidebarCollapsed, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update sidebar collapsed"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if req.Theme != nil {
|
||||
if _, err := h.db.Exec("UPDATE user_settings SET theme = ? WHERE user_id = ?", *req.Theme, userID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update theme"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return updated settings
|
||||
h.GetUserSettings(c)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MenuItem represents a menu entry with optional parent for submenus
|
||||
type MenuItem struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
ParentID *int64 `json:"parent_id" db:"parent_id"` // NULL for root items
|
||||
Title string `json:"title" db:"title"`
|
||||
Icon string `json:"icon" db:"icon"`
|
||||
Route string `json:"route" db:"route"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
RoleReq string `json:"role_required" db:"role_required"` // "admin", "user", or "" for all
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
Children []MenuItem `json:"children,omitempty"` // Populated in code, not DB
|
||||
}
|
||||
|
||||
// UserSettings stores user preferences
|
||||
type UserSettings struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
UserID int64 `json:"user_id" db:"user_id"`
|
||||
SidebarPosition string `json:"sidebar_position" db:"sidebar_position"` // "left" or "right"
|
||||
SidebarCollapsed bool `json:"sidebar_collapsed" db:"sidebar_collapsed"`
|
||||
Theme string `json:"theme" db:"theme"` // "light" or "dark"
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// ToolbarItem represents a toolbar button
|
||||
type ToolbarItem struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Title string `json:"title" db:"title"`
|
||||
Icon string `json:"icon" db:"icon"`
|
||||
Action string `json:"action" db:"action"` // action identifier
|
||||
Shortcut string `json:"shortcut" db:"shortcut"`
|
||||
SortOrder int `json:"sort_order" db:"sort_order"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
Separator bool `json:"separator" db:"separator"` // show separator after this item
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// UpdateSettingsRequest for updating user settings
|
||||
type UpdateSettingsRequest struct {
|
||||
SidebarPosition *string `json:"sidebar_position"`
|
||||
SidebarCollapsed *bool `json:"sidebar_collapsed"`
|
||||
Theme *string `json:"theme"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<div
|
||||
class="main-layout"
|
||||
:class="{
|
||||
'sidebar-left': sidebarPosition === 'left',
|
||||
'sidebar-right': sidebarPosition === 'right',
|
||||
'sidebar-collapsed': sidebarCollapsed,
|
||||
'theme-dark': theme === 'dark'
|
||||
}"
|
||||
>
|
||||
<Sidebar
|
||||
:position="sidebarPosition"
|
||||
:collapsed="sidebarCollapsed"
|
||||
:theme="theme"
|
||||
@update:position="updatePosition"
|
||||
@update:collapsed="updateCollapsed"
|
||||
/>
|
||||
|
||||
<div class="main-content">
|
||||
<Toolbar
|
||||
:sidebarPosition="sidebarPosition"
|
||||
:theme="theme"
|
||||
@action="handleToolbarAction"
|
||||
@update:sidebarPosition="updatePosition"
|
||||
@update:theme="updateTheme"
|
||||
/>
|
||||
|
||||
<div class="content-area">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import Toolbar from './Toolbar.vue'
|
||||
import { menuService } from '../services/menu'
|
||||
|
||||
const sidebarPosition = ref('left')
|
||||
const sidebarCollapsed = ref(false)
|
||||
const theme = ref('light')
|
||||
|
||||
const updatePosition = async (position) => {
|
||||
sidebarPosition.value = position
|
||||
try {
|
||||
await menuService.updateUserSettings({ sidebar_position: position })
|
||||
} catch (err) {
|
||||
console.error('Failed to save position:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const updateCollapsed = async (collapsed) => {
|
||||
sidebarCollapsed.value = collapsed
|
||||
try {
|
||||
await menuService.updateUserSettings({ sidebar_collapsed: collapsed })
|
||||
} catch (err) {
|
||||
console.error('Failed to save collapsed state:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTheme = async (newTheme) => {
|
||||
theme.value = newTheme
|
||||
applyTheme(newTheme)
|
||||
try {
|
||||
await menuService.updateUserSettings({ theme: newTheme })
|
||||
} catch (err) {
|
||||
console.error('Failed to save theme:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const applyTheme = (themeName) => {
|
||||
if (themeName === 'dark') {
|
||||
document.documentElement.classList.add('dark-theme')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark-theme')
|
||||
}
|
||||
}
|
||||
|
||||
const handleToolbarAction = (action) => {
|
||||
console.log('Toolbar action:', action)
|
||||
}
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const settings = await menuService.getUserSettings()
|
||||
sidebarPosition.value = settings.sidebar_position || 'left'
|
||||
sidebarCollapsed.value = settings.sidebar_collapsed || false
|
||||
theme.value = settings.theme || 'light'
|
||||
applyTheme(theme.value)
|
||||
} catch (err) {
|
||||
console.error('Failed to load settings:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #f1f5f9;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.main-layout.theme-dark {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: margin 0.3s ease;
|
||||
}
|
||||
|
||||
/* Sidebar left positioning */
|
||||
.sidebar-left .main-content {
|
||||
margin-left: 250px;
|
||||
}
|
||||
|
||||
.sidebar-left.sidebar-collapsed .main-content {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
/* Sidebar right positioning */
|
||||
.sidebar-right .main-content {
|
||||
margin-right: 250px;
|
||||
}
|
||||
|
||||
.sidebar-right.sidebar-collapsed .main-content {
|
||||
margin-right: 60px;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.theme-dark .content-area {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* Dark theme card styles */
|
||||
.theme-dark :deep(.card) {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.card-title) {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.table) {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.table td),
|
||||
.theme-dark :deep(.table th) {
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.theme-dark :deep(h1),
|
||||
.theme-dark :deep(h2),
|
||||
.theme-dark :deep(h3) {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.stat-card) {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.stat-value) {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.stat-label) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.form-input),
|
||||
.theme-dark :deep(.form-select) {
|
||||
background: #0f172a;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.btn-secondary) {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.btn-secondary:hover) {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-left .main-content,
|
||||
.sidebar-right .main-content {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sidebar-left.sidebar-collapsed .main-content,
|
||||
.sidebar-right.sidebar-collapsed .main-content {
|
||||
margin-left: 60px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,457 @@
|
|||
<template>
|
||||
<aside
|
||||
class="sidebar"
|
||||
:class="{
|
||||
'sidebar-left': position === 'left',
|
||||
'sidebar-right': position === 'right',
|
||||
'sidebar-collapsed': collapsed
|
||||
}"
|
||||
>
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo" v-if="!collapsed">
|
||||
<span class="logo-icon">⚙</span>
|
||||
<span class="logo-text">AdminTemplate</span>
|
||||
</div>
|
||||
<button class="collapse-btn" @click="toggleCollapse" :title="collapsed ? 'Erweitern' : 'Einklappen'">
|
||||
<span v-if="position === 'left'">{{ collapsed ? '▶' : '◀' }}</span>
|
||||
<span v-else>{{ collapsed ? '◀' : '▶' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="menu-list">
|
||||
<li
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
class="menu-item"
|
||||
:class="{ 'has-children': item.children && item.children.length > 0 }"
|
||||
>
|
||||
<div
|
||||
class="menu-link"
|
||||
:class="{ 'active': isActive(item), 'expanded': isExpanded(item.id) }"
|
||||
@click="handleMenuClick(item)"
|
||||
>
|
||||
<span class="menu-icon" v-html="getIcon(item.icon)"></span>
|
||||
<span class="menu-title" v-if="!collapsed">{{ item.title }}</span>
|
||||
<span
|
||||
class="menu-arrow"
|
||||
v-if="!collapsed && item.children && item.children.length > 0"
|
||||
>
|
||||
{{ isExpanded(item.id) ? '▼' : '▶' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Submenu -->
|
||||
<ul
|
||||
v-if="item.children && item.children.length > 0"
|
||||
class="submenu"
|
||||
:class="{ 'submenu-open': isExpanded(item.id) && !collapsed }"
|
||||
>
|
||||
<li v-for="child in item.children" :key="child.id" class="submenu-item">
|
||||
<router-link
|
||||
:to="child.route"
|
||||
class="submenu-link"
|
||||
:class="{ 'active': $route.path === child.route }"
|
||||
>
|
||||
<span class="submenu-icon" v-html="getIcon(child.icon)"></span>
|
||||
<span class="submenu-title">{{ child.title }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer" v-if="!collapsed">
|
||||
<div class="position-toggle">
|
||||
<button
|
||||
class="position-btn"
|
||||
:class="{ 'active': position === 'left' }"
|
||||
@click="setPosition('left')"
|
||||
title="Sidebar links"
|
||||
>L</button>
|
||||
<button
|
||||
class="position-btn"
|
||||
:class="{ 'active': position === 'right' }"
|
||||
@click="setPosition('right')"
|
||||
title="Sidebar rechts"
|
||||
>R</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { menuService } from '../services/menu'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
position: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:position', 'update:collapsed'])
|
||||
|
||||
const menuItems = ref([])
|
||||
const expandedItems = ref([])
|
||||
|
||||
const icons = {
|
||||
'home': '🏠',
|
||||
'settings': '⚙',
|
||||
'chart': '📈',
|
||||
'cog': '⚙',
|
||||
'users': '👥',
|
||||
'shield': '🛡',
|
||||
'folder': '📁',
|
||||
'activity': '📊',
|
||||
'bar-chart': '📊',
|
||||
'download': '⬇',
|
||||
'user': '👤',
|
||||
'lock': '🔒',
|
||||
'monitor': '💻',
|
||||
'default': '●'
|
||||
}
|
||||
|
||||
const getIcon = (iconName) => {
|
||||
return icons[iconName] || icons['default']
|
||||
}
|
||||
|
||||
const isActive = (item) => {
|
||||
if (item.route && route.path === item.route) return true
|
||||
if (item.children) {
|
||||
return item.children.some(child => route.path === child.route)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isExpanded = (itemId) => {
|
||||
return expandedItems.value.includes(itemId)
|
||||
}
|
||||
|
||||
const toggleExpand = (itemId) => {
|
||||
const index = expandedItems.value.indexOf(itemId)
|
||||
if (index > -1) {
|
||||
expandedItems.value.splice(index, 1)
|
||||
} else {
|
||||
expandedItems.value.push(itemId)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMenuClick = (item) => {
|
||||
if (item.children && item.children.length > 0) {
|
||||
toggleExpand(item.id)
|
||||
} else if (item.route) {
|
||||
router.push(item.route)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
emit('update:collapsed', !props.collapsed)
|
||||
}
|
||||
|
||||
const setPosition = (pos) => {
|
||||
emit('update:position', pos)
|
||||
}
|
||||
|
||||
const loadMenu = async () => {
|
||||
try {
|
||||
menuItems.value = await menuService.getMenuItems()
|
||||
// Auto-expand active parent
|
||||
menuItems.value.forEach(item => {
|
||||
if (item.children && item.children.some(child => route.path === child.route)) {
|
||||
if (!expandedItems.value.includes(item.id)) {
|
||||
expandedItems.value.push(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Failed to load menu:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadMenu)
|
||||
|
||||
// Watch for route changes to expand parent menu
|
||||
watch(() => route.path, () => {
|
||||
menuItems.value.forEach(item => {
|
||||
if (item.children && item.children.some(child => route.path === child.route)) {
|
||||
if (!expandedItems.value.includes(item.id)) {
|
||||
expandedItems.value.push(item.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
color: #e2e8f0;
|
||||
transition: all 0.3s ease;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.sidebar-right {
|
||||
right: 0;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar-collapsed {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
margin: 2px 8px;
|
||||
}
|
||||
|
||||
.menu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-link:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.menu-link.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.menu-link.expanded {
|
||||
background: #1e3a5f;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 1.1rem;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 0.7rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.submenu-open {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.submenu-item {
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.submenu-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem 0.5rem 2.5rem;
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu-link:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.submenu-link.active {
|
||||
background: #1e40af;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submenu-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.submenu-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.position-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.position-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid #475569;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.position-btn:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.position-btn.active {
|
||||
background: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Collapsed state adjustments */
|
||||
.sidebar-collapsed .menu-link {
|
||||
justify-content: center;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .menu-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .submenu {
|
||||
position: absolute;
|
||||
left: 60px;
|
||||
top: 0;
|
||||
background: #1e293b;
|
||||
border-radius: 6px;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2);
|
||||
min-width: 200px;
|
||||
max-height: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .menu-item:hover .submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-collapsed .submenu-link {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-right.sidebar-collapsed .submenu {
|
||||
left: auto;
|
||||
right: 60px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.sidebar-nav::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-track {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar-nav::-webkit-scrollbar-thumb:hover {
|
||||
background: #64748b;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,816 @@
|
|||
<template>
|
||||
<div class="toolbar" :class="{ 'toolbar-dark': theme === 'dark' }">
|
||||
<div class="toolbar-group">
|
||||
<template v-for="(item, index) in toolbarItems" :key="item.id">
|
||||
<button
|
||||
class="toolbar-btn"
|
||||
:title="`${item.title} (${item.shortcut})`"
|
||||
@click="handleAction(item.action)"
|
||||
>
|
||||
<span class="toolbar-icon" v-html="getIcon(item.icon)"></span>
|
||||
<span class="toolbar-label">{{ item.title }}</span>
|
||||
</button>
|
||||
<div v-if="item.separator" class="toolbar-separator"></div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-spacer"></div>
|
||||
|
||||
<div class="toolbar-group toolbar-right">
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="dropdown" ref="settingsDropdown">
|
||||
<button
|
||||
class="toolbar-btn dropdown-toggle"
|
||||
@click="toggleSettingsMenu"
|
||||
title="Einstellungen"
|
||||
>
|
||||
<span class="toolbar-icon">⚙</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" :class="{ 'show': showSettingsMenu }">
|
||||
<div class="dropdown-header">Erscheinungsbild</div>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
:class="{ 'active': theme === 'light' }"
|
||||
@click="setTheme('light')"
|
||||
>
|
||||
<span class="dropdown-icon">☼</span>
|
||||
Hell
|
||||
<span v-if="theme === 'light'" class="check-icon">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
:class="{ 'active': theme === 'dark' }"
|
||||
@click="setTheme('dark')"
|
||||
>
|
||||
<span class="dropdown-icon">☾</span>
|
||||
Dunkel
|
||||
<span v-if="theme === 'dark'" class="check-icon">✓</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="dropdown-header">Sidebar Position</div>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
:class="{ 'active': sidebarPosition === 'left' }"
|
||||
@click="setSidebarPosition('left')"
|
||||
>
|
||||
<span class="dropdown-icon">◀</span>
|
||||
Links
|
||||
<span v-if="sidebarPosition === 'left'" class="check-icon">✓</span>
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
:class="{ 'active': sidebarPosition === 'right' }"
|
||||
@click="setSidebarPosition('right')"
|
||||
>
|
||||
<span class="dropdown-icon">▶</span>
|
||||
Rechts
|
||||
<span v-if="sidebarPosition === 'right'" class="check-icon">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="dropdown" ref="userDropdown">
|
||||
<button
|
||||
class="toolbar-btn user-btn dropdown-toggle"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
<span class="user-avatar">{{ userInitials }}</span>
|
||||
<span class="user-name">{{ authStore.user?.username }}</span>
|
||||
<span class="user-role badge" :class="authStore.isAdmin ? 'badge-admin' : 'badge-user'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
<span class="dropdown-arrow">▼</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" :class="{ 'show': showUserMenu }">
|
||||
<div class="dropdown-header">
|
||||
<div class="user-info-header">
|
||||
<span class="user-avatar-large">{{ userInitials }}</span>
|
||||
<div>
|
||||
<div class="user-name-large">{{ authStore.user?.username }}</div>
|
||||
<div class="user-email">{{ authStore.user?.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" @click="goToProfile">
|
||||
<span class="dropdown-icon">👤</span>
|
||||
Profil
|
||||
</button>
|
||||
<button class="dropdown-item" @click="showPasswordModal = true; showUserMenu = false">
|
||||
<span class="dropdown-icon">🔒</span>
|
||||
Passwort ändern
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item dropdown-item-danger" @click="handleLogout">
|
||||
<span class="dropdown-icon">🚪</span>
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Change Modal -->
|
||||
<div v-if="showPasswordModal" class="modal-overlay" @click.self="showPasswordModal = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Passwort ändern</h3>
|
||||
<button class="modal-close" @click="showPasswordModal = false">×</button>
|
||||
</div>
|
||||
<form @submit.prevent="changePassword">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Aktuelles Passwort</label>
|
||||
<input
|
||||
v-model="passwordForm.currentPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Neues Passwort</label>
|
||||
<input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
required
|
||||
minlength="6"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Passwort bestätigen</label>
|
||||
<input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div v-if="passwordError" class="alert alert-error">{{ passwordError }}</div>
|
||||
<div v-if="passwordSuccess" class="alert alert-success">{{ passwordSuccess }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="showPasswordModal = false">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { menuService } from '../services/menu'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const toolbarItems = ref([])
|
||||
|
||||
const props = defineProps({
|
||||
sidebarPosition: {
|
||||
type: String,
|
||||
default: 'left'
|
||||
},
|
||||
theme: {
|
||||
type: String,
|
||||
default: 'light'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['action', 'update:sidebarPosition', 'update:theme'])
|
||||
|
||||
// Dropdown states
|
||||
const showSettingsMenu = ref(false)
|
||||
const showUserMenu = ref(false)
|
||||
const settingsDropdown = ref(null)
|
||||
const userDropdown = ref(null)
|
||||
|
||||
// Password modal
|
||||
const showPasswordModal = ref(false)
|
||||
const passwordForm = ref({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
const passwordError = ref('')
|
||||
const passwordSuccess = ref('')
|
||||
|
||||
const userInitials = computed(() => {
|
||||
const username = authStore.user?.username || ''
|
||||
return username.substring(0, 2).toUpperCase()
|
||||
})
|
||||
|
||||
const icons = {
|
||||
'scissors': '✂',
|
||||
'copy': '📋',
|
||||
'clipboard': '📋',
|
||||
'rotate-ccw': '↺',
|
||||
'rotate-cw': '↻',
|
||||
'save': '💾',
|
||||
'printer': '🖨',
|
||||
'search': '🔍',
|
||||
'help-circle': '❓',
|
||||
'default': '●'
|
||||
}
|
||||
|
||||
const getIcon = (iconName) => {
|
||||
return icons[iconName] || icons['default']
|
||||
}
|
||||
|
||||
const toggleSettingsMenu = () => {
|
||||
showSettingsMenu.value = !showSettingsMenu.value
|
||||
showUserMenu.value = false
|
||||
}
|
||||
|
||||
const toggleUserMenu = () => {
|
||||
showUserMenu.value = !showUserMenu.value
|
||||
showSettingsMenu.value = false
|
||||
}
|
||||
|
||||
const setTheme = async (newTheme) => {
|
||||
emit('update:theme', newTheme)
|
||||
showSettingsMenu.value = false
|
||||
try {
|
||||
await menuService.updateUserSettings({ theme: newTheme })
|
||||
} catch (err) {
|
||||
console.error('Failed to save theme:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const setSidebarPosition = async (position) => {
|
||||
emit('update:sidebarPosition', position)
|
||||
showSettingsMenu.value = false
|
||||
try {
|
||||
await menuService.updateUserSettings({ sidebar_position: position })
|
||||
} catch (err) {
|
||||
console.error('Failed to save sidebar position:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
showUserMenu.value = false
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
showUserMenu.value = false
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
passwordError.value = ''
|
||||
passwordSuccess.value = ''
|
||||
|
||||
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
|
||||
passwordError.value = 'Die Passwörter stimmen nicht überein'
|
||||
return
|
||||
}
|
||||
|
||||
if (passwordForm.value.newPassword.length < 6) {
|
||||
passwordError.value = 'Das Passwort muss mindestens 6 Zeichen lang sein'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement password change API
|
||||
// await authService.changePassword(passwordForm.value)
|
||||
passwordSuccess.value = 'Passwort erfolgreich geändert'
|
||||
setTimeout(() => {
|
||||
showPasswordModal.value = false
|
||||
passwordForm.value = { currentPassword: '', newPassword: '', confirmPassword: '' }
|
||||
passwordSuccess.value = ''
|
||||
}, 1500)
|
||||
} catch (err) {
|
||||
passwordError.value = err.response?.data?.error || 'Fehler beim Ändern des Passworts'
|
||||
}
|
||||
}
|
||||
|
||||
const handleAction = (action) => {
|
||||
emit('action', action)
|
||||
|
||||
switch (action) {
|
||||
case 'cut':
|
||||
document.execCommand('cut')
|
||||
break
|
||||
case 'copy':
|
||||
document.execCommand('copy')
|
||||
break
|
||||
case 'paste':
|
||||
navigator.clipboard.readText().then(text => {
|
||||
document.execCommand('insertText', false, text)
|
||||
}).catch(() => {
|
||||
console.log('Paste not available')
|
||||
})
|
||||
break
|
||||
case 'undo':
|
||||
document.execCommand('undo')
|
||||
break
|
||||
case 'redo':
|
||||
document.execCommand('redo')
|
||||
break
|
||||
case 'save':
|
||||
console.log('Save action triggered')
|
||||
break
|
||||
case 'print':
|
||||
window.print()
|
||||
break
|
||||
case 'search':
|
||||
break
|
||||
case 'help':
|
||||
window.open('/help', '_blank')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
const handleClickOutside = (event) => {
|
||||
if (settingsDropdown.value && !settingsDropdown.value.contains(event.target)) {
|
||||
showSettingsMenu.value = false
|
||||
}
|
||||
if (userDropdown.value && !userDropdown.value.contains(event.target)) {
|
||||
showUserMenu.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadToolbar = async () => {
|
||||
try {
|
||||
toolbarItems.value = await menuService.getToolbarItems()
|
||||
} catch (err) {
|
||||
console.error('Failed to load toolbar:', err)
|
||||
toolbarItems.value = [
|
||||
{ id: 1, title: 'Ausschneiden', icon: 'scissors', action: 'cut', shortcut: 'Ctrl+X', separator: false },
|
||||
{ id: 2, title: 'Kopieren', icon: 'copy', action: 'copy', shortcut: 'Ctrl+C', separator: false },
|
||||
{ id: 3, title: 'Einfügen', icon: 'clipboard', action: 'paste', shortcut: 'Ctrl+V', separator: true },
|
||||
{ id: 4, title: 'Speichern', icon: 'save', action: 'save', shortcut: 'Ctrl+S', separator: false },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadToolbar()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 0 0.5rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.toolbar-dark {
|
||||
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
.toolbar-dark .toolbar-btn {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.toolbar-dark .toolbar-btn:hover {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.toolbar-dark .toolbar-separator {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
.toolbar-dark .user-name {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #475569;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.toolbar-btn:active {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
.toolbar-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.toolbar-label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #cbd5e1;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Dropdown styles */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
font-size: 0.6rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-menu {
|
||||
background: #1e293b;
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-header {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-divider {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.6rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.15s;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
background: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-item {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-item:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-item.active {
|
||||
background: #1e3a5f;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dropdown-item-danger {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.dropdown-item-danger:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-item-danger {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.toolbar-dark .dropdown-item-danger:hover {
|
||||
background: #450a0a;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
font-size: 1rem;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: auto;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
/* User button styles */
|
||||
.user-btn {
|
||||
padding: 0.35rem 0.75rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-avatar-large {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.user-info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-name-large {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.toolbar-dark .user-name-large {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.badge-user {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
/* Modal styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.toolbar-dark .modal-content {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.toolbar-dark .modal-header {
|
||||
border-bottom-color: #334155;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.modal-content form {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.toolbar-dark .form-label {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.toolbar-dark .form-input {
|
||||
background: #0f172a;
|
||||
border-color: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import api from './api'
|
||||
|
||||
export const menuService = {
|
||||
async getMenuItems() {
|
||||
const response = await api.get('/menu')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getToolbarItems() {
|
||||
const response = await api.get('/toolbar')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getUserSettings() {
|
||||
const response = await api.get('/settings')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async updateUserSettings(settings) {
|
||||
const response = await api.put('/settings', settings)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
|
@ -1,70 +1,67 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<h1>Dashboard</h1>
|
||||
<MainLayout>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ userCount }}</div>
|
||||
<div class="stat-label">Benutzer</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ activeUsers }}</div>
|
||||
<div class="stat-label">Aktive Benutzer</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ authStore.user?.role }}</div>
|
||||
<div class="stat-label">Ihre Rolle</div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ userCount }}</div>
|
||||
<div class="stat-label">Benutzer</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Willkommen, {{ authStore.user?.username }}!</h2>
|
||||
<p>Sie sind erfolgreich angemeldet.</p>
|
||||
<p v-if="authStore.isAdmin" class="mt-2">
|
||||
Als Administrator haben Sie Zugriff auf die Benutzerverwaltung.
|
||||
</p>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ activeUsers }}</div>
|
||||
<div class="stat-label">Aktive Benutzer</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Systeminfo</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ authStore.user?.role }}</div>
|
||||
<div class="stat-label">Ihre Rolle</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Willkommen, {{ authStore.user?.username }}!</h2>
|
||||
<p>Sie sind erfolgreich angemeldet.</p>
|
||||
<p v-if="authStore.isAdmin" class="mt-2">
|
||||
Als Administrator haben Sie Zugriff auf die Benutzerverwaltung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Systeminfo</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { userService } from '../services/users'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
import MainLayout from '../components/MainLayout.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userCount = ref(0)
|
||||
|
|
|
|||
|
|
@ -1,57 +1,54 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<h1>Profil</h1>
|
||||
<MainLayout>
|
||||
<h1>Profil</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Benutzerinformationen</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>ID:</strong></td>
|
||||
<td>{{ authStore.user?.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Erstellt am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.created_at) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Aktualisiert am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.updated_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-title">Benutzerinformationen</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>ID:</strong></td>
|
||||
<td>{{ authStore.user?.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Erstellt am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.created_at) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Aktualisiert am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.updated_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
import MainLayout from '../components/MainLayout.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<MainLayout>
|
||||
<div class="flex-between mb-3">
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
<button @click="showCreateModal = true" class="btn btn-success">
|
||||
|
|
@ -100,14 +98,13 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { userService } from '../services/users'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
import MainLayout from '../components/MainLayout.vue'
|
||||
|
||||
const users = ref([])
|
||||
const error = ref('')
|
||||
|
|
|
|||
Loading…
Reference in New Issue