From e8b16124b586bd5c386e656a97f125b8dc15fc90 Mon Sep 17 00:00:00 2001 From: Mathias Magnusson Date: Wed, 13 Nov 2024 22:20:44 +0100 Subject: [PATCH] add a user overview to the admin page --- database/sql/user.sql | 11 ++ database/user.sql.go | 49 +++++ handlers/admin.go | 25 ++- handlers/routes.go | 3 +- pkg/static/public/style.dist.css | 74 +++++++- service/user.go | 8 + templates/admin.templ | 91 +++++++-- templates/admin_templ.go | 313 ++++++++++++++++++++++++++++--- templates/layout.templ | 5 +- templates/layout_templ.go | 3 +- 10 files changed, 529 insertions(+), 53 deletions(-) diff --git a/database/sql/user.sql b/database/sql/user.sql index 51223a4..55690da 100644 --- a/database/sql/user.sql +++ b/database/sql/user.sql @@ -31,6 +31,17 @@ select * from users where kthid = $1; +-- name: ListUsers :many +select * +from users +where case + when @search::text = '' then true + else kthid = @search or first_name ~* @search or family_name ~* @search +end +order by kthid +limit $1 +offset $2; + -- name: UserSetMemberTo :exec update users set member_to = $2 diff --git a/database/user.sql.go b/database/user.sql.go index 161c100..8e22fd9 100644 --- a/database/user.sql.go +++ b/database/user.sql.go @@ -100,6 +100,55 @@ func (q *Queries) GetUser(ctx context.Context, kthid string) (User, error) { return i, err } +const listUsers = `-- name: ListUsers :many +select kthid, ug_kthid, email, first_name, family_name, year_tag, member_to, webauthn_id, first_name_change_request, family_name_change_request +from users +where case + when $3::text = '' then true + else kthid = $3 or first_name ~* $3 or family_name ~* $3 +end +order by kthid +limit $1 +offset $2 +` + +type ListUsersParams struct { + Limit int32 + Offset int32 + Search string +} + +func (q *Queries) ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error) { + rows, err := q.db.Query(ctx, listUsers, arg.Limit, arg.Offset, arg.Search) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.Kthid, + &i.UgKthid, + &i.Email, + &i.FirstName, + &i.FamilyName, + &i.YearTag, + &i.MemberTo, + &i.WebauthnID, + &i.FirstNameChangeRequest, + &i.FamilyNameChangeRequest, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const removeSession = `-- name: RemoveSession :exec delete from sessions where id = $1 diff --git a/handlers/admin.go b/handlers/admin.go index f44a598..bf9ad3a 100644 --- a/handlers/admin.go +++ b/handlers/admin.go @@ -80,10 +80,33 @@ func admin(s *service.Service, w http.ResponseWriter, r *http.Request) httputil. return templates.AdminPage() } -func members(s *service.Service, w http.ResponseWriter, r *http.Request) httputil.ToResponse { +func membersPage(s *service.Service, w http.ResponseWriter, r *http.Request) httputil.ToResponse { return templates.Members() } +func adminUsersForm(s *service.Service, w http.ResponseWriter, r *http.Request) httputil.ToResponse { + search := r.FormValue("search") + offsetStr := r.FormValue("offset") + offset, err := strconv.ParseInt(offsetStr, 10, 32) + if err != nil && offsetStr != "" { + return httputil.BadRequest("Invalid int for offset") + } + users, err := s.DB.ListUsers(r.Context(), database.ListUsersParams{ + Search: search, + Limit: 21, + Offset: int32(offset), + }) + if err != nil { + return err + } + more := false + if len(users) == 21 { + users = users[0:20:20] + more = true + } + return templates.MemberList(service.DBUsersToModel(users), search, int(offset), more) +} + func invites(s *service.Service, w http.ResponseWriter, r *http.Request) httputil.ToResponse { invs, err := s.DB.ListInvites(r.Context()) if err != nil { diff --git a/handlers/routes.go b/handlers/routes.go index 96556dd..994e271 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -23,7 +23,8 @@ func MountRoutes(s *service.Service) { // admin.go http.Handle("GET /admin", authAdmin(s, httputil.Route(s, admin))) - http.Handle("GET /admin/members", authAdmin(s, httputil.Route(s, members))) + http.Handle("GET /admin/members", authAdmin(s, httputil.Route(s, membersPage))) + http.Handle("GET /admin/users", authAdmin(s, httputil.Route(s, adminUsersForm))) http.Handle("POST /admin/members/upload-sheet", authAdmin(s, httputil.Route(s, uploadSheet))) http.Handle("GET /admin/members/upload-sheet", authAdmin(s, httputil.Route(s, processSheet))) diff --git a/pkg/static/public/style.dist.css b/pkg/static/public/style.dist.css index cb32f50..43d0bb8 100644 --- a/pkg/static/public/style.dist.css +++ b/pkg/static/public/style.dist.css @@ -692,6 +692,22 @@ html { grid-template-columns: subgrid; } +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-cols-\[repeat\(4\2c auto\)\] { + grid-template-columns: repeat(4,auto); +} + +.grid-rows-subgrid { + grid-template-rows: subgrid; +} + .flex-col { flex-direction: column; } @@ -736,6 +752,24 @@ html { gap: 2rem; } +.gap-x-1 { + -moz-column-gap: 0.25rem; + column-gap: 0.25rem; +} + +.gap-y-2 { + row-gap: 0.5rem; +} + +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + +.gap-y-1 { + row-gap: 0.25rem; +} + .overflow-auto { overflow: auto; } @@ -847,6 +881,11 @@ html { padding-right: 0.375rem; } +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + .pb-4 { padding-bottom: 1rem; } @@ -863,10 +902,6 @@ html { text-align: center; } -.align-middle { - vertical-align: middle; -} - .text-lg { font-size: 1.125rem; line-height: 1.75rem; @@ -1049,12 +1084,43 @@ html { border-color: rgb(238 42 123 / var(--tw-border-opacity)); } +.enabled\:hover\:border-cerise-light:hover:enabled { + --tw-border-opacity: 1; + border-color: rgb(236 95 153 / var(--tw-border-opacity)); +} + +.enabled\:focus\:border-cerise-strong:focus:enabled { + --tw-border-opacity: 1; + border-color: rgb(238 42 123 / var(--tw-border-opacity)); +} + +.disabled\:text-gray-500:disabled { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + @media (min-width: 768px) { .md\:max-w-\[calc\(100vw-8rem\)\] { max-width: calc(100vw - 8rem); } } +.\[\&\>\*\]\:h-4>* { + height: 1rem; +} + +.\[\&\>\*\]\:h-6>* { + height: 1.5rem; +} + +.\[\&\>\*\]\:h-8>* { + height: 2rem; +} + +.\[\&\>\*\]\:h-7>* { + height: 1.75rem; +} + .\[\&\>\.error\]\:mt-2>.error { margin-top: 0.5rem; } diff --git a/service/user.go b/service/user.go index a4713db..2d7e770 100644 --- a/service/user.go +++ b/service/user.go @@ -36,6 +36,14 @@ func dbUserToModel(user database.User) models.User { } } +func DBUsersToModel(users []database.User) []models.User { + us := make([]models.User, len(users)) + for i, u := range users { + us[i] = dbUserToModel(u) + } + return us +} + func (s *Service) GetUser(ctx context.Context, kthid string) (*models.User, error) { user, err := s.DB.GetUser(ctx, kthid) if err == pgx.ErrNoRows { diff --git a/templates/admin.templ b/templates/admin.templ index f3a028b..685c686 100644 --- a/templates/admin.templ +++ b/templates/admin.templ @@ -1,6 +1,10 @@ package templates -import "fmt" +import ( + "fmt" + "github.com/datasektionen/logout/models" + "time" +) templ adminNav() { Members @@ -17,34 +21,89 @@ templ AdminPage() { templ Members() { @AdminPage() {
+
+ @MemberList([]models.User{}, "", 0, false) +
@uploadForm() @UploadStatus(false)
} } +templ MemberList(users []models.User, search string, offset int, more bool) { +
+
+ Username + Name + Year + Member until +
+ for _, user := range users { +
+ { user.KTHID } + { user.FirstName } { user.FamilyName } + { user.YearTag } + { user.MemberTo.Format(time.DateOnly) } +
+ } + for i := len(users); i < 20; i++ { +
+ } +
+
+ + + +
+} + templ uploadForm() {
-
- - -
+

Upload THS membership sheet

+ +
} diff --git a/templates/admin_templ.go b/templates/admin_templ.go index 9ed357b..0ba5527 100644 --- a/templates/admin_templ.go +++ b/templates/admin_templ.go @@ -8,7 +8,11 @@ package templates import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "fmt" +import ( + "fmt" + "github.com/datasektionen/logout/models" + "time" +) func adminNav() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { @@ -119,7 +123,15 @@ func Members() templ.Component { }() } ctx = templ.InitializeContext(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = MemberList([]models.User{}, "", 0, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -145,7 +157,7 @@ func Members() templ.Component { }) } -func uploadForm() templ.Component { +func MemberList(users []models.User, search string, offset int, more bool) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -166,12 +178,257 @@ func uploadForm() templ.Component { templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Username Name Year Member until
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 = []any{button + "h-auto"} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + for _, user := range users { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(user.KTHID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 43, Col: 22} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(user.FirstName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 44, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(user.FamilyName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 44, Col: 46} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.YearTag) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 45, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.MemberTo.Format(time.DateOnly)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 46, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for i := len(users); i < 20; i++ { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 = []any{input} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 = []any{button} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 = []any{button} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func uploadForm() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Upload THS membership sheet

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 = []any{button + "h-auto"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -179,16 +436,16 @@ func uploadForm() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String()) + var templ_7745c5c3_Var23 string + templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String()) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">Upload
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">Upload") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -212,9 +469,9 @@ func UploadStatus(withStuff bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) if withStuff { @@ -248,9 +505,9 @@ func UploadProgress(progress float64) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - templ_7745c5c3_Var10 = templ.NopComponent + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("