Compare commits
2 Commits
aa3b63095c
...
812d38c8f1
| Author | SHA1 | Date | |
|---|---|---|---|
| 812d38c8f1 | |||
| 2dfe0a48ea |
@ -15,6 +15,7 @@ func runMigrations(db *sql.DB) error {
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
|
||||
@ -105,6 +106,19 @@ func runMigrations(db *sql.DB) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Manual migration for existing users table
|
||||
var roleExists int
|
||||
db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('users') WHERE name='role'").Scan(&roleExists)
|
||||
if roleExists == 0 {
|
||||
if _, err := db.Exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'"); err != nil {
|
||||
log.Printf("Failed to add role column: %v", err)
|
||||
} else {
|
||||
log.Println("Added role column to users table")
|
||||
// Update existing admin user if exists
|
||||
db.Exec("UPDATE users SET role = 'admin' WHERE username = 'admin'")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Migrations completed")
|
||||
return nil
|
||||
}
|
||||
@ -118,7 +132,7 @@ func seedData(db *sql.DB) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("hashing password: %w", err)
|
||||
}
|
||||
_, err = db.Exec("INSERT INTO users (username, password_hash) VALUES (?, ?)", "admin", string(hash))
|
||||
_, err = db.Exec("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)", "admin", string(hash), "admin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting admin user: %w", err)
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ func (h *Handler) LoginSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := h.Store.Get(r, "erp-session")
|
||||
session.Values["user_id"] = user.ID
|
||||
session.Values["username"] = user.Username
|
||||
session.Values["role"] = user.Role
|
||||
session.Save(r, w)
|
||||
|
||||
// HTMX redirect
|
||||
@ -52,6 +53,7 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := h.Store.Get(r, "erp-session")
|
||||
session.Values["user_id"] = nil
|
||||
session.Values["username"] = nil
|
||||
session.Values["role"] = nil
|
||||
session.Options.MaxAge = -1
|
||||
session.Save(r, w)
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -39,11 +40,18 @@ func (h *Handler) CustomerList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// HTMX partial for search
|
||||
if r.Header.Get("HX-Request") == "true" && r.URL.Query().Get("partial") == "true" {
|
||||
// Construct clean URL for history
|
||||
u := fmt.Sprintf("/customers?page=%d", page)
|
||||
if search != "" {
|
||||
u += "&search=" + search
|
||||
}
|
||||
w.Header().Set("HX-Push-Url", u)
|
||||
|
||||
h.renderPartial(w, "customers/list.html", "customer-table", data)
|
||||
return
|
||||
}
|
||||
|
||||
h.render(w, []string{"layout.html", "customers/list.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/list.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) CustomerNew(w http.ResponseWriter, r *http.Request) {
|
||||
@ -54,7 +62,7 @@ func (h *Handler) CustomerNew(w http.ResponseWriter, r *http.Request) {
|
||||
"Customer": &models.Customer{},
|
||||
"IsNew": true,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "customers/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) CustomerCreate(w http.ResponseWriter, r *http.Request) {
|
||||
@ -74,7 +82,7 @@ func (h *Handler) CustomerCreate(w http.ResponseWriter, r *http.Request) {
|
||||
"IsNew": true,
|
||||
"Error": "Name is required",
|
||||
}
|
||||
h.render(w, []string{"layout.html", "customers/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
@ -118,7 +126,7 @@ func (h *Handler) CustomerDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"Orders": customerOrders,
|
||||
"Invoices": invoices,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "customers/detail.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/detail.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) CustomerEdit(w http.ResponseWriter, r *http.Request) {
|
||||
@ -136,7 +144,7 @@ func (h *Handler) CustomerEdit(w http.ResponseWriter, r *http.Request) {
|
||||
"Customer": customer,
|
||||
"IsNew": false,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "customers/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) CustomerUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
@ -158,7 +166,7 @@ func (h *Handler) CustomerUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
"IsNew": false,
|
||||
"Error": "Name is required",
|
||||
}
|
||||
h.render(w, []string{"layout.html", "customers/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "customers/form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -19,5 +19,5 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
"FulfilledOrders": models.OrderCountByStatus(h.DB, "fulfilled"),
|
||||
}
|
||||
|
||||
h.render(w, []string{"layout.html", "dashboard.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "dashboard.html"}, data)
|
||||
}
|
||||
|
||||
@ -21,12 +21,24 @@ var funcMap = template.FuncMap{
|
||||
"formatMoney": func(amount float64) string {
|
||||
return fmt.Sprintf("$%.2f", amount)
|
||||
},
|
||||
"formatDate": func(date string) string {
|
||||
t, err := time.Parse("2006-01-02", date)
|
||||
if err != nil {
|
||||
return date
|
||||
"formatDate": func(v interface{}) string {
|
||||
if t, ok := v.(time.Time); ok {
|
||||
return t.Format("Jan 02, 2006")
|
||||
}
|
||||
return t.Format("Jan 02, 2006")
|
||||
if date, ok := v.(string); ok {
|
||||
t, err := time.Parse("2006-01-02", date)
|
||||
if err != nil {
|
||||
return date
|
||||
}
|
||||
return t.Format("Jan 02, 2006")
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"formatDateTime": func(v interface{}) string {
|
||||
if t, ok := v.(time.Time); ok {
|
||||
return t.Format("Jan 02, 2006 15:04")
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"statusBadge": func(status string) template.HTML {
|
||||
colors := map[string]string{
|
||||
@ -69,7 +81,12 @@ var funcMap = template.FuncMap{
|
||||
},
|
||||
}
|
||||
|
||||
func (h *Handler) render(w http.ResponseWriter, templates []string, data interface{}) {
|
||||
func (h *Handler) render(w http.ResponseWriter, r *http.Request, templates []string, data interface{}) {
|
||||
if d, ok := data.(map[string]interface{}); ok {
|
||||
d["Username"] = h.getUsername(r)
|
||||
d["IsAdmin"] = h.getRole(r) == "admin"
|
||||
}
|
||||
|
||||
paths := make([]string, len(templates))
|
||||
for i, t := range templates {
|
||||
paths[i] = filepath.Join("templates", t)
|
||||
@ -106,3 +123,11 @@ func (h *Handler) getUsername(r *http.Request) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *Handler) getRole(r *http.Request) string {
|
||||
session, _ := h.Store.Get(r, "erp-session")
|
||||
if role, ok := session.Values["role"].(string); ok {
|
||||
return role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ func (h *Handler) InvoiceList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.render(w, []string{"layout.html", "invoices/list.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "invoices/list.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) InvoiceDetail(w http.ResponseWriter, r *http.Request) {
|
||||
@ -60,7 +60,7 @@ func (h *Handler) InvoiceDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"ActivePage": "invoices",
|
||||
"Invoice": invoice,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "invoices/detail.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "invoices/detail.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) InvoicePay(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -21,7 +21,7 @@ func (h *Handler) ChartOfAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
"ActivePage": "ledger",
|
||||
"Accounts": accounts,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/chart_of_accounts.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/chart_of_accounts.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) JournalEntries(w http.ResponseWriter, r *http.Request) {
|
||||
@ -51,7 +51,7 @@ func (h *Handler) JournalEntries(w http.ResponseWriter, r *http.Request) {
|
||||
"PrevPage": page - 1,
|
||||
"NextPage": page + 1,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/journal_entries.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/journal_entries.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) JournalEntryNew(w http.ResponseWriter, r *http.Request) {
|
||||
@ -64,7 +64,7 @@ func (h *Handler) JournalEntryNew(w http.ResponseWriter, r *http.Request) {
|
||||
"Accounts": accounts,
|
||||
"Today": time.Now().Format("2006-01-02"),
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) JournalEntryCreate(w http.ResponseWriter, r *http.Request) {
|
||||
@ -107,7 +107,7 @@ func (h *Handler) JournalEntryCreate(w http.ResponseWriter, r *http.Request) {
|
||||
"Today": time.Now().Format("2006-01-02"),
|
||||
"Error": "At least two lines are required",
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ func (h *Handler) JournalEntryCreate(w http.ResponseWriter, r *http.Request) {
|
||||
"Today": time.Now().Format("2006-01-02"),
|
||||
"Error": err.Error(),
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/journal_entry_form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
@ -146,7 +146,7 @@ func (h *Handler) JournalEntryDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"ActivePage": "ledger",
|
||||
"Entry": entry,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/journal_entry_detail.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/journal_entry_detail.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) TrialBalance(w http.ResponseWriter, r *http.Request) {
|
||||
@ -170,5 +170,5 @@ func (h *Handler) TrialBalance(w http.ResponseWriter, r *http.Request) {
|
||||
"TotalDebit": totalDebit,
|
||||
"TotalCredit": totalCredit,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "ledger/trial_balance.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "ledger/trial_balance.html"}, data)
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ func (h *Handler) OrderList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.render(w, []string{"layout.html", "orders/list.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "orders/list.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) OrderNew(w http.ResponseWriter, r *http.Request) {
|
||||
@ -58,7 +58,7 @@ func (h *Handler) OrderNew(w http.ResponseWriter, r *http.Request) {
|
||||
"Customers": customers,
|
||||
"IsNew": true,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "orders/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "orders/form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) OrderCreate(w http.ResponseWriter, r *http.Request) {
|
||||
@ -81,7 +81,7 @@ func (h *Handler) OrderCreate(w http.ResponseWriter, r *http.Request) {
|
||||
"IsNew": true,
|
||||
"Error": "Customer is required",
|
||||
}
|
||||
h.render(w, []string{"layout.html", "orders/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "orders/form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ func (h *Handler) OrderDetail(w http.ResponseWriter, r *http.Request) {
|
||||
"ActivePage": "orders",
|
||||
"Order": order,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "orders/detail.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "orders/detail.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) OrderEdit(w http.ResponseWriter, r *http.Request) {
|
||||
@ -141,7 +141,7 @@ func (h *Handler) OrderEdit(w http.ResponseWriter, r *http.Request) {
|
||||
"Customers": customers,
|
||||
"IsNew": false,
|
||||
}
|
||||
h.render(w, []string{"layout.html", "orders/form.html"}, data)
|
||||
h.render(w, r, []string{"layout.html", "orders/form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) OrderUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
90
internal/handlers/users.go
Normal file
90
internal/handlers/users.go
Normal file
@ -0,0 +1,90 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"erp_system/internal/models"
|
||||
)
|
||||
|
||||
func (h *Handler) UserList(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := models.UserGetAll(h.DB)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Title": "Users",
|
||||
"ActivePage": "users",
|
||||
"Users": users,
|
||||
}
|
||||
h.render(w, r, []string{"layout.html", "users/list.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) UserNew(w http.ResponseWriter, r *http.Request) {
|
||||
data := map[string]interface{}{
|
||||
"Title": "New User",
|
||||
"ActivePage": "users",
|
||||
"User": &models.User{},
|
||||
"IsNew": true,
|
||||
}
|
||||
h.render(w, r, []string{"layout.html", "users/form.html"}, data)
|
||||
}
|
||||
|
||||
func (h *Handler) UserCreate(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
role := r.FormValue("role")
|
||||
|
||||
if username == "" || password == "" {
|
||||
data := map[string]interface{}{
|
||||
"Title": "New User",
|
||||
"ActivePage": "users",
|
||||
"User": &models.User{Username: username, Role: role},
|
||||
"IsNew": true,
|
||||
"Error": "Username and Password are required",
|
||||
}
|
||||
h.render(w, r, []string{"layout.html", "users/form.html"}, data)
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.UserCreate(h.DB, username, password, role); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("HX-Redirect", "/users")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) UserDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.Atoi(r.PathValue("id"))
|
||||
|
||||
// Prevent deleting yourself
|
||||
session, _ := h.Store.Get(r, "erp-session")
|
||||
currentUserID := session.Values["user_id"].(int)
|
||||
if id == currentUserID {
|
||||
w.Header().Set("HX-Trigger", `{"showMessage": "Cannot delete yourself"}`)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Cannot delete yourself")
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.UserDelete(h.DB, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
// Just remove the row from the table or reload
|
||||
// Since it's a delete action, usually redirect or reload list
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/users", http.StatusSeeOther)
|
||||
}
|
||||
@ -25,3 +25,17 @@ func RequireAuth(store *sessions.CookieStore) func(http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAdmin(store *sessions.CookieStore) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := store.Get(r, "erp-session")
|
||||
role, ok := session.Values["role"].(string)
|
||||
if !ok || role != "admin" {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,15 +12,16 @@ type User struct {
|
||||
ID int
|
||||
Username string
|
||||
PasswordHash string
|
||||
Role string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func Authenticate(db *sql.DB, username, password string) (*User, error) {
|
||||
u := &User{}
|
||||
err := db.QueryRow(
|
||||
"SELECT id, username, password_hash, created_at FROM users WHERE username = ?",
|
||||
"SELECT id, username, password_hash, role, created_at FROM users WHERE username = ?",
|
||||
username,
|
||||
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.CreatedAt)
|
||||
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.Role, &u.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
@ -31,3 +32,35 @@ func Authenticate(db *sql.DB, username, password string) (*User, error) {
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func UserGetAll(db *sql.DB) ([]User, error) {
|
||||
rows, err := db.Query("SELECT id, username, role, created_at FROM users ORDER BY username")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []User
|
||||
for rows.Next() {
|
||||
var u User
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Role, &u.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func UserCreate(db *sql.DB, username, password, role string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)", username, string(hash), role)
|
||||
return err
|
||||
}
|
||||
|
||||
func UserDelete(db *sql.DB, id int) error {
|
||||
_, err := db.Exec("DELETE FROM users WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
7
main.go
7
main.go
@ -40,6 +40,7 @@ func main() {
|
||||
|
||||
// Auth middleware
|
||||
authMw := middleware.RequireAuth(store)
|
||||
adminMw := middleware.RequireAdmin(store)
|
||||
|
||||
// Router
|
||||
mux := http.NewServeMux()
|
||||
@ -94,6 +95,12 @@ func main() {
|
||||
mux.Handle("GET /ledger/journal/{id}", authMw(http.HandlerFunc(h.JournalEntryDetail)))
|
||||
mux.Handle("GET /ledger/trial-balance", authMw(http.HandlerFunc(h.TrialBalance)))
|
||||
|
||||
// User Management (Admin only)
|
||||
mux.Handle("GET /users", authMw(adminMw(http.HandlerFunc(h.UserList))))
|
||||
mux.Handle("GET /users/new", authMw(adminMw(http.HandlerFunc(h.UserNew))))
|
||||
mux.Handle("POST /users", authMw(adminMw(http.HandlerFunc(h.UserCreate))))
|
||||
mux.Handle("DELETE /users/{id}", authMw(adminMw(http.HandlerFunc(h.UserDelete))))
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "1337"
|
||||
|
||||
@ -72,7 +72,8 @@
|
||||
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
{{if .HasPrev}}
|
||||
<button
|
||||
hx-get="/customers?page={{.PrevPage}}&search={{.Search}}&partial=true"
|
||||
hx-get="/customers?page={{.PrevPage}}{{if .Search}}&search={{.Search}}{{end}}&partial=true"
|
||||
hx-push-url="/customers?page={{.PrevPage}}{{if .Search}}&search={{.Search}}{{end}}"
|
||||
hx-target="#customer-table"
|
||||
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Previous</span>
|
||||
@ -83,7 +84,8 @@
|
||||
{{end}}
|
||||
{{if .HasNext}}
|
||||
<button
|
||||
hx-get="/customers?page={{.NextPage}}&search={{.Search}}&partial=true"
|
||||
hx-get="/customers?page={{.NextPage}}{{if .Search}}&search={{.Search}}{{end}}&partial=true"
|
||||
hx-push-url="/customers?page={{.NextPage}}{{if .Search}}&search={{.Search}}{{end}}"
|
||||
hx-target="#customer-table"
|
||||
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Next</span>
|
||||
|
||||
@ -76,7 +76,8 @@
|
||||
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
{{if .HasPrev}}
|
||||
<button
|
||||
hx-get="/invoices?page={{.PrevPage}}&status={{.FilterStatus}}&partial=true"
|
||||
hx-get="/invoices?page={{.PrevPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}&partial=true"
|
||||
hx-push-url="/invoices?page={{.PrevPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}"
|
||||
hx-target="#invoice-table"
|
||||
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Previous</span>
|
||||
@ -87,7 +88,8 @@
|
||||
{{end}}
|
||||
{{if .HasNext}}
|
||||
<button
|
||||
hx-get="/invoices?page={{.NextPage}}&status={{.FilterStatus}}&partial=true"
|
||||
hx-get="/invoices?page={{.NextPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}&partial=true"
|
||||
hx-push-url="/invoices?page={{.NextPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}"
|
||||
hx-target="#invoice-table"
|
||||
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Next</span>
|
||||
|
||||
@ -28,6 +28,9 @@
|
||||
<a href="/customers" class="{{if eq .ActivePage "customers"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Customers</a>
|
||||
<a href="/orders" class="{{if eq .ActivePage "orders"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Orders</a>
|
||||
<a href="/invoices" class="{{if eq .ActivePage "invoices"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Invoices</a>
|
||||
{{if .IsAdmin}}
|
||||
<a href="/users" class="{{if eq .ActivePage "users"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium">Users</a>
|
||||
{{end}}
|
||||
<div class="relative">
|
||||
<button type="button" onclick="toggleDropdown(event, 'ledger-dropdown')" class="{{if eq .ActivePage "ledger"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} rounded-md px-3 py-2 text-sm font-medium inline-flex items-center">
|
||||
General Ledger
|
||||
@ -66,6 +69,9 @@
|
||||
<a href="/customers" class="{{if eq .ActivePage "customers"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Customers</a>
|
||||
<a href="/orders" class="{{if eq .ActivePage "orders"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Orders</a>
|
||||
<a href="/invoices" class="{{if eq .ActivePage "invoices"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Invoices</a>
|
||||
{{if .IsAdmin}}
|
||||
<a href="/users" class="{{if eq .ActivePage "users"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} block rounded-md px-3 py-2 text-base font-medium">Users</a>
|
||||
{{end}}
|
||||
<div>
|
||||
<button type="button" onclick="toggleDropdown(event, 'ledger-mobile-dropdown')" class="{{if eq .ActivePage "ledger"}}bg-indigo-700 text-white{{else}}text-indigo-200 hover:bg-indigo-500 hover:text-white{{end}} w-full text-left block rounded-md px-3 py-2 text-base font-medium flex justify-between items-center">
|
||||
General Ledger
|
||||
|
||||
@ -78,7 +78,8 @@
|
||||
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||
{{if .HasPrev}}
|
||||
<button
|
||||
hx-get="/orders?page={{.PrevPage}}&status={{.FilterStatus}}&partial=true"
|
||||
hx-get="/orders?page={{.PrevPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}&partial=true"
|
||||
hx-push-url="/orders?page={{.PrevPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}"
|
||||
hx-target="#order-table"
|
||||
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Previous</span>
|
||||
@ -89,7 +90,8 @@
|
||||
{{end}}
|
||||
{{if .HasNext}}
|
||||
<button
|
||||
hx-get="/orders?page={{.NextPage}}&status={{.FilterStatus}}&partial=true"
|
||||
hx-get="/orders?page={{.NextPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}&partial=true"
|
||||
hx-push-url="/orders?page={{.NextPage}}{{if .FilterStatus}}&status={{.FilterStatus}}{{end}}"
|
||||
hx-target="#order-table"
|
||||
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0">
|
||||
<span class="sr-only">Next</span>
|
||||
|
||||
54
templates/users/form.html
Normal file
54
templates/users/form.html
Normal file
@ -0,0 +1,54 @@
|
||||
{{define "content"}}
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{{if .IsNew}}New User{{else}}Edit User{{end}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden bg-white shadow sm:rounded-lg p-6 max-w-lg">
|
||||
<form method="POST" action="/users{{if not .IsNew}}/{{.User.ID}}{{end}}">
|
||||
{{if .Error}}
|
||||
<div class="rounded-md bg-red-50 p-4 mb-6">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">{{.Error}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium leading-6 text-gray-900">Username</label>
|
||||
<div class="mt-2">
|
||||
<input type="text" name="username" id="username" value="{{.User.Username}}" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium leading-6 text-gray-900">Password</label>
|
||||
<div class="mt-2">
|
||||
<input type="password" name="password" id="password" required class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium leading-6 text-gray-900">Role</label>
|
||||
<div class="mt-2">
|
||||
<select id="role" name="role" class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6">
|
||||
<option value="user" {{if eq .User.Role "user"}}selected{{end}}>User</option>
|
||||
<option value="admin" {{if eq .User.Role "admin"}}selected{{end}}>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex items-center justify-end gap-x-6">
|
||||
<a href="/users" class="text-sm font-semibold leading-6 text-gray-900">Cancel</a>
|
||||
<button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
37
templates/users/list.html
Normal file
37
templates/users/list.html
Normal file
@ -0,0 +1,37 @@
|
||||
{{define "content"}}
|
||||
<div class="md:flex md:items-center md:justify-between mb-8">
|
||||
<div class="min-w-0 flex-1">
|
||||
<h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">Users</h2>
|
||||
</div>
|
||||
<div class="mt-4 flex md:ml-4 md:mt-0">
|
||||
<a href="/users/new" class="ml-3 inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">New User</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden bg-white shadow sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Username</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Created At</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{{.Username}}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{.Role}}</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{{.CreatedAt | formatDateTime}}</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||
<button hx-delete="/users/{{.ID}}" hx-confirm="Are you sure you want to delete this user?" class="text-red-600 hover:text-red-900">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
Loading…
x
Reference in New Issue
Block a user