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) { Clients +

Clients

@@ -58,9 +60,20 @@ func ListClients(w http.ResponseWriter, r *http.Request) { ` + c.Email + ` View +
+ +
+ + + + + + +
+ `)) } @@ -88,7 +101,7 @@ func CreateClient(w http.ResponseWriter, r *http.Request) { } func ViewClient(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) client, err := db.GetClientByID(DB, id) if err != nil { http.Error(w, "Client not found", http.StatusNotFound) @@ -101,6 +114,7 @@ func ViewClient(w http.ResponseWriter, r *http.Request) {

` + client.Name + `

+

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(`

Customers

`)) + w.Write([]byte(`

Customers

NamePhoneBirth DateInstagramActions
`)) for _, c := range customers { - w.Write([]byte(``)) + w.Write([]byte(``)) } w.Write([]byte(`
NamePhoneBirth DateInstagramActions
` + c.Name + `` + c.Phone + `` + c.BirthDate + `` + c.Instagram + `View
` + c.Name + `` + c.Phone + `` + c.BirthDate + `` + c.Instagram + `View
`)) } @@ -55,7 +64,7 @@ func CreateCustomer(w http.ResponseWriter, r *http.Request) { } func ViewCustomer(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + 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) if err != nil { @@ -68,7 +77,7 @@ func ViewCustomer(w http.ResponseWriter, r *http.Request) { } func UpdateCustomer(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() clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) _, err := DB.Exec("UPDATE customers SET client_id=?, name=?, phone=?, birth_date=?, instagram=? WHERE customer_id=?", clientID, r.FormValue("name"), r.FormValue("phone"), r.FormValue("birth_date"), r.FormValue("instagram"), id) @@ -76,9 +85,10 @@ func UpdateCustomer(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } + w.Header().Set("HX-Refresh", "true") } func DeleteCustomer(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 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 03cf617..a5f6430 100644 --- a/apps/go-crm/internal/handlers/payments.go +++ b/apps/go-crm/internal/handlers/payments.go @@ -6,6 +6,8 @@ import ( "time" "go-crm/internal/db" + + "github.com/go-chi/chi/v5" ) func ListPayments(w http.ResponseWriter, r *http.Request) { @@ -22,14 +24,46 @@ func ListPayments(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(`

Payments

`)) + w.Write([]byte(`

Payments

CustomerAmountPaidDateMethodActions
`)) for _, p := range payments { paid := "No" if p.HasPaid { paid = "Yes" } - w.Write([]byte(``)) + clientName := clientNames[p.ClientID] + if clientName == "" { + clientName = strconv.FormatInt(p.ClientID, 10) + } + customerName := customerNames[p.CustomerID] + if customerName == "" { + customerName = strconv.FormatInt(p.CustomerID, 10) + } + w.Write([]byte(``)) } w.Write([]byte(`
ClientCustomerAmountPaidDateMethodActions
` + 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
`)) } @@ -62,7 +96,7 @@ func CreatePayment(w http.ResponseWriter, r *http.Request) { } func ViewPayment(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10) + id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64) var p db.Payment var hasPaid int err := DB.QueryRow("SELECT payment_id, client_id, customer_id, schedule_id, has_paid, amount, payment_date, payment_method, created_at FROM payments WHERE payment_id = ?", id).Scan(&p.PaymentID, &p.ClientID, &p.CustomerID, &p.ScheduleID, &hasPaid, &p.Amount, &p.PaymentDate, &p.PaymentMethod, &p.CreatedAt) @@ -76,9 +110,25 @@ func ViewPayment(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`

Payment

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

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

Method: ` + p.PaymentMethod + `

Back`)) } -func UpdatePayment(w http.ResponseWriter, r *http.Request) {} +func 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(`

Questions

`)) + w.Write([]byte(`

Questions

CustomerQuestionStatusActions
`)) for _, q := range questions { - w.Write([]byte(``)) + clientName := clientNames[q.ClientID] + if clientName == "" { + clientName = strconv.FormatInt(q.ClientID, 10) + } + customerName := customerNames[q.CustomerID] + if customerName == "" { + customerName = strconv.FormatInt(q.CustomerID, 10) + } + w.Write([]byte(``)) } w.Write([]byte(`
ClientCustomerQuestionStatusActions
` + strconv.FormatInt(q.CustomerID, 10) + `` + q.Question + `` + q.Status + `View
` + clientName + `` + customerName + `` + q.Question + `` + q.Status + `View
`)) } @@ -52,13 +86,35 @@ func CreateQuestion(w http.ResponseWriter, r *http.Request) { } func ViewQuestion(w http.ResponseWriter, r *http.Request) { + 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) + if err != nil { + http.Error(w, "Question not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`

Question

Back`)) + w.Write([]byte(`

Question

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(`

Answers

`)) - for _, a := range answers { - w.Write([]byte(``)) + questions, err := db.ListQuestions(DB, 0, limit, offset) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - w.Write([]byte(`
QuestionAnswerStatusActions
` + strconv.FormatInt(a.QuestionID, 10) + `` + a.Answer + `` + a.Status + `View
`)) + + var questionOptions string + questionText := make(map[int64]string) + for _, q := range questions { + questionOptions += `` + questionText[q.QuestionID] = q.Question + } + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(` + + + + + + +

Answers

+ + + + + + + +`)) + for _, a := range answers { + qText := a.QuestionText + if qText == "" { + qText = questionText[a.QuestionID] + if qText == "" { + qText = strconv.FormatInt(a.QuestionID, 10) + } + } + clientName := a.ClientName + if clientName == "" { + clientName = strconv.FormatInt(a.ClientID, 10) + } + customerName := a.CustomerName + if customerName == "" { + customerName = "Unknown" + } + w.Write([]byte(` + + + + + + + + + + + `)) + } + w.Write([]byte(` + +
ClientCustomerQuestionAnswerStatusActions
` + clientName + `` + customerName + `` + qText + `` + a.Answer + `` + a.Status + ` + View + +
+ +
+
+ +`)) } func CreateAnswer(w http.ResponseWriter, r *http.Request) { r.ParseForm() - clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) questionID, _ := strconv.ParseInt(r.FormValue("question_id"), 10, 64) + + var clientID int64 + err := DB.QueryRow("SELECT client_id FROM questions WHERE question_id = ?", questionID).Scan(&clientID) + if err != nil { + http.Error(w, "Question not found", http.StatusBadRequest) + return + } + answer := db.Answer{ - ClientID: clientID, + ClientID: clientID, QuestionID: questionID, Answer: r.FormValue("answer"), - Timestamp: time.Now().Unix(), - Status: r.FormValue("status"), - CreatedAt: time.Now().Unix(), + Timestamp: time.Now().Unix(), + Status: "active", + CreatedAt: time.Now().Unix(), } if err := answer.Create(DB); err != nil { @@ -104,10 +252,31 @@ func CreateAnswer(w http.ResponseWriter, r *http.Request) { } func ViewAnswer(w http.ResponseWriter, r *http.Request) { + 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) + if err != nil { + http.Error(w, "Answer not found", http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`

Answer

Back`)) + w.Write([]byte(`

Answer

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(`

Scheduling

`)) + w.Write([]byte(`

Scheduling

CustomerDateTimeStatusActions
`)) for _, s := range schedules { - w.Write([]byte(``)) + clientName := clientNames[s.ClientID] + if clientName == "" { + clientName = strconv.FormatInt(s.ClientID, 10) + } + customerName := customerNames[s.CustomerID] + if customerName == "" { + customerName = strconv.FormatInt(s.CustomerID, 10) + } + serviceName := serviceNames[s.ServiceID] + if serviceName == "" { + serviceName = strconv.FormatInt(s.ServiceID, 10) + } + w.Write([]byte(``)) } w.Write([]byte(`
ClientCustomerServiceDateHourStatusActions
` + strconv.FormatInt(s.CustomerID, 10) + `` + s.PlanDate + `` + s.Time + `` + s.Status + `View
` + clientName + `` + customerName + `` + serviceName + `` + s.PlanDate + `` + s.Time + `` + s.Status + `View
`)) } func CreateSchedule(w http.ResponseWriter, r *http.Request) { r.ParseForm() - customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64) clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) + customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64) + serviceID, _ := strconv.ParseInt(r.FormValue("service_id"), 10, 64) schedule := db.Schedule{ - CustomerID: customerID, - ClientID: clientID, - PlanDate: r.FormValue("plan_date"), - ConfirmedDate: r.FormValue("confirmed_date"), - Time: r.FormValue("time"), - Status: r.FormValue("status"), - Notes: r.FormValue("notes"), - CreatedAt: time.Now().Unix(), + ClientID: clientID, + CustomerID: customerID, + ServiceID: serviceID, + PlanDate: r.FormValue("plan_date"), + Time: r.FormValue("time"), + Status: r.FormValue("status"), + CreatedAt: time.Now().Unix(), } if err := schedule.Create(DB); err != nil { @@ -58,27 +104,33 @@ func CreateSchedule(w http.ResponseWriter, r *http.Request) { } func ViewSchedule(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) var sch db.Schedule - err := DB.QueryRow("SELECT schedule_id, customer_id, client_id, plan_date, confirmed_date, time, status, notes, created_at FROM scheduling WHERE schedule_id = ?", id).Scan(&sch.ScheduleID, &sch.CustomerID, &sch.ClientID, &sch.PlanDate, &sch.ConfirmedDate, &sch.Time, &sch.Status, &sch.Notes, &sch.CreatedAt) + 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) if err != nil { http.Error(w, "Schedule not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`

Schedule

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

Date: ` + sch.PlanDate + `

Time: ` + sch.Time + `

Status: ` + sch.Status + `

Back`)) + w.Write([]byte(`

Schedule

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(`

Services

`)) + w.Write([]byte(`

Services

NamePriceDurationActions
`)) for _, s := range services { - w.Write([]byte(``)) + w.Write([]byte(``)) } w.Write([]byte(`
NamePriceDurationActions
` + s.Name + `` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `` + s.Duration + `View
` + s.Name + `` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `` + s.Duration + `View
`)) } @@ -55,7 +65,7 @@ func CreateService(w http.ResponseWriter, r *http.Request) { } func ViewService(w http.ResponseWriter, r *http.Request) { - id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10) + 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) if err != nil { @@ -68,14 +78,19 @@ func ViewService(w http.ResponseWriter, r *http.Request) { } func UpdateService(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() clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64) price, _ := strconv.ParseFloat(r.FormValue("price"), 64) - DB.Exec("UPDATE services SET client_id=?, name=?, price=?, description=?, duration=? WHERE service_id=?", clientID, r.FormValue("name"), price, r.FormValue("description"), r.FormValue("duration"), id) + _, err := DB.Exec("UPDATE services SET client_id=?, name=?, price=?, description=?, duration=? WHERE service_id=?", clientID, r.FormValue("name"), price, r.FormValue("description"), r.FormValue("duration"), id) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("HX-Refresh", "true") } func DeleteService(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 services WHERE service_id = ?", id) } \ No newline at end of file diff --git a/apps/go-crm/internal/templates/layout.go b/apps/go-crm/internal/templates/layout.go index 020f807..3598e50 100644 --- a/apps/go-crm/internal/templates/layout.go +++ b/apps/go-crm/internal/templates/layout.go @@ -58,10 +58,11 @@ func Layout(title, content string) *template.Template {
  • Scheduling
  • Payments
  • Questions
  • +
  • Answers
  • - {{ template "content" . }} + {{ .Content }}
    `)) diff --git a/apps/go-crm/main.go b/apps/go-crm/main.go index 2287ea3..5578276 100644 --- a/apps/go-crm/main.go +++ b/apps/go-crm/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "strings" "go-crm/internal/db" "go-crm/internal/handlers" @@ -31,7 +32,15 @@ func main() { templates.Init() r.Get("/", func(w http.ResponseWriter, r *http.Request) { - templates.Layout("Home", "home.html").Render(r.Context(), w) + 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

    `)) + } }) r.Route("/auth", func(r chi.Router) { @@ -74,7 +83,7 @@ func main() { r.Delete("/{id}", handlers.DeleteSchedule) }) - r.Route("/payments", func(r chi.Router) { +r.Route("/payments", func(r chi.Router) { r.Get("/", handlers.ListPayments) r.Post("/", handlers.CreatePayment) r.Get("/{id}", handlers.ViewPayment) diff --git a/apps/go-crm/tmp/build-errors.log b/apps/go-crm/tmp/build-errors.log new file mode 100644 index 0000000..4660a1c --- /dev/null +++ b/apps/go-crm/tmp/build-errors.log @@ -0,0 +1 @@ +exit status 1exit status 1 \ No newline at end of file diff --git a/apps/go-crm/tmp/build-errors.toml b/apps/go-crm/tmp/build-errors.toml new file mode 100644 index 0000000..481923f --- /dev/null +++ b/apps/go-crm/tmp/build-errors.toml @@ -0,0 +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 diff --git a/apps/go-crm/tmp/main b/apps/go-crm/tmp/main new file mode 100755 index 0000000..0cf8656 Binary files /dev/null and b/apps/go-crm/tmp/main differ