diff --git a/apps/go-crm/go.mod b/apps/go-crm/go.mod new file mode 100644 index 0000000..bbb8ffe --- /dev/null +++ b/apps/go-crm/go.mod @@ -0,0 +1,14 @@ +module go-crm + +go 1.22 + +require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-webauthen/webauthen v0.0.0 + golang.org/x/crypto v0.27.0 +) + +require ( + github.com/go-fresty/xtpl v0.6.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect +) \ No newline at end of file diff --git a/apps/go-crm/internal/db/crud.go b/apps/go-crm/internal/db/crud.go new file mode 100644 index 0000000..05a424d --- /dev/null +++ b/apps/go-crm/internal/db/crud.go @@ -0,0 +1,420 @@ +package db + +import ( + "database/sql" + "fmt" +) + +type Client struct { + ClientID int64 `json:"client_id"` + Name string `json:"name"` + Phone string `json:"phone,omitempty"` + Email string `json:"email,omitempty"` + Address string `json:"address,omitempty"` + Notes string `json:"notes,omitempty"` + CreatedAt int64 `json:"created_at"` +} + +type Customer struct { + CustomerID int64 `json:"customer_id"` + ClientID int64 `json:"client_id"` + Name string `json:"name"` + Phone string `json:"phone,omitempty"` + BirthDate string `json:"birth_date,omitempty"` + Instagram string `json:"instagram,omitempty"` + CreatedAt int64 `json:"created_at"` +} + +type Service struct { + ServiceID int64 `json:"service_id"` + ClientID int64 `json:"client_id"` + Name string `json:"name"` + Price float64 `json:"price,omitempty"` + Description string `json:"description,omitempty"` + Duration string `json:"duration,omitempty"` + CreatedAt int64 `json:"created_at"` +} + +type Schedule struct { + ScheduleID int64 `json:"schedule_id"` + CustomerID int64 `json:"customer_id"` + ClientID int64 `json:"client_id"` + PlanDate string `json:"plan_date,omitempty"` + ConfirmedDate string `json:"confirmed_date,omitempty"` + Time string `json:"time,omitempty"` + Status string `json:"status"` + Notes string `json:"notes,omitempty"` + CreatedAt int64 `json:"created_at"` +} + +type Payment struct { + PaymentID int64 `json:"payment_id"` + ClientID int64 `json:"client_id"` + CustomerID int64 `json:"customer_id"` + ScheduleID int64 `json:"schedule_id,omitempty"` + HasPaid bool `json:"has_paid"` + Amount float64 `json:"amount,omitempty"` + PaymentDate string `json:"payment_date,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + CreatedAt int64 `json:"created_at"` +} + +type Question struct { + QuestionID int64 `json:"question_id"` + ClientID int64 `json:"client_id"` + CustomerID int64 `json:"customer_id"` + Question string `json:"question"` + Timestamp int64 `json:"timestamp"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` +} + +type Answer struct { + AnswerID int64 `json:"answer_id"` + ClientID int64 `json:"client_id"` + QuestionID int64 `json:"question_id"` + Answer string `json:"answer"` + Timestamp int64 `json:"timestamp"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` +} + +func (c *Client) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO clients (name, phone, email, address, notes, created_at) VALUES (?, ?, ?, ?, ?, ?)", + c.Name, c.Phone, c.Email, c.Address, c.Notes, c.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + c.ClientID = id + return nil +} + +func (c *Client) Read(db *sql.DB, id int64) error { + return db.QueryRow( + "SELECT client_id, name, phone, email, address, notes, created_at FROM clients WHERE client_id = ?", + id, + ).Scan(&c.ClientID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt) +} + +func (c *Client) Update(db *sql.DB) error { + _, err := db.Exec( + "UPDATE clients SET name = ?, phone = ?, email = ?, address = ?, notes = ? WHERE client_id = ?", + c.Name, c.Phone, c.Email, c.Address, c.Notes, c.ClientID, + ) + return err +} + +func (c *Client) Delete(db *sql.DB, id int64) error { + _, err := db.Exec("DELETE FROM clients WHERE client_id = ?", id) + return err +} + +func ListClients(db *sql.DB, limit, offset int) ([]Client, error) { + rows, err := db.Query( + "SELECT client_id, name, phone, email, address, notes, created_at FROM clients ORDER BY created_at DESC LIMIT ? OFFSET ?", + limit, offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var clients []Client + for rows.Next() { + var c Client + if err := rows.Scan(&c.ClientID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt); err != nil { + return nil, err + } + clients = append(clients, c) + } + return clients, rows.Err() +} + +func GetClientByID(db *sql.DB, id int64) (*Client, error) { + var c Client + err := db.QueryRow( + "SELECT client_id, name, phone, email, address, notes, created_at FROM clients WHERE client_id = ?", + id, + ).Scan(&c.ClientID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt) + if err != nil { + return nil, err + } + return &c, nil +} + +func (cu *Customer) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO customers (client_id, name, phone, birth_date, instagram, created_at) VALUES (?, ?, ?, ?, ?, ?)", + cu.ClientID, cu.Name, cu.Phone, cu.BirthDate, cu.Instagram, cu.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + cu.CustomerID = id + return nil +} + +func ListCustomers(db *sql.DB, clientID int64, limit, offset int) ([]Customer, error) { + query := "SELECT customer_id, client_id, name, phone, birth_date, instagram, created_at FROM customers" + var args []interface{} + if clientID > 0 { + query += " WHERE client_id = ?" + args = append(args, clientID) + } + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var customers []Customer + for rows.Next() { + var cu Customer + if err := rows.Scan(&cu.CustomerID, &cu.ClientID, &cu.Name, &cu.Phone, &cu.BirthDate, &cu.Instagram, &cu.CreatedAt); err != nil { + return nil, err + } + customers = append(customers, cu) + } + return customers, rows.Err() +} + +func (s *Service) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO services (client_id, name, price, description, duration, created_at) VALUES (?, ?, ?, ?, ?, ?)", + s.ClientID, s.Name, s.Price, s.Description, s.Duration, s.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + s.ServiceID = id + return nil +} + +func ListServices(db *sql.DB, clientID int64, limit, offset int) ([]Service, error) { + query := "SELECT service_id, client_id, name, price, description, duration, created_at FROM services" + var args []interface{} + if clientID > 0 { + query += " WHERE client_id = ?" + args = append(args, clientID) + } + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var services []Service + for rows.Next() { + var s Service + if err := rows.Scan(&s.ServiceID, &s.ClientID, &s.Name, &s.Price, &s.Description, &s.Duration, &s.CreatedAt); err != nil { + return nil, err + } + services = append(services, s) + } + return services, rows.Err() +} + +func (sch *Schedule) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO scheduling (customer_id, client_id, plan_date, confirmed_date, time, status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + sch.CustomerID, sch.ClientID, sch.PlanDate, sch.ConfirmedDate, sch.Time, sch.Status, sch.Notes, sch.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + sch.ScheduleID = id + return nil +} + +func ListSchedules(db *sql.DB, clientID, customerID int64, limit, offset int) ([]Schedule, error) { + query := "SELECT schedule_id, customer_id, client_id, plan_date, confirmed_date, time, status, notes, created_at FROM scheduling WHERE 1=1" + var args []interface{} + if clientID > 0 { + query += " AND client_id = ?" + args = append(args, clientID) + } + if customerID > 0 { + query += " AND customer_id = ?" + args = append(args, customerID) + } + query += " ORDER BY plan_date DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var schedules []Schedule + for rows.Next() { + var sch Schedule + if err := rows.Scan(&sch.ScheduleID, &sch.CustomerID, &sch.ClientID, &sch.PlanDate, &sch.ConfirmedDate, &sch.Time, &sch.Status, &sch.Notes, &sch.CreatedAt); err != nil { + return nil, err + } + schedules = append(schedules, sch) + } + return schedules, rows.Err() +} + +func (p *Payment) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO payments (client_id, customer_id, schedule_id, has_paid, amount, payment_date, payment_method, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + p.ClientID, p.CustomerID, p.ScheduleID, boolToInt(p.HasPaid), p.Amount, p.PaymentDate, p.PaymentMethod, p.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + p.PaymentID = id + return nil +} + +func ListPayments(db *sql.DB, clientID int64, limit, offset int) ([]Payment, error) { + query := "SELECT payment_id, client_id, customer_id, schedule_id, has_paid, amount, payment_date, payment_method, created_at FROM payments" + var args []interface{} + if clientID > 0 { + query += " WHERE client_id = ?" + args = append(args, clientID) + } + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var payments []Payment + for rows.Next() { + var p Payment + var hasPaid int + if err := rows.Scan(&p.PaymentID, &p.ClientID, &p.CustomerID, &p.ScheduleID, &hasPaid, &p.Amount, &p.PaymentDate, &p.PaymentMethod, &p.CreatedAt); err != nil { + return nil, err + } + p.HasPaid = hasPaid == 1 + payments = append(payments, p) + } + return payments, rows.Err() +} + +func (q *Question) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO questions (client_id, customer_id, question, timestamp, status, created_at) VALUES (?, ?, ?, ?, ?, ?)", + q.ClientID, q.CustomerID, q.Question, q.Timestamp, q.Status, q.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + q.QuestionID = id + return nil +} + +func ListQuestions(db *sql.DB, clientID int64, limit, offset int) ([]Question, error) { + query := "SELECT question_id, client_id, customer_id, question, timestamp, status, created_at FROM questions" + var args []interface{} + if clientID > 0 { + query += " WHERE client_id = ?" + args = append(args, clientID) + } + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var questions []Question + for rows.Next() { + var q Question + if err := rows.Scan(&q.QuestionID, &q.ClientID, &q.CustomerID, &q.Question, &q.Timestamp, &q.Status, &q.CreatedAt); err != nil { + return nil, err + } + questions = append(questions, q) + } + return questions, rows.Err() +} + +func (a *Answer) Create(db *sql.DB) error { + result, err := db.Exec( + "INSERT INTO answers (client_id, question_id, answer, timestamp, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + a.ClientID, a.QuestionID, a.Answer, a.Timestamp, a.Status, a.CreatedAt, + ) + if err != nil { + return err + } + id, err := result.LastInsertId() + if err != nil { + return err + } + a.AnswerID = id + return nil +} + +func ListAnswers(db *sql.DB, questionID int64, limit, offset int) ([]Answer, error) { + query := "SELECT answer_id, client_id, question_id, answer, timestamp, status, created_at FROM answers" + var args []interface{} + if questionID > 0 { + query += " WHERE question_id = ?" + args = append(args, questionID) + } + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + var answers []Answer + for rows.Next() { + var a Answer + if err := rows.Scan(&a.AnswerID, &a.ClientID, &a.QuestionID, &a.Answer, &a.Timestamp, &a.Status, &a.CreatedAt); err != nil { + return nil, err + } + answers = append(answers, a) + } + return answers, rows.Err() +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} \ No newline at end of file diff --git a/apps/go-crm/internal/db/db.go b/apps/go-crm/internal/db/db.go new file mode 100644 index 0000000..463c473 --- /dev/null +++ b/apps/go-crm/internal/db/db.go @@ -0,0 +1,140 @@ +package db + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + + _ "github.com/mattn/go-sqlite3" +) + +const schema = ` +CREATE TABLE IF NOT EXISTS accounts ( + account_id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + password TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + account_id INTEGER NOT NULL, + expires INTEGER NOT NULL, + FOREIGN KEY (account_id) REFERENCES accounts(account_id) +); + +CREATE TABLE IF NOT EXISTS clients ( + client_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + phone TEXT, + email TEXT, + address TEXT, + notes TEXT, + created_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS customers ( + customer_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + name TEXT NOT NULL, + phone TEXT, + birth_date TEXT, + instagram TEXT, + created_at INTEGER NOT NULL, + FOREIGN KEY (client_id) REFERENCES clients(client_id) +); + +CREATE TABLE IF NOT EXISTS services ( + service_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + name TEXT NOT NULL, + price REAL, + description TEXT, + duration TEXT, + created_at INTEGER NOT NULL, + FOREIGN KEY (client_id) REFERENCES clients(client_id) +); + +CREATE TABLE IF NOT EXISTS scheduling ( + schedule_id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER NOT NULL, + client_id INTEGER NOT NULL, + plan_date TEXT, + confirmed_date TEXT, + time TEXT, + status TEXT DEFAULT 'pending', + notes TEXT, + created_at INTEGER NOT NULL, + FOREIGN KEY (customer_id) REFERENCES customers(customer_id), + FOREIGN KEY (client_id) REFERENCES clients(client_id) +); + +CREATE TABLE IF NOT EXISTS payments ( + payment_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + schedule_id INTEGER, + has_paid INTEGER DEFAULT 0, + amount REAL, + payment_date TEXT, + payment_method TEXT, + created_at INTEGER NOT NULL, + FOREIGN KEY (client_id) REFERENCES clients(client_id), + FOREIGN KEY (customer_id) REFERENCES customers(customer_id), + FOREIGN KEY (schedule_id) REFERENCES scheduling(schedule_id) +); + +CREATE TABLE IF NOT EXISTS questions ( + question_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + question TEXT NOT NULL, + timestamp INTEGER NOT NULL, + status TEXT DEFAULT 'pending', + created_at INTEGER NOT NULL, + FOREIGN KEY (client_id) REFERENCES clients(client_id), + FOREIGN KEY (customer_id) REFERENCES customers(customer_id) +); + +CREATE TABLE IF NOT EXISTS answers ( + answer_id INTEGER PRIMARY KEY AUTOINCREMENT, + client_id INTEGER NOT NULL, + question_id INTEGER NOT NULL, + answer TEXT NOT NULL, + timestamp INTEGER NOT NULL, + status TEXT DEFAULT 'active', + created_at INTEGER NOT NULL, + FOREIGN KEY (client_id) REFERENCES clients(client_id), + FOREIGN KEY (question_id) REFERENCES questions(question_id) +); +` + +var dbPath = filepath.Join(os.Getenv("WORKDIR"), "data", "go-crm.db") + +func Init(path string) (*sql.DB, error) { + if path != "" { + dbPath = path + } + + dir := filepath.Dir(dbPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, fmt.Errorf("failed to create data directory: %w", err) + } + + database, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + if err := database.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + if _, err := database.Exec(schema); err != nil { + return nil, fmt.Errorf("failed to create schema: %w", err) + } + + return database, nil +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/auth.go b/apps/go-crm/internal/handlers/auth.go new file mode 100644 index 0000000..20ba7e5 --- /dev/null +++ b/apps/go-crm/internal/handlers/auth.go @@ -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(` + + + + Sign Up + + +

Sign Up

+
+ + + + +
+

Already have an account? Login

+ +`)) +} + +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(` + + + + Login + + +

Login

+
+ + + +
+

Don't have an account? Sign Up

+ +`)) +} + +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") +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/clients.go b/apps/go-crm/internal/handlers/clients.go new file mode 100644 index 0000000..0e36f28 --- /dev/null +++ b/apps/go-crm/internal/handlers/clients.go @@ -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(` + + + Clients + + + +

Clients

+ + + + + + + +`)) + for _, c := range clients { + w.Write([]byte(` + + + + + `)) + } + w.Write([]byte(`
NamePhoneEmailActions
` + c.Name + `` + c.Phone + `` + c.Email + ` + View +
+ +
+
`)) +} + +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(` + + + +

` + client.Name + `

+

Phone: ` + client.Phone + `

+

Email: ` + client.Email + `

+

Address: ` + client.Address + `

+

Notes: ` + client.Notes + `

+ Back +`)) +} + +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")) +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/customers.go b/apps/go-crm/internal/handlers/customers.go new file mode 100644 index 0000000..a690a90 --- /dev/null +++ b/apps/go-crm/internal/handlers/customers.go @@ -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(`

Customers

`)) + for _, c := range customers { + w.Write([]byte(``)) + } + w.Write([]byte(`
NamePhoneBirth DateInstagramActions
` + c.Name + `` + c.Phone + `` + c.BirthDate + `` + c.Instagram + `View
`)) +} + +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(`

` + c.Name + `

Phone: ` + c.Phone + `

Birth Date: ` + c.BirthDate + `

Instagram: ` + c.Instagram + `

Back`)) +} + +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) +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/payments.go b/apps/go-crm/internal/handlers/payments.go new file mode 100644 index 0000000..03cf617 --- /dev/null +++ b/apps/go-crm/internal/handlers/payments.go @@ -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(`

Payments

`)) + for _, p := range payments { + paid := "No" + if p.HasPaid { + paid = "Yes" + } + w.Write([]byte(``)) + } + w.Write([]byte(`
CustomerAmountPaidDateMethodActions
` + strconv.FormatInt(p.CustomerID, 10) + `` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `` + paid + `` + p.PaymentDate + `` + p.PaymentMethod + `View
`)) +} + +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(`

Payment

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

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

Method: ` + p.PaymentMethod + `

Back`)) +} + +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" +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/questions.go b/apps/go-crm/internal/handlers/questions.go new file mode 100644 index 0000000..d88911a --- /dev/null +++ b/apps/go-crm/internal/handlers/questions.go @@ -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(`

Questions

`)) + for _, q := range questions { + w.Write([]byte(``)) + } + w.Write([]byte(`
CustomerQuestionStatusActions
` + strconv.FormatInt(q.CustomerID, 10) + `` + q.Question + `` + q.Status + `View
`)) +} + +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(`

Question

Back`)) +} + +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(`

Answers

`)) + for _, a := range answers { + w.Write([]byte(``)) + } + w.Write([]byte(`
QuestionAnswerStatusActions
` + strconv.FormatInt(a.QuestionID, 10) + `` + a.Answer + `` + a.Status + `View
`)) +} + +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(`

Answer

Back`)) +} + +func UpdateAnswer(w http.ResponseWriter, r *http.Request) {} + +func DeleteAnswer(w http.ResponseWriter, r *http.Request) {} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/scheduling.go b/apps/go-crm/internal/handlers/scheduling.go new file mode 100644 index 0000000..38aff52 --- /dev/null +++ b/apps/go-crm/internal/handlers/scheduling.go @@ -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(`

Scheduling

`)) + for _, s := range schedules { + w.Write([]byte(``)) + } + w.Write([]byte(`
CustomerDateTimeStatusActions
` + strconv.FormatInt(s.CustomerID, 10) + `` + s.PlanDate + `` + s.Time + `` + s.Status + `View
`)) +} + +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(`

Schedule

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

Date: ` + sch.PlanDate + `

Time: ` + sch.Time + `

Status: ` + sch.Status + `

Back`)) +} + +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) +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/services.go b/apps/go-crm/internal/handlers/services.go new file mode 100644 index 0000000..914d6b8 --- /dev/null +++ b/apps/go-crm/internal/handlers/services.go @@ -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(`

Services

`)) + for _, s := range services { + w.Write([]byte(``)) + } + w.Write([]byte(`
NamePriceDurationActions
` + s.Name + `` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `` + s.Duration + `View
`)) +} + +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(`

` + s.Name + `

Price: ` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `

Description: ` + s.Description + `

Duration: ` + s.Duration + `

Back`)) +} + +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) +} \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/setup.go b/apps/go-crm/internal/handlers/setup.go new file mode 100644 index 0000000..3d07b45 --- /dev/null +++ b/apps/go-crm/internal/handlers/setup.go @@ -0,0 +1,14 @@ +package handlers + +import ( + "database/sql" + "time" +) + +func SetupHandlers(db *sql.DB) { + DB = db +} + +func getCurrentTimestamp() int64 { + return time.Now().Unix() +} \ No newline at end of file diff --git a/apps/go-crm/internal/templates/home.html b/apps/go-crm/internal/templates/home.html new file mode 100644 index 0000000..2c4e35f --- /dev/null +++ b/apps/go-crm/internal/templates/home.html @@ -0,0 +1,4 @@ +{{ define "content" }} +

Welcome to CRM

+

Manage your clients, customers, and scheduling.

+{{ end }} \ No newline at end of file diff --git a/apps/go-crm/internal/templates/layout.go b/apps/go-crm/internal/templates/layout.go new file mode 100644 index 0000000..020f807 --- /dev/null +++ b/apps/go-crm/internal/templates/layout.go @@ -0,0 +1,76 @@ +package templates + +import ( + "html/template" + "sync" +) + +var ( + templates *template.Template + mu sync.RWMutex +) + +func Init() { + templates = template.Must(template.New("").ParseGlob("internal/templates/*.html")) +} + +func Layout(title, content string) *template.Template { + mu.RLock() + defer mu.RUnlock() + + tmpl := template.Must(template.New("layout.html").Parse(` + + + + + {{ .Title }} + + + + + +
+ {{ template "content" . }} +
+ +`)) + + return tmpl +} + +func Get(name string) *template.Template { + mu.RLock() + defer mu.RUnlock() + return templates.Lookup(name) +} \ No newline at end of file diff --git a/apps/go-crm/main.go b/apps/go-crm/main.go new file mode 100644 index 0000000..2287ea3 --- /dev/null +++ b/apps/go-crm/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "go-crm/internal/db" + "go-crm/internal/handlers" + "go-crm/internal/templates" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +func main() { + database, err := db.Init("/workspace/data/go-crm.db") + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer database.Close() + + handlers.SetupHandlers(database) + + r := chi.NewRouter() + + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.RequestID) + + templates.Init() + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + templates.Layout("Home", "home.html").Render(r.Context(), w) + }) + + r.Route("/auth", func(r chi.Router) { + r.Get("/signup", handlers.SignupPage) + r.Post("/signup", handlers.Signup) + r.Get("/login", handlers.LoginPage) + r.Post("/login", handlers.Login) + r.Post("/logout", handlers.Logout) + }) + + r.Route("/clients", func(r chi.Router) { + r.Get("/", handlers.ListClients) + r.Post("/", handlers.CreateClient) + r.Get("/{id}", handlers.ViewClient) + r.Put("/{id}", handlers.UpdateClient) + r.Delete("/{id}", handlers.DeleteClient) + }) + + r.Route("/customers", func(r chi.Router) { + r.Get("/", handlers.ListCustomers) + r.Post("/", handlers.CreateCustomer) + r.Get("/{id}", handlers.ViewCustomer) + r.Put("/{id}", handlers.UpdateCustomer) + r.Delete("/{id}", handlers.DeleteCustomer) + }) + + r.Route("/services", func(r chi.Router) { + r.Get("/", handlers.ListServices) + r.Post("/", handlers.CreateService) + r.Get("/{id}", handlers.ViewService) + r.Put("/{id}", handlers.UpdateService) + r.Delete("/{id}", handlers.DeleteService) + }) + + r.Route("/scheduling", func(r chi.Router) { + r.Get("/", handlers.ListSchedules) + r.Post("/", handlers.CreateSchedule) + r.Get("/{id}", handlers.ViewSchedule) + r.Put("/{id}", handlers.UpdateSchedule) + r.Delete("/{id}", handlers.DeleteSchedule) + }) + + r.Route("/payments", func(r chi.Router) { + r.Get("/", handlers.ListPayments) + r.Post("/", handlers.CreatePayment) + r.Get("/{id}", handlers.ViewPayment) + r.Put("/{id}", handlers.UpdatePayment) + r.Delete("/{id}", handlers.DeletePayment) + }) + + r.Route("/questions", func(r chi.Router) { + r.Get("/", handlers.ListQuestions) + r.Post("/", handlers.CreateQuestion) + r.Get("/{id}", handlers.ViewQuestion) + r.Put("/{id}", handlers.UpdateQuestion) + r.Delete("/{id}", handlers.DeleteQuestion) + }) + + r.Route("/answers", func(r chi.Router) { + r.Get("/", handlers.ListAnswers) + r.Post("/", handlers.CreateAnswer) + r.Get("/{id}", handlers.ViewAnswer) + r.Put("/{id}", handlers.UpdateAnswer) + r.Delete("/{id}", handlers.DeleteAnswer) + }) + + fmt.Println("CRM server running on http://localhost:8080") + log.Fatal(http.ListenAndServe(":8080", r)) +} \ No newline at end of file