diff --git a/apps/go-crm/internal/handlers/auth.go b/apps/go-crm/internal/handlers/auth.go index 7bf88c2..4514d51 100644 --- a/apps/go-crm/internal/handlers/auth.go +++ b/apps/go-crm/internal/handlers/auth.go @@ -2,9 +2,12 @@ package handlers import ( "database/sql" + "fmt" "net/http" "time" + "go-crm/internal/templates" + "github.com/go-chi/chi/v5" "golang.org/x/crypto/bcrypt" ) @@ -13,24 +16,38 @@ import ( var DB *sql.DB func (a *App) SignupPage(w http.ResponseWriter, r *http.Request) { + content := fmt.Sprintf(` +
+
+
+
+ +
+

go-crm

+

Create your account

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+

Already have an account? Sign in

+
+
`) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - - Sign Up - - -

Sign Up

-
- - - - -
-

Already have an account? Login

- -`)) + templates.WriteHTMLPage(w, "Sign Up", "", content) } func (a *App) Signup(w http.ResponseWriter, r *http.Request) { @@ -50,7 +67,6 @@ func (a *App) Signup(w http.ResponseWriter, r *http.Request) { return } - // Use a transaction so account + client + session are atomic. tx, err := a.DB.Begin() if err != nil { http.Error(w, "Failed to start transaction", http.StatusInternalServerError) @@ -101,23 +117,34 @@ func (a *App) Signup(w http.ResponseWriter, r *http.Request) { } func (a *App) LoginPage(w http.ResponseWriter, r *http.Request) { + content := fmt.Sprintf(` +
+
+
+
+ +
+

go-crm

+

Sign in to your workspace

+
+
+
+
+ + +
+
+ + +
+ +
+
+

Don't have an account? Create one

+
+
`) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - - Login - - -

Login

-
- - - -
-

Don't have an account? Sign Up

- -`)) + templates.WriteHTMLPage(w, "Sign In", "", content) } func (a *App) Login(w http.ResponseWriter, r *http.Request) { @@ -173,14 +200,32 @@ func (a *App) AccountPage(w http.ResponseWriter, r *http.Request) { return } + content := fmt.Sprintf(` +%s +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
`, templates.PageHeader("Account Settings", "Manage your profile and security"), htmlEscape(name), htmlEscape(email)) + w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`Account

Account Settings

-

Name:

-

Email:

-

New Password:

- -
- Back to Dashboard`)) + templates.WriteHTMLPage(w, "Account", "", content) } func (a *App) UpdateAccount(w http.ResponseWriter, r *http.Request) { diff --git a/apps/go-crm/internal/handlers/clients.go b/apps/go-crm/internal/handlers/clients.go index 8a7b501..88dcbeb 100644 --- a/apps/go-crm/internal/handlers/clients.go +++ b/apps/go-crm/internal/handlers/clients.go @@ -1,11 +1,13 @@ package handlers import ( + "fmt" "net/http" "strconv" "time" "go-crm/internal/db" + "go-crm/internal/templates" "github.com/go-chi/chi/v5" ) @@ -28,75 +30,94 @@ func (a *App) ListClients(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - Clients - - - - -

Clients

- - ` return out } -// --- package-level shims kept for existing tests --- +// --- package-level shims --- func LeadKeywordsPage(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).LeadKeywordsPage(w, r) diff --git a/apps/go-crm/internal/handlers/lead_review.go b/apps/go-crm/internal/handlers/lead_review.go index 2c56487..6a8ed87 100644 --- a/apps/go-crm/internal/handlers/lead_review.go +++ b/apps/go-crm/internal/handlers/lead_review.go @@ -8,12 +8,11 @@ import ( "time" "go-crm/internal/db" + "go-crm/internal/templates" "github.com/go-chi/chi/v5" ) -// LeadReviewQueue renders the review queue: leads where needs_review = 1. -// GET /leads/review func (a *App) LeadReviewQueue(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { @@ -40,72 +39,39 @@ func (a *App) LeadReviewQueue(w http.ResponseWriter, r *http.Request) { statuses, _ := db.ListLeadStatuses(a.DB, clientID) services := a.serviceNames(clientID) - pendingCount := len(leads) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprintf(w, ` - - - Review Queue (%d pending) - - - - -

Review Queue %d

-

Leads with service interest not yet identified. Confirm or correct each entry.

- -
-`, pendingCount, pendingCount) + buf := templates.BufRender() + badge := "" + if pendingCount > 0 { + badge = fmt.Sprintf(`%d`, pendingCount) + } + fmt.Fprint(buf, templates.PageHeader("Review Queue", "Confirm or correct leads with unidentified service interest")) + fmt.Fprintf(buf, `
%s
`, badge) if len(leads) == 0 { - fmt.Fprintf(w, `
No leads pending review.
`) + fmt.Fprint(buf, templates.EmptyState(``, "All caught up! No leads pending review.")) } else { - fmt.Fprintf(w, ` - - - - - - - - - - - `) - + fmt.Fprintf(buf, `
`) + fmt.Fprint(buf, templates.TableStart([]string{"Phone", "Name", "Service Interest", "Status", "Arrived", "Actions"})) for _, l := range leads { arrived := time.Unix(l.CreatedAt, 0).Format("02/01 15:04") - fmt.Fprintf(w, ` -
- - + fmt.Fprintf(buf, ` + + - - + + `, @@ -121,14 +87,14 @@ func (a *App) LeadReviewQueue(w http.ResponseWriter, r *http.Request) { l.LeadID, l.LeadID, ) } - fmt.Fprintf(w, `
PhoneNameService InterestStatusArrivedActions
%s%s
%s%s -
- - - -
- -
+ + + + +
%s%s%s%s -
- + +
`) + fmt.Fprint(buf, templates.TableEnd()) + fmt.Fprintf(buf, ``) } - fmt.Fprintf(w, ``) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "Review Queue", "review") } -// ConfirmLeadReview handles the form submission from the review queue. -// PUT /leads/{id}/review func (a *App) ConfirmLeadReview(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { @@ -161,11 +127,9 @@ func (a *App) ConfirmLeadReview(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprintf(w, ``, leadID) + fmt.Fprintf(w, ``, leadID) } -// LeadAllList renders all leads (not just review queue). -// GET /leads/all func (a *App) LeadAllList(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { @@ -192,7 +156,6 @@ func (a *App) LeadAllList(w http.ResponseWriter, r *http.Request) { http.Error(w, svcErr.Error(), http.StatusInternalServerError) return } - // convert domain.Leaf to db.Lead leads = make([]db.Lead, len(domainLeads)) for i, dl := range domainLeads { leads[i] = db.Lead{ @@ -225,98 +188,83 @@ func (a *App) LeadAllList(w http.ResponseWriter, r *http.Request) { statuses, _ := db.ListLeadStatuses(a.DB, clientID) services := a.serviceNames(clientID) - w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprintf(w, ` - - - All Leads - - - - -

