feat(go-crm): full auth, routing, middleware, and supporting infra
- Add auth handlers (signup, login, logout, account management) with bcrypt - Add client, customer, service, scheduling, payment, question, answer handlers - Add dashboard, monthly report, and lead pipeline pages - Add UTF-8 middleware to force charset on HTML responses - Add config package with env-based overrides for DB path, secrets, endpoints - Add parser package for WhatsApp message ingestion - Add clean-arch layers: pkg/domain, pkg/repo, pkg/usecase for leads - Add cmd/migrate utility for DB migrations - Add Makefile, README, run-tests.sh, and dev scripts - Update docker-compose.yml with memory limits - Update .air.toml to exclude DB files and stop on errors - Update whatsapp-sync dependencies and add src/index.js entrypoint - Add whatsme standalone WhatsApp reader app (source only) - Untrack .opencode-sandbox/data/go-crm.db from git history - Expand root .gitignore: ngrok, tmp dirs, sandbox DBs, compiled binaries
This commit is contained in:
@@ -10,8 +10,8 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := requireAuth(w, r)
|
||||
func (a *App) ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := a.requireAuth(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -22,13 +22,13 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
clients, err := db.ListClients(DB, accountID, limit, offset)
|
||||
clients, err := db.ListClients(a.DB, accountID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -60,8 +60,14 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
`))
|
||||
for _, c := range clients {
|
||||
var whatsappCell string
|
||||
if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" {
|
||||
whatsappCell = c.WhatsAppNumber
|
||||
connected := false
|
||||
if c.WhatsAppNumber != "" && a.WAConnector != nil {
|
||||
connected, _ = a.WAConnector.IsConnected(r.Context(), c.ClientID)
|
||||
}
|
||||
if connected {
|
||||
whatsappCell = c.WhatsAppNumber + ` <span style="color:#28a745">●</span>`
|
||||
} else if c.WhatsAppConnected == 1 && c.WhatsAppNumber != "" {
|
||||
whatsappCell = c.WhatsAppNumber + ` <span style="color:#dc3545">○</span> <a href="/leads/connect?client_id=` + strconv.FormatInt(c.ClientID, 10) + `">Reconnect</a>`
|
||||
} else {
|
||||
whatsappCell = `<a href="/leads/connect?client_id=` + strconv.FormatInt(c.ClientID, 10) + `">Connect</a>`
|
||||
}
|
||||
@@ -88,11 +94,13 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
</td>
|
||||
</tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
w.Write([]byte(`</tbody></table>
|
||||
<p><a href="/">Back to Home</a></p>
|
||||
</body></html>`))
|
||||
}
|
||||
|
||||
func CreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := requireAuth(w, r)
|
||||
func (a *App) CreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := a.requireAuth(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -104,33 +112,57 @@ func CreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
Phone: r.FormValue("phone"),
|
||||
Email: r.FormValue("email"),
|
||||
Address: r.FormValue("address"),
|
||||
Notes: r.FormValue("notes"),
|
||||
Notes: r.FormValue("notes"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := client.Create(DB); err != nil {
|
||||
if err := client.Create(a.DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := requireAuth(w, r)
|
||||
func (a *App) ViewClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := a.requireAuth(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
client, err := db.GetClientByID(DB, accountID, id)
|
||||
client, err := db.GetClientByID(a.DB, accountID, id)
|
||||
if err != nil {
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
var whatsappSection string
|
||||
if client.WhatsAppNumber != "" {
|
||||
connected := false
|
||||
if a.WAConnector != nil {
|
||||
connected, _ = a.WAConnector.IsConnected(r.Context(), client.ClientID)
|
||||
}
|
||||
status := "Not connected"
|
||||
style := "color:#999"
|
||||
if connected {
|
||||
status = "Connected"
|
||||
style = "color:#28a745"
|
||||
} else if client.WhatsAppConnected == 1 {
|
||||
status = "Disconnected (was connected)"
|
||||
style = "color:#dc3545"
|
||||
}
|
||||
whatsappSection = `
|
||||
<p>WhatsApp: <strong>` + client.WhatsAppNumber + `</strong> <span style="` + style + `">(` + status + `)</span></p>
|
||||
<p><a href="/leads/connect?client_id=` + strconv.FormatInt(client.ClientID, 10) + `">Reconnect WhatsApp</a></p>`
|
||||
} else {
|
||||
whatsappSection = `
|
||||
<p>WhatsApp: Not configured <a href="/leads/connect?client_id=` + strconv.FormatInt(client.ClientID, 10) + `">Connect</a></p>`
|
||||
}
|
||||
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
@@ -140,13 +172,13 @@ func ViewClient(w http.ResponseWriter, r *http.Request) {
|
||||
<p>Phone: ` + client.Phone + `</p>
|
||||
<p>Email: ` + client.Email + `</p>
|
||||
<p>Address: ` + client.Address + `</p>
|
||||
<p>Notes: ` + client.Notes + `</p>
|
||||
<p>Notes: ` + client.Notes + `</p>` + whatsappSection + `
|
||||
<a href="/clients">Back</a>
|
||||
</body></html>`))
|
||||
}
|
||||
|
||||
func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := requireAuth(w, r)
|
||||
func (a *App) UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := a.requireAuth(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -156,14 +188,14 @@ func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
client := db.Client{
|
||||
ClientID: id,
|
||||
AccountID: accountID,
|
||||
Name: r.FormValue("name"),
|
||||
Phone: r.FormValue("phone"),
|
||||
Email: r.FormValue("email"),
|
||||
Address: r.FormValue("address"),
|
||||
Notes: r.FormValue("notes"),
|
||||
Name: r.FormValue("name"),
|
||||
Phone: r.FormValue("phone"),
|
||||
Email: r.FormValue("email"),
|
||||
Address: r.FormValue("address"),
|
||||
Notes: r.FormValue("notes"),
|
||||
}
|
||||
|
||||
if err := client.Update(DB); err != nil {
|
||||
if err := client.Update(a.DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -171,19 +203,37 @@ func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func DeleteClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := requireAuth(w, r)
|
||||
func (a *App) DeleteClient(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := a.requireAuth(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
client := &db.Client{ClientID: id, AccountID: accountID}
|
||||
if err := client.Delete(DB); err != nil {
|
||||
if err := client.Delete(a.DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
}
|
||||
|
||||
// --- package-level shims kept for existing tests ---
|
||||
|
||||
func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
(&App{DB: DB, WAConnector: WAConnector}).ListClients(w, r)
|
||||
}
|
||||
func CreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
(&App{DB: DB, WAConnector: WAConnector}).CreateClient(w, r)
|
||||
}
|
||||
func ViewClient(w http.ResponseWriter, r *http.Request) {
|
||||
(&App{DB: DB, WAConnector: WAConnector}).ViewClient(w, r)
|
||||
}
|
||||
func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
(&App{DB: DB, WAConnector: WAConnector}).UpdateClient(w, r)
|
||||
}
|
||||
func DeleteClient(w http.ResponseWriter, r *http.Request) {
|
||||
(&App{DB: DB, WAConnector: WAConnector}).DeleteClient(w, r)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user