chore: add profile editor — edit name/department via dashboard

This commit is contained in:
Claus Lohmar 2026-06-17 15:15:23 +00:00
parent 24451b4f4f
commit ce08681d81
4 changed files with 93 additions and 7 deletions

View file

@ -379,6 +379,72 @@ func (h *AuthHandler) SaveOnboarding(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
// ---------------------------------------------------------------------------
// Profile handlers
// ---------------------------------------------------------------------------
// ProfilePage renders the profile editor with the user's current name and
// department pre-filled. Requires onboarding to be completed first.
func (h *AuthHandler) ProfilePage(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
if userID == "" {
w.Header().Set("HX-Redirect", "/")
w.WriteHeader(http.StatusUnauthorized)
return
}
user, _ := database.GetUserByID(h.DB, userID)
if user == nil || !user.Onboarded {
w.Header().Set("HX-Redirect", "/onboarding")
w.WriteHeader(http.StatusOK)
return
}
tmpl := getTemplate("onboarding.html")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.Execute(w, map[string]string{
"Name": user.Name,
"Department": user.Department,
"Editing": "true",
}); err != nil {
log.Printf("ERROR [%s] handlers: ProfilePage: execute template: %v", time.Now().Format(time.RFC3339), err)
}
}
// SaveProfile updates the user's name and department, then redirects to the
// dashboard. Reuses the same DB call as onboarding.
func (h *AuthHandler) SaveProfile(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
if userID == "" {
w.Header().Set("HX-Redirect", "/")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
renderOnboardingError(w, "Cannot parse form data.")
return
}
name := strings.TrimSpace(r.FormValue("name"))
department := strings.TrimSpace(r.FormValue("department"))
if name == "" {
renderOnboardingError(w, "Name is required.")
return
}
if department == "" {
department = "-"
}
if err := database.UpdateUserOnboarding(h.DB, userID, name, department); err != nil {
renderOnboardingError(w, "Failed to save. Please try again.")
return
}
w.Header().Set("HX-Redirect", "/dashboard")
w.WriteHeader(http.StatusOK)
}
func renderOnboardingError(w http.ResponseWriter, message string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<div id="onboarding-error" style="background: #450a0a; border: 1px solid #7f1d1d; color: #fca5a5; padding: 0.75rem; border-radius: 0.5rem; margin-bottom: 1rem;">%s</div>`,

View file

@ -213,6 +213,8 @@ func main() {
r.Get("/dashboard", eventHandler.Dashboard)
r.Get("/onboarding", authHandler.OnboardingPage)
r.Post("/onboarding", authHandler.SaveOnboarding)
r.Get("/profile", authHandler.ProfilePage)
r.Post("/profile", authHandler.SaveProfile)
r.Post("/events", eventHandler.CreateEvent)
r.Put("/events/{id}", eventHandler.UpdateEvent)
r.Get("/events/{id}/edit", eventHandler.EditEvent)

View file

@ -15,8 +15,12 @@
<div class="app-shell">
<header class="app-header">
<h1 class="app-title"><img src="/static/favicon.svg" width="24" height="24" alt="" style="vertical-align: middle; margin-right: 0.5rem;">ReceiptNext</h1>
<button class="btn btn-secondary btn-sm" style="font-size: 0.75rem;"
hx-post="/logout" hx-target="body" hx-push-url="true">Logout</button>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary btn-sm" style="font-size: 0.75rem;"
hx-get="/profile" hx-target="body" hx-push-url="true">Profile</button>
<button class="btn btn-secondary btn-sm" style="font-size: 0.75rem;"
hx-post="/logout" hx-target="body" hx-push-url="true">Logout</button>
</div>
</header>
<main class="main-content">

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#10b981">
<title>Welcome - ReceiptNext</title>
<title>{{if .Editing}}Profile{{else}}Welcome{{end}} - ReceiptNext</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css?v=4">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
@ -12,28 +12,42 @@
<body>
<div class="app-shell">
<header class="app-header">
{{if .Editing}}
<a href="/dashboard" class="btn btn-secondary btn-sm"
hx-get="/dashboard" hx-target="body" hx-push-url="true">&larr; Back</a>
<h1 class="app-title" style="font-size: 1.1rem;">Edit Profile</h1>
<span></span>
{{else}}
<h1 class="app-title" style="font-size: 1.1rem;">Welcome to ReceiptNext</h1>
{{end}}
</header>
<main class="main-content">
{{if .Editing}}
<p style="color: var(--color-text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">
Update your name or department. This appears on all expense reports.
</p>
{{else}}
<p style="color: var(--color-text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">
Let's set up your profile. This information will appear on your expense reports.
</p>
{{end}}
<div id="onboarding-error"></div>
<form hx-post="/onboarding" hx-target="#onboarding-error" hx-swap="innerHTML">
<form hx-post="{{if .Editing}}/profile{{else}}/onboarding{{end}}" hx-target="#onboarding-error" hx-swap="innerHTML">
<div class="form-group">
<label class="form-label" for="name">Full Name</label>
<input type="text" id="name" name="name" placeholder="Your name as it should appear on reports"
required autofocus>
value="{{.Name}}" required autofocus>
</div>
<div class="form-group">
<label class="form-label" for="department">Department / Employee ID</label>
<input type="text" id="department" name="department" placeholder="e.g. Finance, ENG-1234">
<input type="text" id="department" name="department" placeholder="e.g. Finance, ENG-1234"
value="{{.Department}}">
</div>
<button type="submit" class="btn btn-primary btn-block" style="margin-top: 0.5rem;">
Save & Continue
{{if .Editing}}Save Changes{{else}}Save & Continue{{end}}
</button>
</form>
</main>