All Leads

- -
- - - - - - - - -`, pendingCount) + buf := templates.BufRender() + fmt.Fprint(buf, templates.PageHeader("All Leads", "Complete lead database with filters and inline editing")) - for _, l := range leads { - arrived := time.Unix(l.CreatedAt, 0).Format("02/01/2006 15:04") - lastContact := time.Unix(l.LastContactAt, 0).Format("02/01/2006 15:04") - rowClass := "" - if l.NeedsReview { - rowClass = `class="needs-review"` - } - fmt.Fprintf(w, ` - - - - - - - - - - - - - `, - rowClass, l.LeadID, - htmlEscape(l.PhoneNormalized), - htmlEscape(l.Name), - htmlEscape(l.ServiceInterest), - htmlEscape(l.Status), - htmlEscape(l.PaymentStatus), - arrived, - lastContact, - l.LeadID, - l.LeadID, l.LeadID, - l.LeadID, - l.LeadID, l.LeadID, - htmlEscape(l.Name), - buildServiceOptions(services, l.ServiceInterest), - buildStatusOptions(statuses, l.Status), - l.LeadID, - ) + if pendingCount > 0 { + fmt.Fprintf(buf, `
%d pending review
`, pendingCount) } - fmt.Fprintf(w, `
PhoneNameServiceStatusPaymentArrivedLast ContactActions
%s%s%s%s%s%s%s - -
- -
-
`) + if len(leads) == 0 { + fmt.Fprint(buf, templates.EmptyState(``, "No leads captured yet. Connect WhatsApp to start.")) + } else { + fmt.Fprintf(buf, `
`) + fmt.Fprint(buf, templates.TableStart([]string{"Phone", "Name", "Service", "Status", "Payment", "Arrived", "Last Contact", "Actions"})) + for _, l := range leads { + arrived := time.Unix(l.CreatedAt, 0).Format("02/01 15:04") + lastContact := time.Unix(l.LastContactAt, 0).Format("02/01 15:04") + rowClass := "" + if l.NeedsReview { + rowClass = `class="bg-amber-400/[0.03]"` + } + statusPill := statusPillHTML(l.Status) + fmt.Fprintf(buf, ` + %s + %s %s + %s + %s + %s + %s + %s + +
+ +
+ +
+
+ + `, + rowClass, l.LeadID, + htmlEscape(l.PhoneNormalized), + htmlEscape(l.Name), + map[bool]string{true: `review`, false: ""}[l.NeedsReview], + htmlEscape(l.ServiceInterest), + statusPill, + htmlEscape(l.PaymentStatus), + arrived, + lastContact, + l.LeadID, + l.LeadID, l.LeadID, + ) + } + fmt.Fprint(buf, templates.TableEnd()) + fmt.Fprintf(buf, `
`) + + // Edit rows + for _, l := range leads { + fmt.Fprintf(buf, ` +`, l.LeadID, l.LeadID, l.LeadID, l.LeadID, htmlEscape(l.Name), buildServiceOptions(services, l.ServiceInterest), buildStatusOptions(statuses, l.Status), l.LeadID) + } + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "All Leads", "leads") } -// DeleteLeadNew handles DELETE /leads/{id} func (a *App) DeleteLeadNew(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { @@ -330,8 +278,6 @@ func (a *App) DeleteLeadNew(w http.ResponseWriter, r *http.Request) { } // serviceNames returns the list of service names for the review/all-leads dropdowns. -// It reads from the DB-backed services table first, then falls back to the -// hardcoded defaults so the dropdown is never empty on a fresh install. func (a *App) serviceNames(clientID int64) []string { rows, err := a.DB.Query( "SELECT DISTINCT name FROM services WHERE client_id = ? ORDER BY name", @@ -347,7 +293,6 @@ func (a *App) serviceNames(clientID int64) []string { } } if len(names) > 0 { - // Ensure "Não especificou" is always first. hasDefault := false for _, n := range names { if n == "Não especificou" { @@ -361,7 +306,6 @@ func (a *App) serviceNames(clientID int64) []string { return names } } - // Fallback: hardcoded defaults for a fresh install with no services yet. return []string{ "Não especificou", "Head Spa", @@ -373,8 +317,6 @@ func (a *App) serviceNames(clientID int64) []string { } } -// --- rendering helpers ------------------------------------------------------- - func buildServiceOptions(services []string, selected string) string { out := "" for _, s := range services { diff --git a/apps/go-crm/internal/handlers/leads.go b/apps/go-crm/internal/handlers/leads.go index 87023f0..716ce71 100644 --- a/apps/go-crm/internal/handlers/leads.go +++ b/apps/go-crm/internal/handlers/leads.go @@ -3,6 +3,7 @@ package handlers import ( "context" "encoding/json" + "fmt" "log" "net/http" "strconv" @@ -10,6 +11,7 @@ import ( "time" "go-crm/internal/db" + "go-crm/internal/templates" "go-crm/internal/whatsapp" "github.com/go-chi/chi/v5" @@ -32,8 +34,8 @@ func (a *App) ListLeads(w http.ResponseWriter, r *http.Request) { args := []interface{}{accountID} if search != "" { - query += " AND (name LIKE ? OR phone LIKE ?)" - searchPat := "%" + search + "%" + query += " AND (cu.name LIKE ? OR cu.phone LIKE ?)" + searchPat := "%%" + search + "%%" args = append(args, searchPat, searchPat) } @@ -56,72 +58,50 @@ func (a *App) ListLeads(w http.ResponseWriter, r *http.Request) { customers = append(customers, c) } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - Leads - - - - -

Leads

- - - - - - -`)) + buf := templates.BufRender() + fmt.Fprint(buf, templates.PageHeader("Leads", "Search and manage customer leads")) - for _, c := range customers { - waStatus := "Not connected" - waStyle := "color: #999;" - if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" { - waStatus = c.WhatsAppNumber - waStyle = "color: #28a745; font-weight: 600;" + // Search + fmt.Fprintf(buf, ` +
+
+ + + +
`, htmlEscape(search)) + + if len(customers) == 0 { + fmt.Fprint(buf, templates.EmptyState(``, "No leads found. Try a different search or connect WhatsApp to capture leads.")) + } else { + fmt.Fprintf(buf, `
`) + fmt.Fprint(buf, templates.TableStart([]string{"Name", "Phone", "Birth Date", "Instagram", "WhatsApp", "Actions"})) + for _, c := range customers { + waStatus := `` + if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" { + waStatus = fmt.Sprintf(`%s`, htmlEscape(c.WhatsAppNumber)) + } + fmt.Fprintf(buf, `
+ + + + + + + `, htmlEscape(c.Name), htmlEscape(c.Phone), htmlEscape(c.BirthDate), htmlEscape(c.Instagram), waStatus, c.CustomerID, c.CustomerID) } - w.Write([]byte(` - - - - - - - - - - `)) + fmt.Fprint(buf, templates.TableEnd()) + fmt.Fprintf(buf, ``) } - w.Write([]byte(`
NamePhoneBirth DateInstagramWhatsAppActions
%s%s%s%s%s +
+ +
+ +
+
+
` + c.Name + `` + c.Phone + `` + c.BirthDate + `` + c.Instagram + `` + waStatus + ` - -
- -
-
-
- - - - - -
-
-

Back to Home | Back to Clients

-`)) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "Leads", "leads") } func (a *App) UpdateLead(w http.ResponseWriter, r *http.Request) { @@ -163,7 +143,7 @@ func (a *App) DeleteLead(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte("OK")) + w.Write([]byte("")) } func (a *App) LeadsConnectPage(w http.ResponseWriter, r *http.Request) { @@ -179,81 +159,71 @@ func (a *App) LeadsConnectPage(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - Connect WhatsApp - - - - - -

Connect WhatsApp

-

Scan the QR code below with your WhatsApp app to connect

-
-

Loading...

- + -

Back to Home | Back to Clients

-`)) + }) + .catch(() => { + document.getElementById('status').textContent = 'Connected! (could not verify phone)'; + }); + }, 1500); + } else if (data.status === 'error') { + document.getElementById('status').textContent = 'Error: ' + (data.error || 'Unknown') + ' retrying...'; + setTimeout(pollQR, 8000); + } else { + document.getElementById('status').textContent = 'Status: ' + data.status; + setTimeout(pollQR, 5000); + } + }) + .catch(err => { + document.getElementById('status').textContent = 'Connection error retrying...'; + setTimeout(pollQR, 5000); + }); +} +pollQR(); +`, client.ClientID, client.ClientID) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "Connect WhatsApp", "leads") } func jsonEscape(s string) string { @@ -349,8 +319,8 @@ func (a *App) VerifyLead(w http.ResponseWriter, r *http.Request) { match := "unknown" if client.WhatsAppNumber != "" && client.Phone != "" { - cleanWA := strings.TrimPrefix(client.WhatsAppNumber, "+") - cleanClient := strings.TrimPrefix(client.Phone, "+") + cleanWA := cleanPhone(client.WhatsAppNumber) + cleanClient := cleanPhone(client.Phone) if cleanWA == cleanClient { match = "yes" } else { @@ -362,6 +332,12 @@ func (a *App) VerifyLead(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"status":"ok","wa_phone":"` + jsonEscape(client.WhatsAppNumber) + `","client_phone":"` + jsonEscape(client.Phone) + `","match":"` + match + `","client_name":"` + jsonEscape(client.Name) + `"}`)) } +func cleanPhone(s string) string { + s = strings.TrimPrefix(s, "+") + s = strings.TrimPrefix(s, "55") + return s +} + // --- package-level shims kept for existing tests --- func ListLeads(w http.ResponseWriter, r *http.Request) { diff --git a/apps/go-crm/internal/handlers/payments.go b/apps/go-crm/internal/handlers/payments.go index 035538a..8939dad 100644 --- a/apps/go-crm/internal/handlers/payments.go +++ b/apps/go-crm/internal/handlers/payments.go @@ -1,11 +1,13 @@ package handlers import ( + "fmt" "net/http" "strconv" "time" "go-crm/internal/db" + "go-crm/internal/templates" "github.com/go-chi/chi/v5" ) @@ -45,32 +47,105 @@ func (a *App) ListPayments(w http.ResponseWriter, r *http.Request) { clientNames := make(map[int64]string) customerNames := make(map[int64]string) for _, c := range clients { - clientOptions += `` + clientOptions += fmt.Sprintf(``, c.ClientID, htmlEscape(c.Name)) clientNames[c.ClientID] = c.Name } for _, cu := range customers { - customerOptions += `` + customerOptions += fmt.Sprintf(``, cu.CustomerID, htmlEscape(cu.Name)) customerNames[cu.CustomerID] = cu.Name } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`

Payments

`)) - for _, p := range payments { - paid := "No" - if p.HasPaid { - paid = "Yes" + buf := templates.BufRender() + actions := `` + fmt.Fprint(buf, templates.PageHeader("Payments", "Track revenue and transactions", actions)) + + fmt.Fprintf(buf, ` +`, clientOptions, customerOptions) + + if len(payments) == 0 { + fmt.Fprint(buf, templates.EmptyState(``, "No payments recorded yet.")) + } else { + fmt.Fprintf(buf, `
`) + fmt.Fprint(buf, templates.TableStart([]string{"Client", "Customer", "Amount", "Paid", "Date", "Method", "Actions"})) + for _, p := range payments { + paidPill := `unpaid` + if p.HasPaid { + paidPill = `paid` + } + clientName := clientNames[p.ClientID] + if clientName == "" { + clientName = strconv.FormatInt(p.ClientID, 10) + } + customerName := customerNames[p.CustomerID] + if customerName == "" { + customerName = strconv.FormatInt(p.CustomerID, 10) + } + fmt.Fprintf(buf, `
+ + + + + + + + `, htmlEscape(clientName), htmlEscape(customerName), p.Amount, paidPill, htmlEscape(p.PaymentDate), htmlEscape(p.PaymentMethod), p.PaymentID, p.PaymentID, p.PaymentID) } - clientName := clientNames[p.ClientID] - if clientName == "" { - clientName = strconv.FormatInt(p.ClientID, 10) + fmt.Fprint(buf, templates.TableEnd()) + fmt.Fprintf(buf, ``) + + for _, p := range payments { + clientName := clientNames[p.ClientID] + customerName := customerNames[p.CustomerID] + fmt.Fprintf(buf, ` +`, p.PaymentID, p.PaymentID, p.PaymentID, p.ClientID, htmlEscape(clientName), clientOptions, p.CustomerID, htmlEscape(customerName), customerOptions, p.Amount, htmlEscape(p.PaymentDate), p.PaymentMethod, p.PaymentMethod, p.PaymentID) } - customerName := customerNames[p.CustomerID] - if customerName == "" { - customerName = strconv.FormatInt(p.CustomerID, 10) - } - w.Write([]byte(``)) } - w.Write([]byte(`
ClientCustomerAmountPaidDateMethodActions
%s%sR$ %.2f%s%s%s +
+ View + +
+ +
+
+
` + clientName + `` + customerName + `` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `` + paid + `` + p.PaymentDate + `` + p.PaymentMethod + `View
`)) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "Payments", "payments") } func (a *App) CreatePayment(w http.ResponseWriter, r *http.Request) { @@ -111,7 +186,7 @@ func (a *App) ViewPayment(w http.ResponseWriter, r *http.Request) { return } - id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) var p db.Payment var hasPaid int err := a.DB.QueryRow("SELECT payment_id, client_id, customer_id, schedule_id, has_paid, amount, payment_date, payment_method, created_at FROM payments WHERE payment_id = ?", id).Scan(&p.PaymentID, &p.ClientID, &p.CustomerID, &p.ScheduleID, &hasPaid, &p.Amount, &p.PaymentDate, &p.PaymentMethod, &p.CreatedAt) @@ -121,8 +196,38 @@ func (a *App) ViewPayment(w http.ResponseWriter, r *http.Request) { } p.HasPaid = hasPaid == 1 + buf := templates.BufRender() + fmt.Fprint(buf, templates.PageHeader("Payment", "Transaction details")) + paidPill := `unpaid` + if p.HasPaid { + paidPill = `paid` + } + fmt.Fprintf(buf, ` +
+
+
%s
+
+
+ Amount + R$ %.2f +
+
+ Method + %s +
+
+ Date + %s +
+
+
+ Back +
+
+
`, paidPill, p.Amount, htmlEscape(p.PaymentMethod), htmlEscape(p.PaymentDate)) + w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`

Payment

Amount: ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `

Paid: ` + strconv.FormatBool(p.HasPaid) + `

Method: ` + p.PaymentMethod + `

Back`)) + templates.WritePage(w, buf, "Payment", "payments") } func (a *App) UpdatePayment(w http.ResponseWriter, r *http.Request) { @@ -151,11 +256,11 @@ func (a *App) DeletePayment(w http.ResponseWriter, r *http.Request) { return } - id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) a.DB.Exec("DELETE FROM payments WHERE payment_id = ?", id) } -// --- package-level shims kept for existing tests --- +// --- package-level shims --- func ListPayments(w http.ResponseWriter, r *http.Request) { (&App{DB: DB, WAConnector: WAConnector}).ListPayments(w, r) diff --git a/apps/go-crm/internal/handlers/questions.go b/apps/go-crm/internal/handlers/questions.go index af6b984..f96325c 100644 --- a/apps/go-crm/internal/handlers/questions.go +++ b/apps/go-crm/internal/handlers/questions.go @@ -1,11 +1,13 @@ package handlers import ( + "fmt" "net/http" "strconv" "time" "go-crm/internal/db" + "go-crm/internal/templates" "github.com/go-chi/chi/v5" ) @@ -45,28 +47,93 @@ func (a *App) ListQuestions(w http.ResponseWriter, r *http.Request) { clientNames := make(map[int64]string) customerNames := make(map[int64]string) for _, c := range clients { - clientOptions += `` + clientOptions += fmt.Sprintf(``, c.ClientID, htmlEscape(c.Name)) clientNames[c.ClientID] = c.Name } for _, cu := range customers { - customerOptions += `` + customerOptions += fmt.Sprintf(``, cu.CustomerID, htmlEscape(cu.Name)) customerNames[cu.CustomerID] = cu.Name } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`

