chore: add profile editor — edit name/department via dashboard
This commit is contained in:
parent
24451b4f4f
commit
ce08681d81
4 changed files with 93 additions and 7 deletions
|
|
@ -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>`,
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">← 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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue