go-crm improvements

This commit is contained in:
2026-05-01 15:43:13 -03:00
parent ee6673265f
commit 31247a63f6
17 changed files with 416 additions and 63 deletions

Binary file not shown.

View File

@@ -59,6 +59,16 @@ npm run test:coverage # Jest with coverage
```bash
cd apps/whatsapp-reader
# No test script defined
node index.js # Run main script
node sync.js # Run sync script
```
### timesfm-forecast (Python/uv)
```bash
cd apps/timesfm-forecast
uv venv && source .venv/bin/activate # Create & activate venv
uv pip install -e . # Install package
timesfm-app # Run Streamlit app
```
---

Binary file not shown.

Binary file not shown.

BIN
apps/go-crm/go-crm Executable file

Binary file not shown.

View File

@@ -6,6 +6,7 @@ import (
type Client struct {
ClientID int64 `json:"client_id"`
AccountID int64 `json:"account_id"`
Name string `json:"name"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
@@ -59,8 +60,8 @@ type Payment struct {
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,
"INSERT INTO clients (account_id, name, phone, email, address, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
c.AccountID, c.Name, c.Phone, c.Email, c.Address, c.Notes, c.CreatedAt,
)
if err != nil {
return err
@@ -75,29 +76,35 @@ func (c *Client) Create(db *sql.DB) error {
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 = ?",
"SELECT client_id, account_id, name, COALESCE(phone,''), COALESCE(email,''), COALESCE(address,''), COALESCE(notes,''), created_at FROM clients WHERE client_id = ?",
id,
).Scan(&c.ClientID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt)
).Scan(&c.ClientID, &c.AccountID, &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,
"UPDATE clients SET name = ?, phone = ?, email = ?, address = ?, notes = ? WHERE client_id = ? AND account_id = ?",
c.Name, c.Phone, c.Email, c.Address, c.Notes, c.ClientID, c.AccountID,
)
return err
}
func (c *Client) Delete(db *sql.DB, id int64) error {
_, err := db.Exec("DELETE FROM clients WHERE client_id = ?", id)
func (c *Client) Delete(db *sql.DB) error {
_, err := db.Exec("DELETE FROM clients WHERE client_id = ? AND account_id = ?", c.ClientID, c.AccountID)
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,
)
func ListClients(db *sql.DB, accountID int64, limit, offset int) ([]Client, error) {
query := "SELECT client_id, account_id, name, COALESCE(phone,''), COALESCE(email,''), COALESCE(address,''), COALESCE(notes,''), created_at FROM clients"
args := []interface{}{}
if accountID > 0 {
query += " WHERE account_id = ?"
args = append(args, accountID)
}
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
}
@@ -106,7 +113,7 @@ func ListClients(db *sql.DB, limit, offset int) ([]Client, error) {
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 {
if err := rows.Scan(&c.ClientID, &c.AccountID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt); err != nil {
return nil, err
}
clients = append(clients, c)
@@ -114,12 +121,12 @@ func ListClients(db *sql.DB, limit, offset int) ([]Client, error) {
return clients, rows.Err()
}
func GetClientByID(db *sql.DB, id int64) (*Client, error) {
func GetClientByID(db *sql.DB, accountID, 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)
"SELECT client_id, account_id, name, COALESCE(phone,''), COALESCE(email,''), COALESCE(address,''), COALESCE(notes,''), created_at FROM clients WHERE client_id = ? AND account_id = ?",
id, accountID,
).Scan(&c.ClientID, &c.AccountID, &c.Name, &c.Phone, &c.Email, &c.Address, &c.Notes, &c.CreatedAt)
if err != nil {
return nil, err
}
@@ -142,13 +149,24 @@ func (cu *Customer) Create(db *sql.DB) error {
return nil
}
func ListCustomers(db *sql.DB, clientID int64, limit, offset int) ([]Customer, error) {
func ListCustomers(db *sql.DB, accountID, clientID int64, limit, offset int) ([]Customer, error) {
args := []interface{}{}
query := "SELECT customer_id, client_id, name, phone, birth_date, instagram, created_at FROM customers"
var args []interface{}
cond := ""
if clientID > 0 {
query += " WHERE client_id = ?"
cond = "client_id = ?"
args = append(args, clientID)
}
if accountID > 0 {
if cond != "" {
cond += " AND "
}
cond += "client_id IN (SELECT client_id FROM clients WHERE account_id = ?)"
args = append(args, accountID)
}
if cond != "" {
query += " WHERE " + cond
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
@@ -185,13 +203,24 @@ func (s *Service) Create(db *sql.DB) error {
return nil
}
func ListServices(db *sql.DB, clientID int64, limit, offset int) ([]Service, error) {
func ListServices(db *sql.DB, accountID, clientID int64, limit, offset int) ([]Service, error) {
args := []interface{}{}
query := "SELECT service_id, client_id, name, price, description, duration, created_at FROM services"
var args []interface{}
cond := ""
if clientID > 0 {
query += " WHERE client_id = ?"
cond = "client_id = ?"
args = append(args, clientID)
}
if accountID > 0 {
if cond != "" {
cond += " AND "
}
cond += "client_id IN (SELECT client_id FROM clients WHERE account_id = ?)"
args = append(args, accountID)
}
if cond != "" {
query += " WHERE " + cond
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
@@ -430,7 +459,8 @@ type AnswerWithDetails struct {
QuestionText string `json:"question_text"`
}
func ListAnswersWithDetails(db *sql.DB, questionID int64, limit, offset int) ([]AnswerWithDetails, error) {
func ListAnswersWithDetails(db *sql.DB, accountID, questionID int64, limit, offset int) ([]AnswerWithDetails, error) {
args := []interface{}{accountID}
query := `
SELECT
a.answer_id, a.client_id, a.question_id, a.answer, a.timestamp, a.status, a.created_at,
@@ -441,8 +471,7 @@ func ListAnswersWithDetails(db *sql.DB, questionID int64, limit, offset int) ([]
JOIN questions q ON a.question_id = q.question_id
JOIN customers cu ON q.customer_id = cu.customer_id
JOIN clients c ON q.client_id = c.client_id
WHERE 1=1`
var args []interface{}
WHERE c.account_id = ?`
if questionID > 0 {
query += " AND a.question_id = ?"
args = append(args, questionID)

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
_ "github.com/glebarez/sqlite"
)
@@ -27,12 +28,14 @@ CREATE TABLE IF NOT EXISTS sessions (
CREATE TABLE IF NOT EXISTS clients (
client_id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
name TEXT NOT NULL,
phone TEXT,
email TEXT,
address TEXT,
notes TEXT,
created_at INTEGER NOT NULL
created_at INTEGER NOT NULL,
FOREIGN KEY (account_id) REFERENCES accounts(account_id)
);
CREATE TABLE IF NOT EXISTS customers (
@@ -138,5 +141,20 @@ func Init(path string) (*sql.DB, error) {
return nil, fmt.Errorf("failed to create schema: %w", err)
}
if err := migrate(database); err != nil {
return nil, fmt.Errorf("failed to migrate: %w", err)
}
return database, nil
}
func migrate(db *sql.DB) error {
_, err := db.Exec("ALTER TABLE clients ADD COLUMN account_id INTEGER")
if err != nil {
if strings.Contains(err.Error(), "duplicate column name") {
return nil
}
return err
}
return nil
}

View File

@@ -68,7 +68,42 @@ func Signup(w http.ResponseWriter, r *http.Request) {
return
}
http.Redirect(w, r, "/auth/login", http.StatusFound)
var accountID int64
err = DB.QueryRow("SELECT account_id FROM accounts WHERE email = ?", email).Scan(&accountID)
if err != nil {
http.Error(w, "Failed to create account", http.StatusInternalServerError)
return
}
client := struct {
AccountID int64
Name string
CreatedAt int64
}{
AccountID: accountID,
Name: name,
CreatedAt: time.Now().Unix(),
}
_, err = DB.Exec(
"INSERT INTO clients (account_id, name, created_at) VALUES (?, ?, ?)",
client.AccountID, client.Name, client.CreatedAt,
)
if err != nil {
http.Error(w, "Failed to create client", http.StatusInternalServerError)
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, "/", http.StatusFound)
}
func LoginPage(w http.ResponseWriter, r *http.Request) {
@@ -121,7 +156,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: sessionID, Path: "/"})
http.Redirect(w, r, "/clients", http.StatusFound)
http.Redirect(w, r, "/", http.StatusFound)
}
func Logout(w http.ResponseWriter, r *http.Request) {
@@ -158,7 +193,67 @@ func getSession(r *http.Request) (int64, error) {
return accountID, err
}
func GetAccountID(r *http.Request) (int64, error) {
return getSession(r)
}
func requireAuth(w http.ResponseWriter, r *http.Request) (int64, bool) {
accountID, err := getSession(r)
if err != nil {
http.Redirect(w, r, "/auth/login", http.StatusFound)
return 0, false
}
return accountID, true
}
func SetupAuthHandlers(db *sql.DB) {
DB = db
chi.RegisterMethod("GET")
}
func AccountPage(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
var name, email string
err := DB.QueryRow("SELECT name, email FROM accounts WHERE account_id = ?", accountID).Scan(&name, &email)
if err != nil {
http.Error(w, "Account not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><head><title>Account</title></head><body><h1>Account Settings</h1><form method="POST" action="/auth/account">
<p>Name: <input type="text" name="name" value="` + name + `"></p>
<p>Email: <input type="email" value="` + email + `" disabled></p>
<p>New Password: <input type="password" name="password" placeholder="Leave blank to keep current"></p>
<button type="submit">Update</button>
</form>
<a href="/">Back to Dashboard</a></body></html>`))
}
func UpdateAccount(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
name := r.FormValue("name")
password := r.FormValue("password")
if name != "" {
DB.Exec("UPDATE accounts SET name = ? WHERE account_id = ?", name, accountID)
}
if password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err == nil {
DB.Exec("UPDATE accounts SET password = ? WHERE account_id = ?", string(hashedPassword), accountID)
}
}
http.Redirect(w, r, "/auth/account", http.StatusFound)
}

View File

@@ -11,13 +11,18 @@ import (
)
func ListClients(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -81,8 +86,14 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
}
func CreateClient(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
client := db.Client{
AccountID: accountID,
Name: r.FormValue("name"),
Phone: r.FormValue("phone"),
Email: r.FormValue("email"),
@@ -101,8 +112,13 @@ func CreateClient(w http.ResponseWriter, r *http.Request) {
}
func ViewClient(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
client, err := db.GetClientByID(DB, id)
client, err := db.GetClientByID(DB, accountID, id)
if err != nil {
http.Error(w, "Client not found", http.StatusNotFound)
return
@@ -124,10 +140,16 @@ func ViewClient(w http.ResponseWriter, r *http.Request) {
}
func UpdateClient(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
client := db.Client{
ClientID: id,
AccountID: accountID,
Name: r.FormValue("name"),
Phone: r.FormValue("phone"),
Email: r.FormValue("email"),
@@ -144,8 +166,14 @@ func UpdateClient(w http.ResponseWriter, r *http.Request) {
}
func DeleteClient(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err := (&db.Client{}).Delete(DB, id); err != nil {
client := &db.Client{ClientID: id, AccountID: accountID}
if err := client.Delete(DB); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

View File

@@ -11,6 +11,11 @@ import (
)
func ListCustomers(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -18,13 +23,13 @@ func ListCustomers(w http.ResponseWriter, r *http.Request) {
limit = 20
}
customers, err := db.ListCustomers(DB, clientID, limit, offset)
customers, err := db.ListCustomers(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
clients, err := db.ListClients(DB, limit, offset)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -44,8 +49,21 @@ func ListCustomers(w http.ResponseWriter, r *http.Request) {
}
func CreateCustomer(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
var checkID int64
err := DB.QueryRow("SELECT client_id FROM clients WHERE client_id = ? AND account_id = ?", clientID, accountID).Scan(&checkID)
if err != nil {
http.Error(w, "Invalid client", http.StatusBadRequest)
return
}
customer := db.Customer{
ClientID: clientID,
Name: r.FormValue("name"),
@@ -64,6 +82,11 @@ func CreateCustomer(w http.ResponseWriter, r *http.Request) {
}
func ViewCustomer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
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)
@@ -77,6 +100,11 @@ func ViewCustomer(w http.ResponseWriter, r *http.Request) {
}
func UpdateCustomer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
@@ -89,6 +117,11 @@ func UpdateCustomer(w http.ResponseWriter, r *http.Request) {
}
func DeleteCustomer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
DB.Exec("DELETE FROM customers WHERE customer_id = ?", id)
}

View File

@@ -11,6 +11,11 @@ import (
)
func ListPayments(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -24,13 +29,13 @@ func ListPayments(w http.ResponseWriter, r *http.Request) {
return
}
clients, err := db.ListClients(DB, limit, offset)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
customers, err := db.ListCustomers(DB, clientID, limit, offset)
customers, err := db.ListCustomers(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -69,6 +74,11 @@ func ListPayments(w http.ResponseWriter, r *http.Request) {
}
func CreatePayment(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
@@ -96,6 +106,11 @@ func CreatePayment(w http.ResponseWriter, r *http.Request) {
}
func ViewPayment(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
var p db.Payment
var hasPaid int
@@ -111,6 +126,11 @@ func ViewPayment(w http.ResponseWriter, r *http.Request) {
}
func UpdatePayment(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
@@ -126,6 +146,11 @@ func UpdatePayment(w http.ResponseWriter, r *http.Request) {
}
func DeletePayment(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
DB.Exec("DELETE FROM payments WHERE payment_id = ?", id)
}

View File

@@ -11,6 +11,11 @@ import (
)
func ListQuestions(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -24,13 +29,13 @@ func ListQuestions(w http.ResponseWriter, r *http.Request) {
return
}
clients, err := db.ListClients(DB, limit, offset)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
customers, err := db.ListCustomers(DB, clientID, limit, offset)
customers, err := db.ListCustomers(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -65,6 +70,11 @@ func ListQuestions(w http.ResponseWriter, r *http.Request) {
}
func CreateQuestion(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
@@ -86,6 +96,11 @@ func CreateQuestion(w http.ResponseWriter, r *http.Request) {
}
func ViewQuestion(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
var q db.Question
err := DB.QueryRow("SELECT question_id, client_id, customer_id, question, timestamp, status, created_at FROM questions WHERE question_id = ?", id).Scan(&q.QuestionID, &q.ClientID, &q.CustomerID, &q.Question, &q.Timestamp, &q.Status, &q.CreatedAt)
@@ -99,6 +114,11 @@ func ViewQuestion(w http.ResponseWriter, r *http.Request) {
}
func UpdateQuestion(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
@@ -112,11 +132,21 @@ func UpdateQuestion(w http.ResponseWriter, r *http.Request) {
}
func DeleteQuestion(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
DB.Exec("DELETE FROM questions WHERE question_id = ?", id)
}
func ListAnswers(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -124,7 +154,7 @@ func ListAnswers(w http.ResponseWriter, r *http.Request) {
limit = 20
}
answers, err := db.ListAnswersWithDetails(DB, questionID, limit, offset)
answers, err := db.ListAnswersWithDetails(DB, accountID, questionID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -224,6 +254,11 @@ func ListAnswers(w http.ResponseWriter, r *http.Request) {
}
func CreateAnswer(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
questionID, _ := strconv.ParseInt(r.FormValue("question_id"), 10, 64)
@@ -234,6 +269,13 @@ func CreateAnswer(w http.ResponseWriter, r *http.Request) {
return
}
var checkID int64
err = DB.QueryRow("SELECT client_id FROM clients WHERE client_id = ? AND account_id = ?", clientID, accountID).Scan(&checkID)
if err != nil {
http.Error(w, "Invalid question", http.StatusBadRequest)
return
}
answer := db.Answer{
ClientID: clientID,
QuestionID: questionID,
@@ -252,6 +294,11 @@ func CreateAnswer(w http.ResponseWriter, r *http.Request) {
}
func ViewAnswer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
var a db.Answer
err := DB.QueryRow("SELECT answer_id, client_id, question_id, answer, timestamp, status, created_at FROM answers WHERE answer_id = ?", id).Scan(&a.AnswerID, &a.ClientID, &a.QuestionID, &a.Answer, &a.Timestamp, &a.Status, &a.CreatedAt)
@@ -265,6 +312,11 @@ func ViewAnswer(w http.ResponseWriter, r *http.Request) {
}
func UpdateAnswer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
questionID, _ := strconv.ParseInt(r.FormValue("question_id"), 10, 64)
@@ -277,6 +329,11 @@ func UpdateAnswer(w http.ResponseWriter, r *http.Request) {
}
func DeleteAnswer(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
DB.Exec("DELETE FROM answers WHERE answer_id = ?", id)
}

View File

@@ -11,6 +11,11 @@ import (
)
func ListSchedules(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -25,19 +30,19 @@ func ListSchedules(w http.ResponseWriter, r *http.Request) {
return
}
clients, err := db.ListClients(DB, limit, offset)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
customers, err := db.ListCustomers(DB, clientID, limit, offset)
customers, err := db.ListCustomers(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
services, err := db.ListServices(DB, clientID, limit, offset)
services, err := db.ListServices(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -81,6 +86,11 @@ func ListSchedules(w http.ResponseWriter, r *http.Request) {
}
func CreateSchedule(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
@@ -104,6 +114,11 @@ func CreateSchedule(w http.ResponseWriter, r *http.Request) {
}
func ViewSchedule(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
var sch db.Schedule
err := DB.QueryRow("SELECT schedule_id, client_id, customer_id, service_id, plan_date, time, status, created_at FROM scheduling WHERE schedule_id = ?", id).Scan(&sch.ScheduleID, &sch.ClientID, &sch.CustomerID, &sch.ServiceID, &sch.PlanDate, &sch.Time, &sch.Status, &sch.CreatedAt)
@@ -117,6 +132,11 @@ func ViewSchedule(w http.ResponseWriter, r *http.Request) {
}
func UpdateSchedule(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
@@ -131,6 +151,11 @@ func UpdateSchedule(w http.ResponseWriter, r *http.Request) {
}
func DeleteSchedule(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
DB.Exec("DELETE FROM scheduling WHERE schedule_id = ?", id)
}

View File

@@ -11,6 +11,11 @@ import (
)
func ListServices(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
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)
@@ -18,13 +23,13 @@ func ListServices(w http.ResponseWriter, r *http.Request) {
limit = 20
}
services, err := db.ListServices(DB, clientID, limit, offset)
services, err := db.ListServices(DB, accountID, clientID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
clients, err := db.ListClients(DB, limit, offset)
clients, err := db.ListClients(DB, accountID, limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -44,8 +49,21 @@ func ListServices(w http.ResponseWriter, r *http.Request) {
}
func CreateService(w http.ResponseWriter, r *http.Request) {
accountID, ok := requireAuth(w, r)
if !ok {
return
}
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
var checkID int64
err := DB.QueryRow("SELECT client_id FROM clients WHERE client_id = ? AND account_id = ?", clientID, accountID).Scan(&checkID)
if err != nil {
http.Error(w, "Invalid client", http.StatusBadRequest)
return
}
price, _ := strconv.ParseFloat(r.FormValue("price"), 64)
service := db.Service{
ClientID: clientID,
@@ -65,6 +83,11 @@ func CreateService(w http.ResponseWriter, r *http.Request) {
}
func ViewService(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
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)
@@ -78,6 +101,11 @@ func ViewService(w http.ResponseWriter, r *http.Request) {
}
func UpdateService(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
r.ParseForm()
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
@@ -91,6 +119,11 @@ func UpdateService(w http.ResponseWriter, r *http.Request) {
}
func DeleteService(w http.ResponseWriter, r *http.Request) {
_, ok := requireAuth(w, r)
if !ok {
return
}
id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
DB.Exec("DELETE FROM services WHERE service_id = ?", id)
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log"
"net/http"
"strings"
"go-crm/internal/db"
"go-crm/internal/handlers"
@@ -32,15 +31,14 @@ func main() {
templates.Init()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
homeTmpl := templates.Get("home.html")
layoutTmpl := templates.Layout("Home", "")
if homeTmpl != nil && layoutTmpl != nil {
buf := &strings.Builder{}
homeTmpl.Execute(buf, map[string]string{"Title": "Home"})
layoutTmpl.Execute(w, map[string]string{"Title": "Home", "Content": buf.String()})
} else {
w.Write([]byte(`<!DOCTYPE html><html><head><title>Home</title></head><body><h1>Welcome to CRM</h1><nav><a href="/clients">Clients</a> | <a href="/customers">Customers</a> | <a href="/services">Services</a> | <a href="/questions">Questions</a> | <a href="/answers">Answers</a></nav></body></html>`))
_, err := handlers.GetAccountID(r)
if err != nil {
http.Redirect(w, r, "/auth/login", http.StatusFound)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(`<!DOCTYPE html><html><head><title>Dashboard</title></head><body><h1>Welcome to CRM</h1><nav><a href="/clients">Clients</a> | <a href="/customers">Customers</a> | <a href="/services">Services</a> | <a href="/scheduling">Scheduling</a> | <a href="/payments">Payments</a> | <a href="/questions">Questions</a> | <a href="/answers">Answers</a> | <a href="/auth/account">Account</a> | <form method="POST" action="/auth/logout" style="display:inline"><button type="submit">Logout</button></form></nav></body></html>`))
})
r.Route("/auth", func(r chi.Router) {
@@ -49,6 +47,8 @@ func main() {
r.Get("/login", handlers.LoginPage)
r.Post("/login", handlers.Login)
r.Post("/logout", handlers.Logout)
r.Get("/account", handlers.AccountPage)
r.Post("/account", handlers.UpdateAccount)
})
r.Route("/clients", func(r chi.Router) {

View File

@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

Binary file not shown.