Questions

`)) - for _, q := range questions { - clientName := clientNames[q.ClientID] - if clientName == "" { - clientName = strconv.FormatInt(q.ClientID, 10) + buf := templates.BufRender() + actions := `` + fmt.Fprint(buf, templates.PageHeader("Questions", "Customer inquiries and Q&A", actions)) + + fmt.Fprintf(buf, ` +`, clientOptions, customerOptions) + + if len(questions) == 0 { + fmt.Fprint(buf, templates.EmptyState(``, "No questions recorded yet.")) + } else { + fmt.Fprintf(buf, `
`) + fmt.Fprint(buf, templates.TableStart([]string{"Client", "Customer", "Question", "Status", "Actions"})) + for _, q := range questions { + clientName := clientNames[q.ClientID] + if clientName == "" { + clientName = strconv.FormatInt(q.ClientID, 10) + } + customerName := customerNames[q.CustomerID] + if customerName == "" { + customerName = strconv.FormatInt(q.CustomerID, 10) + } + statusPill := `pending` + if q.Status == "answered" { + statusPill = `answered` + } + fmt.Fprintf(buf, `
+ + + + + + `, htmlEscape(clientName), htmlEscape(customerName), htmlEscape(q.Question), statusPill, q.QuestionID, q.QuestionID, q.QuestionID) } - customerName := customerNames[q.CustomerID] - if customerName == "" { - customerName = strconv.FormatInt(q.CustomerID, 10) + fmt.Fprint(buf, templates.TableEnd()) + fmt.Fprintf(buf, ``) + + for _, q := range questions { + clientName := clientNames[q.ClientID] + customerName := customerNames[q.CustomerID] + fmt.Fprintf(buf, ` +`, q.QuestionID, q.QuestionID, q.QuestionID, q.ClientID, htmlEscape(clientName), clientOptions, q.CustomerID, htmlEscape(customerName), customerOptions, htmlEscape(q.Question), q.Status, q.Status, q.QuestionID) } - w.Write([]byte(``)) } - w.Write([]byte(`
ClientCustomerQuestionStatusActions
%s%s%s%s +
+ View + +
+ +
+
+
` + clientName + `` + customerName + `` + q.Question + `` + q.Status + `View
`)) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.WritePage(w, buf, "Questions", "questions") } func (a *App) CreateQuestion(w http.ResponseWriter, r *http.Request) { @@ -109,8 +176,29 @@ func (a *App) ViewQuestion(w http.ResponseWriter, r *http.Request) { return } + buf := templates.BufRender() + fmt.Fprint(buf, templates.PageHeader("Question", "Inquiry details")) + statusPill := `pending` + if q.Status == "answered" { + statusPill = `answered` + } + fmt.Fprintf(buf, ` +
+
+
%s
+
%s
+
+
Client ID: %d
+
Customer ID: %d
+
+
+ Back +
+
+
`, statusPill, htmlEscape(q.Question), q.ClientID, q.CustomerID) + w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(`

Question

Client ID: ` + strconv.FormatInt(q.ClientID, 10) + `

Customer ID: ` + strconv.FormatInt(q.CustomerID, 10) + `

Question: ` + q.Question + `

Status: ` + q.Status + `

Back`)) + templates.WritePage(w, buf, "Question", "questions") } func (a *App) UpdateQuestion(w http.ResponseWriter, r *http.Request) { @@ -141,6 +229,8 @@ func (a *App) DeleteQuestion(w http.ResponseWriter, r *http.Request) { a.DB.Exec("DELETE FROM questions WHERE question_id = ?", id) } +// --- Answers (in same file as original) --- + func (a *App) ListAnswers(w http.ResponseWriter, r *http.Request) { accountID, ok := a.requireAuth(w, r) if !ok { @@ -169,88 +259,93 @@ func (a *App) ListAnswers(w http.ResponseWriter, r *http.Request) { var questionOptions string questionText := make(map[int64]string) for _, q := range questions { - questionOptions += `` + questionOptions += fmt.Sprintf(``, q.QuestionID, htmlEscape(q.Question)) questionText[q.QuestionID] = q.Question } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(` - - - - - - -

Answers

- -