diff --git a/.gitignore b/.gitignore index 6f3435c..7235a64 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ Thumbs.db .opencode-sandbox/.env + +# Go CRM data +data/*.db diff --git a/.opencode-sandbox/Dockerfile b/.opencode-sandbox/Dockerfile index 0882947..0d6e347 100644 --- a/.opencode-sandbox/Dockerfile +++ b/.opencode-sandbox/Dockerfile @@ -10,10 +10,17 @@ RUN pacman -Syu --noconfirm && \ openssh \ opencode \ curl \ + go \ + sqlite \ && pacman -Scc --noconfirm RUN npx -y skills add JuliusBrussee/caveman -g -a opencode -s caveman -y +# 2. Install air using Go +RUN go install github.com/air-verse/air@latest + +# Add Go binaries to your PATH so you can actually run 'air' +ENV PATH="/root/go/bin:${PATH}" # Git safe directory system-wide config RUN git config --system --add safe.directory /workspace diff --git a/.opencode-sandbox/data/go-crm.db b/.opencode-sandbox/data/go-crm.db new file mode 100644 index 0000000..ac4711f Binary files /dev/null and b/.opencode-sandbox/data/go-crm.db differ diff --git a/.opencode-sandbox/docker-compose.yml b/.opencode-sandbox/docker-compose.yml index df48f93..a225967 100644 --- a/.opencode-sandbox/docker-compose.yml +++ b/.opencode-sandbox/docker-compose.yml @@ -15,11 +15,22 @@ services: - /home/ga/.ssh:/root/.ssh # Persist opencode auth - opencode-auth:/root/.local/share/opencode + # Go CRM database + - ./data:/workspace/data environment: - OPENCODE_API_KEY=${OPENCODE_API_KEY} stdin_open: true tty: true + ports: + - "8080:8080" working_dir: /workspace + networks: + - crm-network + +networks: + crm-network: + name: crm-network + external: true volumes: - opencode-auth: + opencode-auth: \ No newline at end of file diff --git a/apps/go-crm/.air.toml b/apps/go-crm/.air.toml new file mode 100644 index 0000000..014d929 --- /dev/null +++ b/apps/go-crm/.air.toml @@ -0,0 +1,35 @@ +root_dir = "." +tmp_dir = "tmp" + +[build] + bin = "./tmp/main" + cmd = "go build -buildvcs=false -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_screen = false + include_dir = [] + include_ext = ["go", "templ"] + kill_delay = "0s" + log = "build-errors.toml" + send_exit = false + send_user = false + stop_on_error = false + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + dao = "0x60" + force_kill = true + kill = true + restart = true + +[screen] + clear_on_rebuild = false + extra_resize = false \ No newline at end of file diff --git a/apps/go-crm/Dockerfile b/apps/go-crm/Dockerfile new file mode 100644 index 0000000..cc7833c --- /dev/null +++ b/apps/go-crm/Dockerfile @@ -0,0 +1,26 @@ +FROM archlinux:latest + +RUN pacman -Syu --noconfirm && \ + pacman -S --noconfirm \ + go \ + git \ + sqlite \ + curl \ + && pacman -Scc --noconfirm + +WORKDIR /tmp + +RUN curl -sSL https://github.com/air-verse/air/releases/download/v1.27.10/air_1.27.10_linux_amd64.tar.gz | tar -xz && \ + chmod +x air && \ + mv air /usr/local/bin/air + +WORKDIR /workspace/apps/go-crm + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +EXPOSE 8080 + +CMD ["air"] \ No newline at end of file diff --git a/apps/go-crm/data/go-crm.db b/apps/go-crm/data/go-crm.db new file mode 100644 index 0000000..a8e422c Binary files /dev/null and b/apps/go-crm/data/go-crm.db differ diff --git a/apps/go-crm/docker-compose.yml b/apps/go-crm/docker-compose.yml new file mode 100644 index 0000000..2ebf3b0 --- /dev/null +++ b/apps/go-crm/docker-compose.yml @@ -0,0 +1,18 @@ +services: + go-crm: + build: . + container_name: go-crm + ports: + - "8888:8080" + volumes: + - /home/ga/workspace:/workspace + - ./data:/workspace/data + networks: + - crm-network + working_dir: /workspace/apps/go-crm + command: air + restart: unless-stopped + +networks: + crm-network: + external: false \ No newline at end of file diff --git a/apps/go-crm/go.mod b/apps/go-crm/go.mod index bbb8ffe..d03912c 100644 --- a/apps/go-crm/go.mod +++ b/apps/go-crm/go.mod @@ -3,12 +3,23 @@ module go-crm go 1.22 require ( + github.com/glebarez/sqlite v1.11.0 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 + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.25.0 // indirect + gorm.io/gorm v1.25.7 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/apps/go-crm/go.sum b/apps/go-crm/go.sum new file mode 100644 index 0000000..e61f4e4 --- /dev/null +++ b/apps/go-crm/go.sum @@ -0,0 +1,36 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/apps/go-crm/internal/db/crud.go b/apps/go-crm/internal/db/crud.go index 05a424d..3f33570 100644 --- a/apps/go-crm/internal/db/crud.go +++ b/apps/go-crm/internal/db/crud.go @@ -2,7 +2,6 @@ package db import ( "database/sql" - "fmt" ) type Client struct { @@ -36,15 +35,14 @@ type Service struct { } 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"` + ScheduleID int64 `json:"schedule_id"` + ClientID int64 `json:"client_id"` + CustomerID int64 `json:"customer_id"` + ServiceID int64 `json:"service_id"` + PlanDate string `json:"plan_date,omitempty"` + Time string `json:"time,omitempty"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` } type Payment struct { @@ -59,26 +57,6 @@ type Payment struct { 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 (?, ?, ?, ?, ?, ?)", @@ -236,8 +214,8 @@ func ListServices(db *sql.DB, clientID int64, limit, offset int) ([]Service, 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, + "INSERT INTO scheduling (client_id, customer_id, service_id, plan_date, time, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + sch.ClientID, sch.CustomerID, sch.ServiceID, sch.PlanDate, sch.Time, sch.Status, sch.CreatedAt, ) if err != nil { return err @@ -251,7 +229,7 @@ func (sch *Schedule) Create(db *sql.DB) error { } 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" + query := "SELECT schedule_id, client_id, customer_id, service_id, plan_date, time, status, created_at FROM scheduling WHERE 1=1" var args []interface{} if clientID > 0 { query += " AND client_id = ?" @@ -273,7 +251,7 @@ func ListSchedules(db *sql.DB, clientID, customerID int64, limit, offset int) ([ 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 { + if err := rows.Scan(&sch.ScheduleID, &sch.ClientID, &sch.CustomerID, &sch.ServiceID, &sch.PlanDate, &sch.Time, &sch.Status, &sch.CreatedAt); err != nil { return nil, err } schedules = append(schedules, sch) @@ -326,6 +304,23 @@ func ListPayments(db *sql.DB, clientID int64, limit, offset int) ([]Payment, err return payments, rows.Err() } +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +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"` +} + 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 (?, ?, ?, ?, ?, ?)", @@ -343,10 +338,10 @@ func (q *Question) Create(db *sql.DB) error { } 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" + query := "SELECT question_id, client_id, customer_id, question, timestamp, status, created_at FROM questions WHERE 1=1" var args []interface{} if clientID > 0 { - query += " WHERE client_id = ?" + query += " AND client_id = ?" args = append(args, clientID) } query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" @@ -369,9 +364,19 @@ func ListQuestions(db *sql.DB, clientID int64, limit, offset int) ([]Question, e return questions, rows.Err() } +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 (a *Answer) Create(db *sql.DB) error { result, err := db.Exec( - "INSERT INTO answers (client_id, question_id, answer, timestamp, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + "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 { @@ -386,10 +391,10 @@ func (a *Answer) Create(db *sql.DB) error { } 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" + query := "SELECT answer_id, client_id, question_id, answer, timestamp, status, created_at FROM answers WHERE 1=1" var args []interface{} if questionID > 0 { - query += " WHERE question_id = ?" + query += " AND question_id = ?" args = append(args, questionID) } query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" @@ -412,9 +417,52 @@ func ListAnswers(db *sql.DB, questionID int64, limit, offset int) ([]Answer, err return answers, rows.Err() } -func boolToInt(b bool) int { - if b { - return 1 +type AnswerWithDetails 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"` + ClientName string `json:"client_name"` + CustomerName string `json:"customer_name"` + QuestionText string `json:"question_text"` +} + +func ListAnswersWithDetails(db *sql.DB, questionID int64, limit, offset int) ([]AnswerWithDetails, error) { + query := ` + SELECT + a.answer_id, a.client_id, a.question_id, a.answer, a.timestamp, a.status, a.created_at, + c.name as client_name, + cu.name as customer_name, + q.question as question_text + FROM answers a + 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{} + if questionID > 0 { + query += " AND a.question_id = ?" + args = append(args, questionID) } - return 0 + query += " ORDER BY a.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 []AnswerWithDetails + for rows.Next() { + var a AnswerWithDetails + if err := rows.Scan(&a.AnswerID, &a.ClientID, &a.QuestionID, &a.Answer, &a.Timestamp, &a.Status, &a.CreatedAt, &a.ClientName, &a.CustomerName, &a.QuestionText); err != nil { + return nil, err + } + answers = append(answers, a) + } + return answers, rows.Err() } \ No newline at end of file diff --git a/apps/go-crm/internal/db/db.go b/apps/go-crm/internal/db/db.go index 463c473..a9972f1 100644 --- a/apps/go-crm/internal/db/db.go +++ b/apps/go-crm/internal/db/db.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - _ "github.com/mattn/go-sqlite3" + _ "github.com/glebarez/sqlite" ) const schema = ` @@ -59,16 +59,16 @@ CREATE TABLE IF NOT EXISTS services ( CREATE TABLE IF NOT EXISTS scheduling ( schedule_id INTEGER PRIMARY KEY AUTOINCREMENT, - customer_id INTEGER NOT NULL, client_id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + service_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 (client_id) REFERENCES clients(client_id), FOREIGN KEY (customer_id) REFERENCES customers(customer_id), - FOREIGN KEY (client_id) REFERENCES clients(client_id) + FOREIGN KEY (service_id) REFERENCES services(service_id) ); CREATE TABLE IF NOT EXISTS payments ( @@ -109,9 +109,11 @@ CREATE TABLE IF NOT EXISTS answers ( 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") +var dbPath = "data/go-crm.db" func Init(path string) (*sql.DB, error) { if path != "" { @@ -123,7 +125,7 @@ func Init(path string) (*sql.DB, error) { return nil, fmt.Errorf("failed to create data directory: %w", err) } - database, err := sql.Open("sqlite3", dbPath) + database, err := sql.Open("sqlite", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } diff --git a/apps/go-crm/internal/handlers/auth.go b/apps/go-crm/internal/handlers/auth.go index 20ba7e5..f30e819 100644 --- a/apps/go-crm/internal/handlers/auth.go +++ b/apps/go-crm/internal/handlers/auth.go @@ -5,8 +5,6 @@ import ( "net/http" "time" - "go-crm/internal/db" - "github.com/go-chi/chi/v5" "golang.org/x/crypto/bcrypt" ) @@ -61,7 +59,7 @@ func Signup(w http.ResponseWriter, r *http.Request) { CreatedAt: time.Now().Unix(), } - _, err = db.DB.Exec( + _, err = DB.Exec( "INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)", account.Email, account.Name, account.Password, account.CreatedAt, ) diff --git a/apps/go-crm/internal/handlers/clients.go b/apps/go-crm/internal/handlers/clients.go index 0e36f28..fed28c3 100644 --- a/apps/go-crm/internal/handlers/clients.go +++ b/apps/go-crm/internal/handlers/clients.go @@ -1,8 +1,6 @@ package handlers import ( - "database/sql" - "encoding/json" "net/http" "strconv" "time" @@ -31,6 +29,10 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
Client ID: ` + strconv.FormatInt(client.ClientID, 10) + `
Phone: ` + client.Phone + `
Email: ` + client.Email + `
Address: ` + client.Address + `
@@ -110,7 +124,7 @@ func ViewClient(w http.ResponseWriter, r *http.Request) { } func UpdateClient(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) r.ParseForm() client := db.Client{ ClientID: id, @@ -126,12 +140,11 @@ func UpdateClient(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(client) + w.Header().Set("HX-Refresh", "true") } func DeleteClient(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) if err := (&db.Client{}).Delete(DB, id); 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 a690a90..97360a1 100644 --- a/apps/go-crm/internal/handlers/customers.go +++ b/apps/go-crm/internal/handlers/customers.go @@ -1,8 +1,6 @@ package handlers import ( - "database/sql" - "encoding/json" "net/http" "strconv" "time" @@ -26,10 +24,21 @@ func ListCustomers(w http.ResponseWriter, r *http.Request) { return } + clients, err := db.ListClients(DB, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var clientOptions string + for _, c := range clients { + clientOptions += `` + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`| Name | Phone | Birth Date | Actions |
|---|
| Name | Phone | Birth Date | Actions | |
|---|---|---|---|---|
| ` + c.Name + ` | ` + c.Phone + ` | ` + c.BirthDate + ` | ` + c.Instagram + ` | View |
| ` + c.Name + ` | ` + c.Phone + ` | ` + c.BirthDate + ` | ` + c.Instagram + ` | View |
| Customer | Amount | Paid | Date | Method | Actions |
|---|
| Client | Customer | Amount | Paid | Date | Method | Actions |
|---|---|---|---|---|---|---|
| ` + strconv.FormatInt(p.CustomerID, 10) + ` | ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + ` | ` + paid + ` | ` + p.PaymentDate + ` | ` + p.PaymentMethod + ` | View | |
| ` + clientName + ` | ` + customerName + ` | ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + ` | ` + paid + ` | ` + p.PaymentDate + ` | ` + p.PaymentMethod + ` | View |
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 UpdatePayment(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + r.ParseForm() + clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) + customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64) + amount, _ := strconv.ParseFloat(r.FormValue("amount"), 64) + hasPaid := r.FormValue("has_paid") == "on" + _, err := DB.Exec("UPDATE payments SET client_id=?, customer_id=?, has_paid=?, amount=?, payment_date=?, payment_method=? WHERE payment_id=?", clientID, customerID, hasPaid, amount, r.FormValue("payment_date"), r.FormValue("payment_method"), id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("HX-Refresh", "true") +} -func DeletePayment(w http.ResponseWriter, r *http.Request) {} +func DeletePayment(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) + DB.Exec("DELETE FROM payments WHERE payment_id = ?", id) +} func boolToStr(b bool) string { if b { diff --git a/apps/go-crm/internal/handlers/questions.go b/apps/go-crm/internal/handlers/questions.go index d88911a..d2d4185 100644 --- a/apps/go-crm/internal/handlers/questions.go +++ b/apps/go-crm/internal/handlers/questions.go @@ -6,6 +6,8 @@ import ( "time" "go-crm/internal/db" + + "github.com/go-chi/chi/v5" ) func ListQuestions(w http.ResponseWriter, r *http.Request) { @@ -22,10 +24,42 @@ func ListQuestions(w http.ResponseWriter, r *http.Request) { return } + clients, err := db.ListClients(DB, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + customers, err := db.ListCustomers(DB, clientID, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var clientOptions, customerOptions string + clientNames := make(map[int64]string) + customerNames := make(map[int64]string) + for _, c := range clients { + clientOptions += `` + clientNames[c.ClientID] = c.Name + } + for _, cu := range customers { + customerOptions += `` + customerNames[cu.CustomerID] = cu.Name + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`| Customer | Question | Status | Actions |
|---|
| Client | Customer | Question | Status | Actions |
|---|---|---|---|---|
| ` + strconv.FormatInt(q.CustomerID, 10) + ` | ` + q.Question + ` | ` + q.Status + ` | View | |
| ` + clientName + ` | ` + customerName + ` | ` + q.Question + ` | ` + q.Status + ` | View |
Client ID: ` + strconv.FormatInt(q.ClientID, 10) + `
Customer ID: ` + strconv.FormatInt(q.CustomerID, 10) + `
Question: ` + q.Question + `
Status: ` + q.Status + `
Back`)) } -func UpdateQuestion(w http.ResponseWriter, r *http.Request) {} +func UpdateQuestion(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + r.ParseForm() + clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) + customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64) + _, err := DB.Exec("UPDATE questions SET client_id=?, customer_id=?, question=?, status=? WHERE question_id=?", clientID, customerID, r.FormValue("question"), r.FormValue("status"), id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("HX-Refresh", "true") +} -func DeleteQuestion(w http.ResponseWriter, r *http.Request) {} +func DeleteQuestion(w http.ResponseWriter, r *http.Request) { + 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) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) @@ -68,31 +124,123 @@ func ListAnswers(w http.ResponseWriter, r *http.Request) { limit = 20 } - answers, err := db.ListAnswers(DB, questionID, limit, offset) + answers, err := db.ListAnswersWithDetails(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(`| Question | Answer | Status | Actions |
|---|---|---|---|
| ` + strconv.FormatInt(a.QuestionID, 10) + ` | ` + a.Answer + ` | ` + a.Status + ` | View |
| Client | Customer | Question | Answer | Status | Actions |
|---|---|---|---|---|---|
| ` + clientName + ` | +` + customerName + ` | +` + qText + ` | +` + a.Answer + ` | +` + a.Status + ` | ++ View + + + | +
Question ID: ` + strconv.FormatInt(a.QuestionID, 10) + `
Answer: ` + a.Answer + `
Back`)) } -func UpdateAnswer(w http.ResponseWriter, r *http.Request) {} +func UpdateAnswer(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) + r.ParseForm() + questionID, _ := strconv.ParseInt(r.FormValue("question_id"), 10, 64) + _, err := DB.Exec("UPDATE answers SET question_id=?, answer=? WHERE answer_id=?", questionID, r.FormValue("answer"), id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("HX-Refresh", "true") +} -func DeleteAnswer(w http.ResponseWriter, r *http.Request) {} \ No newline at end of file +func DeleteAnswer(w http.ResponseWriter, r *http.Request) { + 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 38aff52..96e07d9 100644 --- a/apps/go-crm/internal/handlers/scheduling.go +++ b/apps/go-crm/internal/handlers/scheduling.go @@ -1,7 +1,6 @@ package handlers import ( - "database/sql" "net/http" "strconv" "time" @@ -26,27 +25,74 @@ func ListSchedules(w http.ResponseWriter, r *http.Request) { return } + clients, err := db.ListClients(DB, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + customers, err := db.ListCustomers(DB, clientID, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + services, err := db.ListServices(DB, clientID, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var clientOptions, customerOptions, serviceOptions string + clientNames := make(map[int64]string) + customerNames := make(map[int64]string) + serviceNames := make(map[int64]string) + for _, c := range clients { + clientOptions += `` + clientNames[c.ClientID] = c.Name + } + for _, cu := range customers { + customerOptions += `` + customerNames[cu.CustomerID] = cu.Name + } + for _, s := range services { + serviceOptions += `` + serviceNames[s.ServiceID] = s.Name + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`| Customer | Date | Time | Status | Actions |
|---|
| Client | Customer | Service | Date | Hour | Status | Actions |
|---|---|---|---|---|---|---|
| ` + strconv.FormatInt(s.CustomerID, 10) + ` | ` + s.PlanDate + ` | ` + s.Time + ` | ` + s.Status + ` | View | ||
| ` + clientName + ` | ` + customerName + ` | ` + serviceName + ` | ` + s.PlanDate + ` | ` + s.Time + ` | ` + s.Status + ` | View |
Customer ID: ` + strconv.FormatInt(sch.CustomerID, 10) + `
Date: ` + sch.PlanDate + `
Time: ` + sch.Time + `
Status: ` + sch.Status + `
Back`)) + w.Write([]byte(`Client ID: ` + strconv.FormatInt(sch.ClientID, 10) + `
Customer ID: ` + strconv.FormatInt(sch.CustomerID, 10) + `
Service ID: ` + strconv.FormatInt(sch.ServiceID, 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) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) 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) + customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64) + serviceID, _ := strconv.ParseInt(r.FormValue("service_id"), 10, 64) + _, err := DB.Exec("UPDATE scheduling SET client_id=?, customer_id=?, service_id=?, plan_date=?, time=?, status=? WHERE schedule_id=?", clientID, customerID, serviceID, r.FormValue("plan_date"), r.FormValue("time"), r.FormValue("status"), id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("HX-Refresh", "true") } func DeleteSchedule(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + 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 914d6b8..309a3a4 100644 --- a/apps/go-crm/internal/handlers/services.go +++ b/apps/go-crm/internal/handlers/services.go @@ -1,7 +1,6 @@ package handlers import ( - "database/sql" "net/http" "strconv" "time" @@ -25,10 +24,21 @@ func ListServices(w http.ResponseWriter, r *http.Request) { return } + clients, err := db.ListClients(DB, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var clientOptions string + for _, c := range clients { + clientOptions += `` + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`| Name | Price | Duration | Actions |
|---|
| Name | Price | Duration | Actions |
|---|---|---|---|
| ` + s.Name + ` | ` + strconv.FormatFloat(s.Price, 'f', 2, 64) + ` | ` + s.Duration + ` | View |
| ` + s.Name + ` | ` + strconv.FormatFloat(s.Price, 'f', 2, 64) + ` | ` + s.Duration + ` | View |