Add GOTH stack CRM platform skeleton
- Go module with chi router, bcrypt - SQLite schema for 9 tables - Auth, Clients, Customers, Services, Scheduling, Payments, Q&A handlers - HTMX template layouts
This commit is contained in:
166
apps/go-crm/internal/handlers/auth.go
Normal file
166
apps/go-crm/internal/handlers/auth.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func SignupPage(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Sign Up</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Sign Up</h1>
|
||||
<form method="POST" action="/auth/signup">
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
<input type="text" name="name" placeholder="Name" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Sign Up</button>
|
||||
</form>
|
||||
<p>Already have an account? <a href="/auth/login">Login</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
func Signup(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
email := r.FormValue("email")
|
||||
name := r.FormValue("name")
|
||||
password := r.FormValue("password")
|
||||
|
||||
if email == "" || name == "" || password == "" {
|
||||
http.Error(w, "All fields required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to hash password", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
account := struct {
|
||||
Email string
|
||||
Name string
|
||||
Password string
|
||||
CreatedAt int64
|
||||
}{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Password: string(hashedPassword),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
_, err = db.DB.Exec(
|
||||
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
|
||||
account.Email, account.Name, account.Password, account.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(w, "Email already exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||
}
|
||||
|
||||
func LoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form method="POST" action="/auth/login">
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
<input type="password" name="password" placeholder="Password" required>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<p>Don't have an account? <a href="/auth/signup">Sign Up</a></p>
|
||||
</body>
|
||||
</html>`))
|
||||
}
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
email := r.FormValue("email")
|
||||
password := r.FormValue("password")
|
||||
|
||||
var accountID int64
|
||||
var hashedPassword string
|
||||
err := DB.QueryRow("SELECT account_id, password FROM accounts WHERE email = ?", email).Scan(&accountID, &hashedPassword)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err != nil {
|
||||
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID := generateSessionID()
|
||||
expires := time.Now().Add(24 * time.Hour).Unix()
|
||||
|
||||
_, err = DB.Exec("INSERT INTO sessions (session_id, account_id, expires) VALUES (?, ?, ?)", sessionID, accountID, expires)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to create session", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
|
||||
http.Redirect(w, r, "/clients", http.StatusFound)
|
||||
}
|
||||
|
||||
func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err == nil {
|
||||
DB.Exec("DELETE FROM sessions WHERE session_id = ?", cookie.Value)
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Path: "/", MaxAge: -1})
|
||||
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||
}
|
||||
|
||||
func generateSessionID() string {
|
||||
return time.Now().Format("20060102150405") + "-" + randomString(32)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func getSession(r *http.Request) (int64, error) {
|
||||
cookie, err := r.Cookie("session")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var accountID int64
|
||||
err = DB.QueryRow("SELECT account_id FROM sessions WHERE session_id = ? AND expires > ?", cookie.Value, time.Now().Unix()).Scan(&accountID)
|
||||
return accountID, err
|
||||
}
|
||||
|
||||
func SetupAuthHandlers(db *sql.DB) {
|
||||
DB = db
|
||||
chi.RegisterMethod("GET")
|
||||
}
|
||||
142
apps/go-crm/internal/handlers/clients.go
Normal file
142
apps/go-crm/internal/handlers/clients.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func ListClients(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
clients, err := db.ListClients(DB, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Clients</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Clients</h1>
|
||||
<button class="btn" onclick="document.getElementById('clientForm').style.display='block'">Add Client</button>
|
||||
<div id="clientForm" style="display:none; margin-top:1rem;">
|
||||
<form hx-post="/clients" hx-target="#clientList" hx-swap="innerHTML">
|
||||
<input type="text" name="name" placeholder="Name" required>
|
||||
<input type="tel" name="phone" placeholder="Phone">
|
||||
<input type="email" name="email" placeholder="Email">
|
||||
<input type="text" name="address" placeholder="Address">
|
||||
<textarea name="notes" placeholder="Notes"></textarea>
|
||||
<button type="submit">Add Client</button>
|
||||
</form>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Phone</th><th>Email</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="clientList">
|
||||
`))
|
||||
for _, c := range clients {
|
||||
w.Write([]byte(`<tr>
|
||||
<td>` + c.Name + `</td>
|
||||
<td>` + c.Phone + `</td>
|
||||
<td>` + c.Email + `</td>
|
||||
<td>
|
||||
<a href="/clients/` + strconv.FormatInt(c.ClientID, 10) + `">View</a>
|
||||
<form method="DELETE" style="display:inline" hx-delete="/clients/` + strconv.FormatInt(c.ClientID, 10) + `" hx-target="closest tr">
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateClient(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
client := db.Client{
|
||||
Name: r.FormValue("name"),
|
||||
Phone: r.FormValue("phone"),
|
||||
Email: r.FormValue("email"),
|
||||
Address: r.FormValue("address"),
|
||||
Notes: r.FormValue("notes"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := client.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewClient(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
client, err := db.GetClientByID(DB, id)
|
||||
if err != nil {
|
||||
http.Error(w, "Client not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h1>` + client.Name + `</h1>
|
||||
<p>Phone: ` + client.Phone + `</p>
|
||||
<p>Email: ` + client.Email + `</p>
|
||||
<p>Address: ` + client.Address + `</p>
|
||||
<p>Notes: ` + client.Notes + `</p>
|
||||
<a href="/clients">Back</a>
|
||||
</body></html>`))
|
||||
}
|
||||
|
||||
func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
r.ParseForm()
|
||||
client := db.Client{
|
||||
ClientID: id,
|
||||
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 {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(client)
|
||||
}
|
||||
|
||||
func DeleteClient(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
if err := (&db.Client{}).Delete(DB, id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
84
apps/go-crm/internal/handlers/customers.go
Normal file
84
apps/go-crm/internal/handlers/customers.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func ListCustomers(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
customers, err := db.ListCustomers(DB, clientID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Customers</h1><button class="btn" onclick="document.getElementById('customerForm').style.display='block'">Add Customer</button><div id="customerForm" style="display:none; margin-top:1rem;"><form hx-post="/customers" hx-target="#customerList"><input type="text" name="name" placeholder="Name" required><input type="tel" name="phone" placeholder="Phone"><input type="date" name="birth_date" placeholder="Birth Date"><input type="text" name="instagram" placeholder="Instagram"><input type="number" name="client_id" placeholder="Client ID" required><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Phone</th><th>Birth Date</th><th>Instagram</th><th>Actions</th></tr></thead><tbody id="customerList">`))
|
||||
for _, c := range customers {
|
||||
w.Write([]byte(`<tr><td>` + c.Name + `</td><td>` + c.Phone + `</td><td>` + c.BirthDate + `</td><td>` + c.Instagram + `</td><td><a href="/customers/` + strconv.FormatInt(c.CustomerID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
customer := db.Customer{
|
||||
ClientID: clientID,
|
||||
Name: r.FormValue("name"),
|
||||
Phone: r.FormValue("phone"),
|
||||
BirthDate: r.FormValue("birth_date"),
|
||||
Instagram: r.FormValue("instagram"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := customer.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
var c db.Customer
|
||||
err := DB.QueryRow("SELECT customer_id, client_id, name, phone, birth_date, instagram, created_at FROM customers WHERE customer_id = ?", id).Scan(&c.CustomerID, &c.ClientID, &c.Name, &c.Phone, &c.BirthDate, &c.Instagram, &c.CreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Customer not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>` + c.Name + `</h1><p>Phone: ` + c.Phone + `</p><p>Birth Date: ` + c.BirthDate + `</p><p>Instagram: ` + c.Instagram + `</p><a href="/customers">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdateCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
_, err := DB.Exec("UPDATE customers SET client_id=?, name=?, phone=?, birth_date=?, instagram=? WHERE customer_id=?", clientID, r.FormValue("name"), r.FormValue("phone"), r.FormValue("birth_date"), r.FormValue("instagram"), id)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
DB.Exec("DELETE FROM customers WHERE customer_id = ?", id)
|
||||
}
|
||||
88
apps/go-crm/internal/handlers/payments.go
Normal file
88
apps/go-crm/internal/handlers/payments.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
)
|
||||
|
||||
func ListPayments(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
payments, err := db.ListPayments(DB, clientID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Payments</h1><button class="btn" onclick="document.getElementById('paymentForm').style.display='block'">Add Payment</button><div id="paymentForm" style="display:none; margin-top:1rem;"><form hx-post="/payments" hx-target="#paymentList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="customer_id" placeholder="Customer ID" required><input type="number" name="schedule_id" placeholder="Schedule ID"><input type="checkbox" name="has_paid"><label>Paid</label><input type="number" name="amount" placeholder="Amount" step="0.01"><input type="date" name="payment_date"><input type="text" name="payment_method" placeholder="Payment Method"><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Amount</th><th>Paid</th><th>Date</th><th>Method</th><th>Actions</th></tr></thead><tbody id="paymentList">`))
|
||||
for _, p := range payments {
|
||||
paid := "No"
|
||||
if p.HasPaid {
|
||||
paid = "Yes"
|
||||
}
|
||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(p.CustomerID, 10) + `</td><td>` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</td><td>` + paid + `</td><td>` + p.PaymentDate + `</td><td>` + p.PaymentMethod + `</td><td><a href="/payments/` + strconv.FormatInt(p.PaymentID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreatePayment(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
||||
scheduleID, _ := strconv.ParseInt(r.FormValue("schedule_id"), 10, 64)
|
||||
amount, _ := strconv.ParseFloat(r.FormValue("amount"), 64)
|
||||
hasPaid := r.FormValue("has_paid") == "on"
|
||||
|
||||
payment := db.Payment{
|
||||
ClientID: clientID,
|
||||
CustomerID: customerID,
|
||||
ScheduleID: scheduleID,
|
||||
HasPaid: hasPaid,
|
||||
Amount: amount,
|
||||
PaymentDate: r.FormValue("payment_date"),
|
||||
PaymentMethod: r.FormValue("payment_method"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := payment.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewPayment(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10)
|
||||
var p db.Payment
|
||||
var hasPaid int
|
||||
err := 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)
|
||||
if err != nil {
|
||||
http.Error(w, "Payment not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
p.HasPaid = hasPaid == 1
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Payment</h1><p>Amount: ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</p><p>Paid: ` + strconv.FormatBool(p.HasPaid) + `</p><p>Method: ` + p.PaymentMethod + `</p><a href="/payments">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdatePayment(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func DeletePayment(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func boolToStr(b bool) string {
|
||||
if b {
|
||||
return "Yes"
|
||||
}
|
||||
return "No"
|
||||
}
|
||||
113
apps/go-crm/internal/handlers/questions.go
Normal file
113
apps/go-crm/internal/handlers/questions.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
)
|
||||
|
||||
func ListQuestions(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
questions, err := db.ListQuestions(DB, clientID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Questions</h1><button class="btn" onclick="document.getElementById('questionForm').style.display='block'">Add Question</button><div id="questionForm" style="display:none; margin-top:1rem;"><form hx-post="/questions" hx-target="#questionList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="customer_id" placeholder="Customer ID" required><textarea name="question" placeholder="Question" required></textarea><select name="status"><option value="pending">Pending</option><option value="answered">Answered</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Question</th><th>Status</th><th>Actions</th></tr></thead><tbody id="questionList">`))
|
||||
for _, q := range questions {
|
||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(q.CustomerID, 10) + `</td><td>` + q.Question + `</td><td>` + q.Status + `</td><td><a href="/questions/` + strconv.FormatInt(q.QuestionID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
||||
question := db.Question{
|
||||
ClientID: clientID,
|
||||
CustomerID: customerID,
|
||||
Question: r.FormValue("question"),
|
||||
Timestamp: time.Now().Unix(),
|
||||
Status: r.FormValue("status"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := question.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Question</h1><a href="/questions">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdateQuestion(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func DeleteQuestion(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func ListAnswers(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
questionID, _ := strconv.ParseInt(r.URL.Query().Get("question_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
answers, err := db.ListAnswers(DB, questionID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Answers</h1><button class="btn" onclick="document.getElementById('answerForm').style.display='block'">Add Answer</button><div id="answerForm" style="display:none; margin-top:1rem;"><form hx-post="/answers" hx-target="#answerList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="question_id" placeholder="Question ID" required><textarea name="answer" placeholder="Answer" required></textarea><select name="status"><option value="active">Active</option><option value="deleted">Deleted</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Question</th><th>Answer</th><th>Status</th><th>Actions</th></tr></thead><tbody id="answerList">`))
|
||||
for _, a := range answers {
|
||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(a.QuestionID, 10) + `</td><td>` + a.Answer + `</td><td>` + a.Status + `</td><td><a href="/answers/` + strconv.FormatInt(a.AnswerID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateAnswer(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
questionID, _ := strconv.ParseInt(r.FormValue("question_id"), 10, 64)
|
||||
answer := db.Answer{
|
||||
ClientID: clientID,
|
||||
QuestionID: questionID,
|
||||
Answer: r.FormValue("answer"),
|
||||
Timestamp: time.Now().Unix(),
|
||||
Status: r.FormValue("status"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := answer.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewAnswer(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Answer</h1><a href="/answers">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdateAnswer(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func DeleteAnswer(w http.ResponseWriter, r *http.Request) {}
|
||||
84
apps/go-crm/internal/handlers/scheduling.go
Normal file
84
apps/go-crm/internal/handlers/scheduling.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func ListSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
|
||||
customerID, _ := strconv.ParseInt(r.URL.Query().Get("customer_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
schedules, err := db.ListSchedules(DB, clientID, customerID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Scheduling</h1><button class="btn" onclick="document.getElementById('scheduleForm').style.display='block'">Add Schedule</button><div id="scheduleForm" style="display:none; margin-top:1rem;"><form hx-post="/scheduling" hx-target="#scheduleList"><input type="number" name="customer_id" placeholder="Customer ID" required><input type="number" name="client_id" placeholder="Client ID" required><input type="date" name="plan_date"><input type="date" name="confirmed_date"><input type="time" name="time"><select name="status"><option value="pending">Pending</option><option value="confirmed">Confirmed</option><option value="cancelled">Cancelled</option></select><textarea name="notes" placeholder="Notes"></textarea><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Date</th><th>Time</th><th>Status</th><th>Actions</th></tr></thead><tbody id="scheduleList">`))
|
||||
for _, s := range schedules {
|
||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(s.CustomerID, 10) + `</td><td>` + s.PlanDate + `</td><td>` + s.Time + `</td><td>` + s.Status + `</td><td><a href="/scheduling/` + strconv.FormatInt(s.ScheduleID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
schedule := db.Schedule{
|
||||
CustomerID: customerID,
|
||||
ClientID: clientID,
|
||||
PlanDate: r.FormValue("plan_date"),
|
||||
ConfirmedDate: r.FormValue("confirmed_date"),
|
||||
Time: r.FormValue("time"),
|
||||
Status: r.FormValue("status"),
|
||||
Notes: r.FormValue("notes"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := schedule.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
var sch db.Schedule
|
||||
err := DB.QueryRow("SELECT schedule_id, customer_id, client_id, plan_date, confirmed_date, time, status, notes, created_at FROM scheduling WHERE schedule_id = ?", id).Scan(&sch.ScheduleID, &sch.CustomerID, &sch.ClientID, &sch.PlanDate, &sch.ConfirmedDate, &sch.Time, &sch.Status, &sch.Notes, &sch.CreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Schedule not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Schedule</h1><p>Customer ID: ` + strconv.FormatInt(sch.CustomerID, 10) + `</p><p>Date: ` + sch.PlanDate + `</p><p>Time: ` + sch.Time + `</p><p>Status: ` + sch.Status + `</p><a href="/scheduling">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdateSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
r.ParseForm()
|
||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
DB.Exec("UPDATE scheduling SET customer_id=?, client_id=?, plan_date=?, confirmed_date=?, time=?, status=?, notes=? WHERE schedule_id=?", customerID, clientID, r.FormValue("plan_date"), r.FormValue("confirmed_date"), r.FormValue("time"), r.FormValue("status"), r.FormValue("notes"), id)
|
||||
}
|
||||
|
||||
func DeleteSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
DB.Exec("DELETE FROM scheduling WHERE schedule_id = ?", id)
|
||||
}
|
||||
81
apps/go-crm/internal/handlers/services.go
Normal file
81
apps/go-crm/internal/handlers/services.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-crm/internal/db"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func ListServices(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
clientID, _ := strconv.ParseInt(r.URL.Query().Get("client_id"), 10, 64)
|
||||
if limit == 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
services, err := db.ListServices(DB, clientID, limit, offset)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Services</h1><button class="btn" onclick="document.getElementById('serviceForm').style.display='block'">Add Service</button><div id="serviceForm" style="display:none; margin-top:1rem;"><form hx-post="/services" hx-target="#serviceList"><input type="text" name="name" placeholder="Service Name" required><input type="number" name="price" placeholder="Price" step="0.01"><textarea name="description" placeholder="Description"></textarea><input type="text" name="duration" placeholder="Duration"><input type="number" name="client_id" placeholder="Client ID" required><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Price</th><th>Duration</th><th>Actions</th></tr></thead><tbody id="serviceList">`))
|
||||
for _, s := range services {
|
||||
w.Write([]byte(`<tr><td>` + s.Name + `</td><td>` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `</td><td>` + s.Duration + `</td><td><a href="/services/` + strconv.FormatInt(s.ServiceID, 10) + `">View</a></td></tr>`))
|
||||
}
|
||||
w.Write([]byte(`</tbody></table></body></html>`))
|
||||
}
|
||||
|
||||
func CreateService(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
price, _ := strconv.ParseFloat(r.FormValue("price"), 64)
|
||||
service := db.Service{
|
||||
ClientID: clientID,
|
||||
Name: r.FormValue("name"),
|
||||
Price: price,
|
||||
Description: r.FormValue("description"),
|
||||
Duration: r.FormValue("duration"),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := service.Create(DB); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
}
|
||||
|
||||
func ViewService(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
var s db.Service
|
||||
err := DB.QueryRow("SELECT service_id, client_id, name, price, description, duration, created_at FROM services WHERE service_id = ?", id).Scan(&s.ServiceID, &s.ClientID, &s.Name, &s.Price, &s.Description, &s.Duration, &s.CreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Service not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><body><h1>` + s.Name + `</h1><p>Price: ` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `</p><p>Description: ` + s.Description + `</p><p>Duration: ` + s.Duration + `</p><a href="/services">Back</a></body></html>`))
|
||||
}
|
||||
|
||||
func UpdateService(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
r.ParseForm()
|
||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||
price, _ := strconv.ParseFloat(r.FormValue("price"), 64)
|
||||
DB.Exec("UPDATE services SET client_id=?, name=?, price=?, description=?, duration=? WHERE service_id=?", clientID, r.FormValue("name"), price, r.FormValue("description"), r.FormValue("duration"), id)
|
||||
}
|
||||
|
||||
func DeleteService(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10)
|
||||
DB.Exec("DELETE FROM services WHERE service_id = ?", id)
|
||||
}
|
||||
14
apps/go-crm/internal/handlers/setup.go
Normal file
14
apps/go-crm/internal/handlers/setup.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
func SetupHandlers(db *sql.DB) {
|
||||
DB = db
|
||||
}
|
||||
|
||||
func getCurrentTimestamp() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
Reference in New Issue
Block a user