diff --git a/.opencode-sandbox/data/go-crm.db b/.opencode-sandbox/data/go-crm.db index ac4711f..26ff913 100644 Binary files a/.opencode-sandbox/data/go-crm.db and b/.opencode-sandbox/data/go-crm.db differ diff --git a/AGENTS.md b/AGENTS.md index 5fccf4e..c6060f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ``` --- diff --git a/apps/go-crm/data/go-crm.db b/apps/go-crm/data/go-crm.db index a8e422c..0490b0c 100644 Binary files a/apps/go-crm/data/go-crm.db and b/apps/go-crm/data/go-crm.db differ diff --git a/apps/go-crm/data/go-crm.db.bak b/apps/go-crm/data/go-crm.db.bak new file mode 100644 index 0000000..a8e422c Binary files /dev/null and b/apps/go-crm/data/go-crm.db.bak differ diff --git a/apps/go-crm/go-crm b/apps/go-crm/go-crm new file mode 100755 index 0000000..ad77618 Binary files /dev/null and b/apps/go-crm/go-crm differ diff --git a/apps/go-crm/internal/db/crud.go b/apps/go-crm/internal/db/crud.go index 3f33570..0429188 100644 --- a/apps/go-crm/internal/db/crud.go +++ b/apps/go-crm/internal/db/crud.go @@ -5,13 +5,14 @@ import ( ) 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"` + ClientID int64 `json:"client_id"` + AccountID int64 `json:"account_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 { @@ -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) diff --git a/apps/go-crm/internal/db/db.go b/apps/go-crm/internal/db/db.go index a9972f1..4aa3935 100644 --- a/apps/go-crm/internal/db/db.go +++ b/apps/go-crm/internal/db/db.go @@ -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 } \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/auth.go b/apps/go-crm/internal/handlers/auth.go index f30e819..c7c2025 100644 --- a/apps/go-crm/internal/handlers/auth.go +++ b/apps/go-crm/internal/handlers/auth.go @@ -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(`Account

Account Settings

+

Name:

+

Email:

+

New Password:

+ +
+ Back to Dashboard`)) +} + +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) } \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/clients.go b/apps/go-crm/internal/handlers/clients.go index fed28c3..969d613 100644 --- a/apps/go-crm/internal/handlers/clients.go +++ b/apps/go-crm/internal/handlers/clients.go @@ -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, + 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 } diff --git a/apps/go-crm/internal/handlers/customers.go b/apps/go-crm/internal/handlers/customers.go index 97360a1..220da62 100644 --- a/apps/go-crm/internal/handlers/customers.go +++ b/apps/go-crm/internal/handlers/customers.go @@ -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) } \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/payments.go b/apps/go-crm/internal/handlers/payments.go index a5f6430..a652a7d 100644 --- a/apps/go-crm/internal/handlers/payments.go +++ b/apps/go-crm/internal/handlers/payments.go @@ -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) } diff --git a/apps/go-crm/internal/handlers/questions.go b/apps/go-crm/internal/handlers/questions.go index d2d4185..309367d 100644 --- a/apps/go-crm/internal/handlers/questions.go +++ b/apps/go-crm/internal/handlers/questions.go @@ -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) } \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/scheduling.go b/apps/go-crm/internal/handlers/scheduling.go index 96e07d9..cf47c3d 100644 --- a/apps/go-crm/internal/handlers/scheduling.go +++ b/apps/go-crm/internal/handlers/scheduling.go @@ -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) } \ No newline at end of file diff --git a/apps/go-crm/internal/handlers/services.go b/apps/go-crm/internal/handlers/services.go index 309a3a4..8c73299 100644 --- a/apps/go-crm/internal/handlers/services.go +++ b/apps/go-crm/internal/handlers/services.go @@ -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) } \ No newline at end of file diff --git a/apps/go-crm/main.go b/apps/go-crm/main.go index 5578276..d6a4d5d 100644 --- a/apps/go-crm/main.go +++ b/apps/go-crm/main.go @@ -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(`Home

Welcome to CRM

`)) + _, 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(`Dashboard

Welcome to CRM

`)) }) 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) { diff --git a/apps/go-crm/tmp/build-errors.toml b/apps/go-crm/tmp/build-errors.toml index 481923f..94b18bd 100644 --- a/apps/go-crm/tmp/build-errors.toml +++ b/apps/go-crm/tmp/build-errors.toml @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/apps/go-crm/tmp/main b/apps/go-crm/tmp/main index 0cf8656..5038eac 100755 Binary files a/apps/go-crm/tmp/main and b/apps/go-crm/tmp/main differ