go-crm initial
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -50,3 +50,6 @@ Thumbs.db
|
|||||||
|
|
||||||
|
|
||||||
.opencode-sandbox/.env
|
.opencode-sandbox/.env
|
||||||
|
|
||||||
|
# Go CRM data
|
||||||
|
data/*.db
|
||||||
|
|||||||
@@ -10,10 +10,17 @@ RUN pacman -Syu --noconfirm && \
|
|||||||
openssh \
|
openssh \
|
||||||
opencode \
|
opencode \
|
||||||
curl \
|
curl \
|
||||||
|
go \
|
||||||
|
sqlite \
|
||||||
&& pacman -Scc --noconfirm
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
RUN npx -y skills add JuliusBrussee/caveman -g -a opencode -s caveman -y
|
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
|
# Git safe directory system-wide config
|
||||||
RUN git config --system --add safe.directory /workspace
|
RUN git config --system --add safe.directory /workspace
|
||||||
|
|||||||
BIN
.opencode-sandbox/data/go-crm.db
Normal file
BIN
.opencode-sandbox/data/go-crm.db
Normal file
Binary file not shown.
@@ -15,11 +15,22 @@ services:
|
|||||||
- /home/ga/.ssh:/root/.ssh
|
- /home/ga/.ssh:/root/.ssh
|
||||||
# Persist opencode auth
|
# Persist opencode auth
|
||||||
- opencode-auth:/root/.local/share/opencode
|
- opencode-auth:/root/.local/share/opencode
|
||||||
|
# Go CRM database
|
||||||
|
- ./data:/workspace/data
|
||||||
environment:
|
environment:
|
||||||
- OPENCODE_API_KEY=${OPENCODE_API_KEY}
|
- OPENCODE_API_KEY=${OPENCODE_API_KEY}
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
working_dir: /workspace
|
working_dir: /workspace
|
||||||
|
networks:
|
||||||
|
- crm-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
crm-network:
|
||||||
|
name: crm-network
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
opencode-auth:
|
opencode-auth:
|
||||||
35
apps/go-crm/.air.toml
Normal file
35
apps/go-crm/.air.toml
Normal file
@@ -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
|
||||||
26
apps/go-crm/Dockerfile
Normal file
26
apps/go-crm/Dockerfile
Normal file
@@ -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"]
|
||||||
BIN
apps/go-crm/data/go-crm.db
Normal file
BIN
apps/go-crm/data/go-crm.db
Normal file
Binary file not shown.
18
apps/go-crm/docker-compose.yml
Normal file
18
apps/go-crm/docker-compose.yml
Normal file
@@ -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
|
||||||
@@ -3,12 +3,23 @@ module go-crm
|
|||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/go-chi/chi/v5 v5.1.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
|
golang.org/x/crypto v0.27.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-fresty/xtpl v0.6.0 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // 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
|
||||||
|
)
|
||||||
|
|||||||
36
apps/go-crm/go.sum
Normal file
36
apps/go-crm/go.sum
Normal file
@@ -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=
|
||||||
@@ -2,7 +2,6 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -36,15 +35,14 @@ type Service struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Schedule struct {
|
type Schedule struct {
|
||||||
ScheduleID int64 `json:"schedule_id"`
|
ScheduleID int64 `json:"schedule_id"`
|
||||||
CustomerID int64 `json:"customer_id"`
|
ClientID int64 `json:"client_id"`
|
||||||
ClientID int64 `json:"client_id"`
|
CustomerID int64 `json:"customer_id"`
|
||||||
PlanDate string `json:"plan_date,omitempty"`
|
ServiceID int64 `json:"service_id"`
|
||||||
ConfirmedDate string `json:"confirmed_date,omitempty"`
|
PlanDate string `json:"plan_date,omitempty"`
|
||||||
Time string `json:"time,omitempty"`
|
Time string `json:"time,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Notes string `json:"notes,omitempty"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Payment struct {
|
type Payment struct {
|
||||||
@@ -59,26 +57,6 @@ type Payment struct {
|
|||||||
CreatedAt int64 `json:"created_at"`
|
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 {
|
func (c *Client) Create(db *sql.DB) error {
|
||||||
result, err := db.Exec(
|
result, err := db.Exec(
|
||||||
"INSERT INTO clients (name, phone, email, address, notes, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
"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 {
|
func (sch *Schedule) Create(db *sql.DB) error {
|
||||||
result, err := db.Exec(
|
result, err := db.Exec(
|
||||||
"INSERT INTO scheduling (customer_id, client_id, plan_date, confirmed_date, time, status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
"INSERT INTO scheduling (client_id, customer_id, service_id, plan_date, time, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
sch.CustomerID, sch.ClientID, sch.PlanDate, sch.ConfirmedDate, sch.Time, sch.Status, sch.Notes, sch.CreatedAt,
|
sch.ClientID, sch.CustomerID, sch.ServiceID, sch.PlanDate, sch.Time, sch.Status, sch.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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) {
|
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{}
|
var args []interface{}
|
||||||
if clientID > 0 {
|
if clientID > 0 {
|
||||||
query += " AND client_id = ?"
|
query += " AND client_id = ?"
|
||||||
@@ -273,7 +251,7 @@ func ListSchedules(db *sql.DB, clientID, customerID int64, limit, offset int) ([
|
|||||||
var schedules []Schedule
|
var schedules []Schedule
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var sch Schedule
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
schedules = append(schedules, sch)
|
schedules = append(schedules, sch)
|
||||||
@@ -326,6 +304,23 @@ func ListPayments(db *sql.DB, clientID int64, limit, offset int) ([]Payment, err
|
|||||||
return payments, rows.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 {
|
func (q *Question) Create(db *sql.DB) error {
|
||||||
result, err := db.Exec(
|
result, err := db.Exec(
|
||||||
"INSERT INTO questions (client_id, customer_id, question, timestamp, status, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
"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) {
|
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{}
|
var args []interface{}
|
||||||
if clientID > 0 {
|
if clientID > 0 {
|
||||||
query += " WHERE client_id = ?"
|
query += " AND client_id = ?"
|
||||||
args = append(args, clientID)
|
args = append(args, clientID)
|
||||||
}
|
}
|
||||||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
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()
|
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 {
|
func (a *Answer) Create(db *sql.DB) error {
|
||||||
result, err := db.Exec(
|
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,
|
a.ClientID, a.QuestionID, a.Answer, a.Timestamp, a.Status, a.CreatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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) {
|
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{}
|
var args []interface{}
|
||||||
if questionID > 0 {
|
if questionID > 0 {
|
||||||
query += " WHERE question_id = ?"
|
query += " AND question_id = ?"
|
||||||
args = append(args, questionID)
|
args = append(args, questionID)
|
||||||
}
|
}
|
||||||
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
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()
|
return answers, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func boolToInt(b bool) int {
|
type AnswerWithDetails struct {
|
||||||
if b {
|
AnswerID int64 `json:"answer_id"`
|
||||||
return 1
|
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()
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/glebarez/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
const schema = `
|
const schema = `
|
||||||
@@ -59,16 +59,16 @@ CREATE TABLE IF NOT EXISTS services (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS scheduling (
|
CREATE TABLE IF NOT EXISTS scheduling (
|
||||||
schedule_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
schedule_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
customer_id INTEGER NOT NULL,
|
|
||||||
client_id INTEGER NOT NULL,
|
client_id INTEGER NOT NULL,
|
||||||
|
customer_id INTEGER NOT NULL,
|
||||||
|
service_id INTEGER NOT NULL,
|
||||||
plan_date TEXT,
|
plan_date TEXT,
|
||||||
confirmed_date TEXT,
|
|
||||||
time TEXT,
|
time TEXT,
|
||||||
status TEXT DEFAULT 'pending',
|
status TEXT DEFAULT 'pending',
|
||||||
notes TEXT,
|
|
||||||
created_at INTEGER NOT NULL,
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (client_id) REFERENCES clients(client_id),
|
||||||
FOREIGN KEY (customer_id) REFERENCES customers(customer_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 (
|
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 (client_id) REFERENCES clients(client_id),
|
||||||
FOREIGN KEY (question_id) REFERENCES questions(question_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) {
|
func Init(path string) (*sql.DB, error) {
|
||||||
if path != "" {
|
if path != "" {
|
||||||
@@ -123,7 +125,7 @@ func Init(path string) (*sql.DB, error) {
|
|||||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-crm/internal/db"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -61,7 +59,7 @@ func Signup(w http.ResponseWriter, r *http.Request) {
|
|||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.DB.Exec(
|
_, err = DB.Exec(
|
||||||
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
|
"INSERT INTO accounts (email, name, password, created_at) VALUES (?, ?, ?, ?)",
|
||||||
account.Email, account.Name, account.Password, account.CreatedAt,
|
account.Email, account.Name, account.Password, account.CreatedAt,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -31,6 +29,10 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
|
|||||||
<head>
|
<head>
|
||||||
<title>Clients</title>
|
<title>Clients</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
<style>
|
||||||
|
.edit-form { display:none; margin-top:0.5rem; padding:0.5rem; border:1px solid #ccc; }
|
||||||
|
.edit-row { display:none; }
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Clients</h1>
|
<h1>Clients</h1>
|
||||||
@@ -58,9 +60,20 @@ func ListClients(w http.ResponseWriter, r *http.Request) {
|
|||||||
<td>` + c.Email + `</td>
|
<td>` + c.Email + `</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/clients/` + strconv.FormatInt(c.ClientID, 10) + `">View</a>
|
<a href="/clients/` + strconv.FormatInt(c.ClientID, 10) + `">View</a>
|
||||||
|
<button type="button" onclick="document.getElementById('editForm` + strconv.FormatInt(c.ClientID, 10) + `').style.display='block'">Edit</button>
|
||||||
<form method="DELETE" style="display:inline" hx-delete="/clients/` + strconv.FormatInt(c.ClientID, 10) + `" hx-target="closest tr">
|
<form method="DELETE" style="display:inline" hx-delete="/clients/` + strconv.FormatInt(c.ClientID, 10) + `" hx-target="closest tr">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
|
<tr id="editForm` + strconv.FormatInt(c.ClientID, 10) + `" class="edit-row"><td colspan="4">
|
||||||
|
<form hx-put="/clients/` + strconv.FormatInt(c.ClientID, 10) + `" hx-target="#clientList" hx-swap="innerHTML">
|
||||||
|
<input type="text" name="name" value="` + c.Name + `">
|
||||||
|
<input type="tel" name="phone" value="` + c.Phone + `">
|
||||||
|
<input type="email" name="email" value="` + c.Email + `">
|
||||||
|
<input type="text" name="address" value="` + c.Address + `">
|
||||||
|
<textarea name="notes">` + c.Notes + `</textarea>
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</td></tr>
|
||||||
</td>
|
</td>
|
||||||
</tr>`))
|
</tr>`))
|
||||||
}
|
}
|
||||||
@@ -88,7 +101,7 @@ func CreateClient(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ViewClient(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)
|
client, err := db.GetClientByID(DB, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Client not found", http.StatusNotFound)
|
http.Error(w, "Client not found", http.StatusNotFound)
|
||||||
@@ -101,6 +114,7 @@ func ViewClient(w http.ResponseWriter, r *http.Request) {
|
|||||||
<head></head>
|
<head></head>
|
||||||
<body>
|
<body>
|
||||||
<h1>` + client.Name + `</h1>
|
<h1>` + client.Name + `</h1>
|
||||||
|
<p>Client ID: ` + strconv.FormatInt(client.ClientID, 10) + `</p>
|
||||||
<p>Phone: ` + client.Phone + `</p>
|
<p>Phone: ` + client.Phone + `</p>
|
||||||
<p>Email: ` + client.Email + `</p>
|
<p>Email: ` + client.Email + `</p>
|
||||||
<p>Address: ` + client.Address + `</p>
|
<p>Address: ` + client.Address + `</p>
|
||||||
@@ -110,7 +124,7 @@ func ViewClient(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateClient(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()
|
r.ParseForm()
|
||||||
client := db.Client{
|
client := db.Client{
|
||||||
ClientID: id,
|
ClientID: id,
|
||||||
@@ -126,12 +140,11 @@ func UpdateClient(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("HX-Refresh", "true")
|
||||||
json.NewEncoder(w).Encode(client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteClient(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if err := (&db.Client{}).Delete(DB, id); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,10 +24,21 @@ func ListCustomers(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 += `<option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>`
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Customers</h1><button class="btn" onclick="document.getElementById('customerForm').style.display='block'">Add Customer</button><div id="customerForm" style="display:none; margin-top:1rem;"><form hx-post="/customers" hx-target="#customerList"><input type="text" name="name" placeholder="Name" required><input type="tel" name="phone" placeholder="Phone"><input type="date" name="birth_date" placeholder="Birth Date"><input type="text" name="instagram" placeholder="Instagram"><input type="number" name="client_id" placeholder="Client ID" required><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Phone</th><th>Birth Date</th><th>Instagram</th><th>Actions</th></tr></thead><tbody id="customerList">`))
|
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script><style>.edit-form{display:none;margin-top:0.5rem;padding:0.5rem;border:1px solid #ccc}</style></head><body><h1>Customers</h1><button class="btn" onclick="document.getElementById('customerForm').style.display='block'">Add Customer</button><div id="customerForm" style="display:none; margin-top:1rem;"><form hx-post="/customers" hx-target="#customerList"><input type="text" name="name" placeholder="Name" required><input type="tel" name="phone" placeholder="Phone"><input type="date" name="birth_date" placeholder="Birth Date"><input type="text" name="instagram" placeholder="Instagram"><select name="client_id" required><option value="">Select Client</option>` + clientOptions + `</select><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Phone</th><th>Birth Date</th><th>Instagram</th><th>Actions</th></tr></thead><tbody id="customerList">`))
|
||||||
for _, c := range customers {
|
for _, c := range customers {
|
||||||
w.Write([]byte(`<tr><td>` + c.Name + `</td><td>` + c.Phone + `</td><td>` + c.BirthDate + `</td><td>` + c.Instagram + `</td><td><a href="/customers/` + strconv.FormatInt(c.CustomerID, 10) + `">View</a></td></tr>`))
|
w.Write([]byte(`<tr><td>` + c.Name + `</td><td>` + c.Phone + `</td><td>` + c.BirthDate + `</td><td>` + c.Instagram + `</td><td><a href="/customers/` + strconv.FormatInt(c.CustomerID, 10) + `">View</a><button type="button" onclick="document.getElementById('editCust` + strconv.FormatInt(c.CustomerID, 10) + `').style.display='block'">Edit</button><form method="DELETE" style="display:inline" hx-delete="/customers/` + strconv.FormatInt(c.CustomerID, 10) + `" hx-target="closest tr"><button type="submit">Delete</button></form></td></tr><tr id="editCust` + strconv.FormatInt(c.CustomerID, 10) + `" style="display:none"><td colspan="5"><form hx-put="/customers/` + strconv.FormatInt(c.CustomerID, 10) + `" hx-target="#customerList" hx-swap="innerHTML"><input type="text" name="name" value="` + c.Name + `"><input type="tel" name="phone" value="` + c.Phone + `"><input type="date" name="birth_date" value="` + c.BirthDate + `"><input type="text" name="instagram" value="` + c.Instagram + `"><select name="client_id"><option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>` + clientOptions + `</select><button type="submit">Save</button></form></td></tr>`))
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
w.Write([]byte(`</tbody></table></body></html>`))
|
||||||
}
|
}
|
||||||
@@ -55,7 +64,7 @@ func CreateCustomer(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ViewCustomer(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
|
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)
|
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 {
|
if err != nil {
|
||||||
@@ -68,7 +77,7 @@ func ViewCustomer(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateCustomer(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()
|
r.ParseForm()
|
||||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
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)
|
_, 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)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.Header().Set("HX-Refresh", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteCustomer(w http.ResponseWriter, r *http.Request) {
|
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)
|
DB.Exec("DELETE FROM customers WHERE customer_id = ?", id)
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-crm/internal/db"
|
"go-crm/internal/db"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListPayments(w http.ResponseWriter, r *http.Request) {
|
func ListPayments(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -22,14 +24,46 @@ func ListPayments(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 += `<option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>`
|
||||||
|
clientNames[c.ClientID] = c.Name
|
||||||
|
}
|
||||||
|
for _, cu := range customers {
|
||||||
|
customerOptions += `<option value="` + strconv.FormatInt(cu.CustomerID, 10) + `">` + cu.Name + `</option>`
|
||||||
|
customerNames[cu.CustomerID] = cu.Name
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Payments</h1><button class="btn" onclick="document.getElementById('paymentForm').style.display='block'">Add Payment</button><div id="paymentForm" style="display:none; margin-top:1rem;"><form hx-post="/payments" hx-target="#paymentList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="customer_id" placeholder="Customer ID" required><input type="number" name="schedule_id" placeholder="Schedule ID"><input type="checkbox" name="has_paid"><label>Paid</label><input type="number" name="amount" placeholder="Amount" step="0.01"><input type="date" name="payment_date"><input type="text" name="payment_method" placeholder="Payment Method"><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Amount</th><th>Paid</th><th>Date</th><th>Method</th><th>Actions</th></tr></thead><tbody id="paymentList">`))
|
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script><style>.edit-row{display:none}</style></head><body><h1>Payments</h1><button class="btn" onclick="document.getElementById('paymentForm').style.display='block'">Add Payment</button><div id="paymentForm" style="display:none; margin-top:1rem;"><form hx-post="/payments" hx-target="#paymentList"><select name="client_id" required><option value="">Select Client</option>` + clientOptions + `</select><select name="customer_id" required><option value="">Select Customer</option>` + customerOptions + `</select><input type="checkbox" name="has_paid"><label>Paid</label><input type="number" name="amount" placeholder="Amount" step="0.01"><input type="date" name="payment_date"><select name="payment_method"><option value="">Select Payment Method</option><option value="PIX">PIX</option><option value="Dinheiro">Dinheiro</option><option value="Débito">Débito</option><option value="Crédito à Vista">Crédito à Vista</option><option value="Parcelado">Parcelado</option><option value="Boleto">Boleto</option><option value="Transferência Bancária">Transferência Bancária</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Client</th><th>Customer</th><th>Amount</th><th>Paid</th><th>Date</th><th>Method</th><th>Actions</th></tr></thead><tbody id="paymentList">`))
|
||||||
for _, p := range payments {
|
for _, p := range payments {
|
||||||
paid := "No"
|
paid := "No"
|
||||||
if p.HasPaid {
|
if p.HasPaid {
|
||||||
paid = "Yes"
|
paid = "Yes"
|
||||||
}
|
}
|
||||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(p.CustomerID, 10) + `</td><td>` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</td><td>` + paid + `</td><td>` + p.PaymentDate + `</td><td>` + p.PaymentMethod + `</td><td><a href="/payments/` + strconv.FormatInt(p.PaymentID, 10) + `">View</a></td></tr>`))
|
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(`<tr><td>` + clientName + `</td><td>` + customerName + `</td><td>` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</td><td>` + paid + `</td><td>` + p.PaymentDate + `</td><td>` + p.PaymentMethod + `</td><td><a href="/payments/` + strconv.FormatInt(p.PaymentID, 10) + `">View</a><button type="button" onclick="document.getElementById('editPay` + strconv.FormatInt(p.PaymentID, 10) + `').style.display='table-row'">Edit</button><form method="DELETE" style="display:inline" hx-delete="/payments/` + strconv.FormatInt(p.PaymentID, 10) + `" hx-target="closest tr"><button type="submit">Delete</button></form></td></tr><tr id="editPay` + strconv.FormatInt(p.PaymentID, 10) + `" style="display:none"><td colspan="7"><form hx-put="/payments/` + strconv.FormatInt(p.PaymentID, 10) + `" hx-target="#paymentList" hx-swap="innerHTML"><select name="client_id"><option value="` + strconv.FormatInt(p.ClientID, 10) + `">` + clientName + `</option>` + clientOptions + `</select><select name="customer_id"><option value="` + strconv.FormatInt(p.CustomerID, 10) + `">` + customerName + `</option>` + customerOptions + `</select><input type="checkbox" name="has_paid"><input type="number" name="amount" value="` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `"><input type="date" name="payment_date" value="` + p.PaymentDate + `"><select name="payment_method"><option value="` + p.PaymentMethod + `">` + p.PaymentMethod + `</option><option value="PIX">PIX</option><option value="Dinheiro">Dinheiro</option><option value="Débito">Débito</option><option value="Crédito à Vista">Crédito à Vista</option><option value="Parcelado">Parcelado</option><option value="Boleto">Boleto</option><option value="Transferência Bancária">Transferência Bancária</option></select><button type="submit">Save</button></form></td></tr>`))
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
w.Write([]byte(`</tbody></table></body></html>`))
|
||||||
}
|
}
|
||||||
@@ -62,7 +96,7 @@ func CreatePayment(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ViewPayment(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 p db.Payment
|
||||||
var hasPaid int
|
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)
|
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(`<!DOCTYPE html><body><h1>Payment</h1><p>Amount: ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</p><p>Paid: ` + strconv.FormatBool(p.HasPaid) + `</p><p>Method: ` + p.PaymentMethod + `</p><a href="/payments">Back</a></body></html>`))
|
w.Write([]byte(`<!DOCTYPE html><body><h1>Payment</h1><p>Amount: ` + strconv.FormatFloat(p.Amount, 'f', 2, 64) + `</p><p>Paid: ` + strconv.FormatBool(p.HasPaid) + `</p><p>Method: ` + p.PaymentMethod + `</p><a href="/payments">Back</a></body></html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func boolToStr(b bool) string {
|
||||||
if b {
|
if b {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-crm/internal/db"
|
"go-crm/internal/db"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListQuestions(w http.ResponseWriter, r *http.Request) {
|
func ListQuestions(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -22,10 +24,42 @@ func ListQuestions(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 += `<option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>`
|
||||||
|
clientNames[c.ClientID] = c.Name
|
||||||
|
}
|
||||||
|
for _, cu := range customers {
|
||||||
|
customerOptions += `<option value="` + strconv.FormatInt(cu.CustomerID, 10) + `">` + cu.Name + `</option>`
|
||||||
|
customerNames[cu.CustomerID] = cu.Name
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Questions</h1><button class="btn" onclick="document.getElementById('questionForm').style.display='block'">Add Question</button><div id="questionForm" style="display:none; margin-top:1rem;"><form hx-post="/questions" hx-target="#questionList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="customer_id" placeholder="Customer ID" required><textarea name="question" placeholder="Question" required></textarea><select name="status"><option value="pending">Pending</option><option value="answered">Answered</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Question</th><th>Status</th><th>Actions</th></tr></thead><tbody id="questionList">`))
|
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script><style>.edit-row{display:none}</style></head><body><h1>Questions</h1><button class="btn" onclick="document.getElementById('questionForm').style.display='block'">Add Question</button><div id="questionForm" style="display:none; margin-top:1rem;"><form hx-post="/questions" hx-target="#questionList"><select name="client_id" required><option value="">Select Client</option>` + clientOptions + `</select><select name="customer_id" required><option value="">Select Customer</option>` + customerOptions + `</select><textarea name="question" placeholder="Question" required></textarea><select name="status"><option value="pending">Pending</option><option value="answered">Answered</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Client</th><th>Customer</th><th>Question</th><th>Status</th><th>Actions</th></tr></thead><tbody id="questionList">`))
|
||||||
for _, q := range questions {
|
for _, q := range questions {
|
||||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(q.CustomerID, 10) + `</td><td>` + q.Question + `</td><td>` + q.Status + `</td><td><a href="/questions/` + strconv.FormatInt(q.QuestionID, 10) + `">View</a></td></tr>`))
|
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(`<tr><td>` + clientName + `</td><td>` + customerName + `</td><td>` + q.Question + `</td><td>` + q.Status + `</td><td><a href="/questions/` + strconv.FormatInt(q.QuestionID, 10) + `">View</a><button type="button" onclick="document.getElementById('editQ` + strconv.FormatInt(q.QuestionID, 10) + `').style.display='table-row'">Edit</button><form method="DELETE" style="display:inline" hx-delete="/questions/` + strconv.FormatInt(q.QuestionID, 10) + `" hx-target="closest tr"><button type="submit">Delete</button></form></td></tr><tr id="editQ` + strconv.FormatInt(q.QuestionID, 10) + `" style="display:none"><td colspan="5"><form hx-put="/questions/` + strconv.FormatInt(q.QuestionID, 10) + `" hx-target="#questionList" hx-swap="innerHTML"><select name="client_id"><option value="` + strconv.FormatInt(q.ClientID, 10) + `">` + clientName + `</option>` + clientOptions + `</select><select name="customer_id"><option value="` + strconv.FormatInt(q.CustomerID, 10) + `">` + customerName + `</option>` + customerOptions + `</select><textarea name="question">` + q.Question + `</textarea><select name="status"><option value="` + q.Status + `">` + q.Status + `</option><option value="pending">Pending</option><option value="answered">Answered</option></select><button type="submit">Save</button></form></td></tr>`))
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
w.Write([]byte(`</tbody></table></body></html>`))
|
||||||
}
|
}
|
||||||
@@ -52,13 +86,35 @@ func CreateQuestion(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ViewQuestion(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.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Question</h1><a href="/questions">Back</a></body></html>`))
|
w.Write([]byte(`<!DOCTYPE html><body><h1>Question</h1><p>Client ID: ` + strconv.FormatInt(q.ClientID, 10) + `</p><p>Customer ID: ` + strconv.FormatInt(q.CustomerID, 10) + `</p><p>Question: ` + q.Question + `</p><p>Status: ` + q.Status + `</p><a href="/questions">Back</a></body></html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func ListAnswers(w http.ResponseWriter, r *http.Request) {
|
||||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||||
@@ -68,31 +124,123 @@ func ListAnswers(w http.ResponseWriter, r *http.Request) {
|
|||||||
limit = 20
|
limit = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
answers, err := db.ListAnswers(DB, questionID, limit, offset)
|
answers, err := db.ListAnswersWithDetails(DB, questionID, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
questions, err := db.ListQuestions(DB, 0, limit, offset)
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Answers</h1><button class="btn" onclick="document.getElementById('answerForm').style.display='block'">Add Answer</button><div id="answerForm" style="display:none; margin-top:1rem;"><form hx-post="/answers" hx-target="#answerList"><input type="number" name="client_id" placeholder="Client ID" required><input type="number" name="question_id" placeholder="Question ID" required><textarea name="answer" placeholder="Answer" required></textarea><select name="status"><option value="active">Active</option><option value="deleted">Deleted</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Question</th><th>Answer</th><th>Status</th><th>Actions</th></tr></thead><tbody id="answerList">`))
|
if err != nil {
|
||||||
for _, a := range answers {
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(a.QuestionID, 10) + `</td><td>` + a.Answer + `</td><td>` + a.Status + `</td><td><a href="/answers/` + strconv.FormatInt(a.AnswerID, 10) + `">View</a></td></tr>`))
|
return
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
|
||||||
|
var questionOptions string
|
||||||
|
questionText := make(map[int64]string)
|
||||||
|
for _, q := range questions {
|
||||||
|
questionOptions += `<option value="` + strconv.FormatInt(q.QuestionID, 10) + `">` + q.Question + `</option>`
|
||||||
|
questionText[q.QuestionID] = q.Question
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Write([]byte(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||||
|
<style>
|
||||||
|
.edit-form { display:none; margin-top:0.5rem; padding:0.5rem; border:1px solid #ccc; }
|
||||||
|
.edit-row { display:none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Answers</h1>
|
||||||
|
<button class="btn" onclick="document.getElementById('answerForm').style.display='block'">Add Answer</button>
|
||||||
|
<div id="answerForm" style="display:none; margin-top:1rem;">
|
||||||
|
<form hx-post="/answers" hx-target="#answerList" hx-swap="innerHTML">
|
||||||
|
<select name="question_id" required>
|
||||||
|
<option value="">Select Question</option>
|
||||||
|
` + questionOptions + `
|
||||||
|
</select>
|
||||||
|
<textarea name="answer" placeholder="Answer" required></textarea>
|
||||||
|
<button type="submit">Add Answer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Client</th><th>Customer</th><th>Question</th><th>Answer</th><th>Status</th><th>Actions</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="answerList">
|
||||||
|
`))
|
||||||
|
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(`
|
||||||
|
<tr>
|
||||||
|
<td>` + clientName + `</td>
|
||||||
|
<td>` + customerName + `</td>
|
||||||
|
<td>` + qText + `</td>
|
||||||
|
<td>` + a.Answer + `</td>
|
||||||
|
<td>` + a.Status + `</td>
|
||||||
|
<td>
|
||||||
|
<a href="/answers/` + strconv.FormatInt(a.AnswerID, 10) + `">View</a>
|
||||||
|
<button type="button" onclick="document.getElementById('editA`+strconv.FormatInt(a.AnswerID, 10)+`').style.display='table-row'">Edit</button>
|
||||||
|
<form method="DELETE" style="display:inline" hx-delete="/answers/`+strconv.FormatInt(a.AnswerID, 10)+`" hx-target="closest tr">
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="editA`+strconv.FormatInt(a.AnswerID, 10)+`" class="edit-row" style="display:none">
|
||||||
|
<td colspan="6">
|
||||||
|
<form hx-put="/answers/`+strconv.FormatInt(a.AnswerID, 10)+`" hx-target="#answerList" hx-swap="innerHTML">
|
||||||
|
<select name="question_id">
|
||||||
|
<option value="`+strconv.FormatInt(a.QuestionID, 10)+`">`+qText+`</option>
|
||||||
|
`+questionOptions+`
|
||||||
|
</select>
|
||||||
|
<textarea name="answer">`+a.Answer+`</textarea>
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>`))
|
||||||
|
}
|
||||||
|
w.Write([]byte(`
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateAnswer(w http.ResponseWriter, r *http.Request) {
|
func CreateAnswer(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
|
||||||
questionID, _ := strconv.ParseInt(r.FormValue("question_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{
|
answer := db.Answer{
|
||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
QuestionID: questionID,
|
QuestionID: questionID,
|
||||||
Answer: r.FormValue("answer"),
|
Answer: r.FormValue("answer"),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
Status: r.FormValue("status"),
|
Status: "active",
|
||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := answer.Create(DB); err != nil {
|
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) {
|
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.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Answer</h1><a href="/answers">Back</a></body></html>`))
|
w.Write([]byte(`<!DOCTYPE html><body><h1>Answer</h1><p>Question ID: ` + strconv.FormatInt(a.QuestionID, 10) + `</p><p>Answer: ` + a.Answer + `</p><a href="/answers">Back</a></body></html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {}
|
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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,27 +25,74 @@ func ListSchedules(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 += `<option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>`
|
||||||
|
clientNames[c.ClientID] = c.Name
|
||||||
|
}
|
||||||
|
for _, cu := range customers {
|
||||||
|
customerOptions += `<option value="` + strconv.FormatInt(cu.CustomerID, 10) + `">` + cu.Name + `</option>`
|
||||||
|
customerNames[cu.CustomerID] = cu.Name
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
serviceOptions += `<option value="` + strconv.FormatInt(s.ServiceID, 10) + `">` + s.Name + `</option>`
|
||||||
|
serviceNames[s.ServiceID] = s.Name
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Scheduling</h1><button class="btn" onclick="document.getElementById('scheduleForm').style.display='block'">Add Schedule</button><div id="scheduleForm" style="display:none; margin-top:1rem;"><form hx-post="/scheduling" hx-target="#scheduleList"><input type="number" name="customer_id" placeholder="Customer ID" required><input type="number" name="client_id" placeholder="Client ID" required><input type="date" name="plan_date"><input type="date" name="confirmed_date"><input type="time" name="time"><select name="status"><option value="pending">Pending</option><option value="confirmed">Confirmed</option><option value="cancelled">Cancelled</option></select><textarea name="notes" placeholder="Notes"></textarea><button type="submit">Add</button></form></div><table><thead><tr><th>Customer</th><th>Date</th><th>Time</th><th>Status</th><th>Actions</th></tr></thead><tbody id="scheduleList">`))
|
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script><style>.edit-row{display:none}</style></head><body><h1>Scheduling</h1><button class="btn" onclick="document.getElementById('scheduleForm').style.display='block'">Add Schedule</button><div id="scheduleForm" style="display:none; margin-top:1rem;"><form hx-post="/scheduling" hx-target="#scheduleList"><select name="client_id" required><option value="">Select Client</option>` + clientOptions + `</select><select name="customer_id" required><option value="">Select Customer</option>` + customerOptions + `</select><select name="service_id" required><option value="">Select Service</option>` + serviceOptions + `</select><input type="date" name="plan_date"><input type="time" name="time"><select name="status"><option value="pending">Pending</option><option value="confirmed">Confirmed</option><option value="cancelled">Cancelled</option></select><button type="submit">Add</button></form></div><table><thead><tr><th>Client</th><th>Customer</th><th>Service</th><th>Date</th><th>Hour</th><th>Status</th><th>Actions</th></tr></thead><tbody id="scheduleList">`))
|
||||||
for _, s := range schedules {
|
for _, s := range schedules {
|
||||||
w.Write([]byte(`<tr><td>` + strconv.FormatInt(s.CustomerID, 10) + `</td><td>` + s.PlanDate + `</td><td>` + s.Time + `</td><td>` + s.Status + `</td><td><a href="/scheduling/` + strconv.FormatInt(s.ScheduleID, 10) + `">View</a></td></tr>`))
|
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(`<tr><td>` + clientName + `</td><td>` + customerName + `</td><td>` + serviceName + `</td><td>` + s.PlanDate + `</td><td>` + s.Time + `</td><td>` + s.Status + `</td><td><a href="/scheduling/` + strconv.FormatInt(s.ScheduleID, 10) + `">View</a><button type="button" onclick="document.getElementById('editSched` + strconv.FormatInt(s.ScheduleID, 10) + `').style.display='table-row'">Edit</button><form method="DELETE" style="display:inline" hx-delete="/scheduling/` + strconv.FormatInt(s.ScheduleID, 10) + `" hx-target="closest tr"><button type="submit">Delete</button></form></td></tr><tr id="editSched` + strconv.FormatInt(s.ScheduleID, 10) + `" class="edit-row" style="display:none"><td colspan="7"><form hx-put="/scheduling/` + strconv.FormatInt(s.ScheduleID, 10) + `" hx-target="#scheduleList" hx-swap="innerHTML"><select name="client_id"><option value="` + strconv.FormatInt(s.ClientID, 10) + `">` + clientName + `</option>` + clientOptions + `</select><select name="customer_id"><option value="` + strconv.FormatInt(s.CustomerID, 10) + `">` + customerName + `</option>` + customerOptions + `</select><select name="service_id"><option value="` + strconv.FormatInt(s.ServiceID, 10) + `">` + serviceName + `</option>` + serviceOptions + `</select><input type="date" name="plan_date" value="` + s.PlanDate + `"><input type="time" name="time" value="` + s.Time + `"><select name="status"><option value="` + s.Status + `">` + s.Status + `</option><option value="pending">Pending</option><option value="confirmed">Confirmed</option><option value="cancelled">Cancelled</option></select><button type="submit">Save</button></form></td></tr>`))
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
w.Write([]byte(`</tbody></table></body></html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateSchedule(w http.ResponseWriter, r *http.Request) {
|
func CreateSchedule(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
|
||||||
clientID, _ := strconv.ParseInt(r.FormValue("client_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{
|
schedule := db.Schedule{
|
||||||
CustomerID: customerID,
|
ClientID: clientID,
|
||||||
ClientID: clientID,
|
CustomerID: customerID,
|
||||||
PlanDate: r.FormValue("plan_date"),
|
ServiceID: serviceID,
|
||||||
ConfirmedDate: r.FormValue("confirmed_date"),
|
PlanDate: r.FormValue("plan_date"),
|
||||||
Time: r.FormValue("time"),
|
Time: r.FormValue("time"),
|
||||||
Status: r.FormValue("status"),
|
Status: r.FormValue("status"),
|
||||||
Notes: r.FormValue("notes"),
|
CreatedAt: time.Now().Unix(),
|
||||||
CreatedAt: time.Now().Unix(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := schedule.Create(DB); err != nil {
|
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) {
|
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
|
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 {
|
if err != nil {
|
||||||
http.Error(w, "Schedule not found", http.StatusNotFound)
|
http.Error(w, "Schedule not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><body><h1>Schedule</h1><p>Customer ID: ` + strconv.FormatInt(sch.CustomerID, 10) + `</p><p>Date: ` + sch.PlanDate + `</p><p>Time: ` + sch.Time + `</p><p>Status: ` + sch.Status + `</p><a href="/scheduling">Back</a></body></html>`))
|
w.Write([]byte(`<!DOCTYPE html><body><h1>Schedule</h1><p>Client ID: ` + strconv.FormatInt(sch.ClientID, 10) + `</p><p>Customer ID: ` + strconv.FormatInt(sch.CustomerID, 10) + `</p><p>Service ID: ` + strconv.FormatInt(sch.ServiceID, 10) + `</p><p>Date: ` + sch.PlanDate + `</p><p>Time: ` + sch.Time + `</p><p>Status: ` + sch.Status + `</p><a href="/scheduling">Back</a></body></html>`))
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateSchedule(w http.ResponseWriter, r *http.Request) {
|
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()
|
r.ParseForm()
|
||||||
customerID, _ := strconv.ParseInt(r.FormValue("customer_id"), 10, 64)
|
|
||||||
clientID, _ := strconv.ParseInt(r.FormValue("client_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) {
|
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)
|
DB.Exec("DELETE FROM scheduling WHERE schedule_id = ?", id)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,10 +24,21 @@ func ListServices(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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 += `<option value="` + strconv.FormatInt(c.ClientID, 10) + `">` + c.Name + `</option>`
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Services</h1><button class="btn" onclick="document.getElementById('serviceForm').style.display='block'">Add Service</button><div id="serviceForm" style="display:none; margin-top:1rem;"><form hx-post="/services" hx-target="#serviceList"><input type="text" name="name" placeholder="Service Name" required><input type="number" name="price" placeholder="Price" step="0.01"><textarea name="description" placeholder="Description"></textarea><input type="text" name="duration" placeholder="Duration"><input type="number" name="client_id" placeholder="Client ID" required><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Price</th><th>Duration</th><th>Actions</th></tr></thead><tbody id="serviceList">`))
|
w.Write([]byte(`<!DOCTYPE html><html><head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head><body><h1>Services</h1><button class="btn" onclick="document.getElementById('serviceForm').style.display='block'">Add Service</button><div id="serviceForm" style="display:none; margin-top:1rem;"><form hx-post="/services" hx-target="#serviceList"><input type="text" name="name" placeholder="Service Name" required><input type="number" name="price" placeholder="Price" step="0.01"><textarea name="description" placeholder="Description"></textarea><input type="text" name="duration" placeholder="Duration"><select name="client_id" required><option value="">Select Client</option>` + clientOptions + `</select><button type="submit">Add</button></form></div><table><thead><tr><th>Name</th><th>Price</th><th>Duration</th><th>Actions</th></tr></thead><tbody id="serviceList">`))
|
||||||
for _, s := range services {
|
for _, s := range services {
|
||||||
w.Write([]byte(`<tr><td>` + s.Name + `</td><td>` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `</td><td>` + s.Duration + `</td><td><a href="/services/` + strconv.FormatInt(s.ServiceID, 10) + `">View</a></td></tr>`))
|
w.Write([]byte(`<tr><td>` + s.Name + `</td><td>` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `</td><td>` + s.Duration + `</td><td><a href="/services/` + strconv.FormatInt(s.ServiceID, 10) + `">View</a><button type="button" onclick="document.getElementById('editServ` + strconv.FormatInt(s.ServiceID, 10) + `').style.display='table-row'">Edit</button><form method="DELETE" style="display:inline" hx-delete="/services/` + strconv.FormatInt(s.ServiceID, 10) + `" hx-target="closest tr"><button type="submit">Delete</button></form></td></tr><tr id="editServ` + strconv.FormatInt(s.ServiceID, 10) + `" style="display:none"><td colspan="4"><form hx-put="/services/` + strconv.FormatInt(s.ServiceID, 10) + `" hx-target="#serviceList" hx-swap="innerHTML"><input type="text" name="name" value="` + s.Name + `"><input type="number" name="price" value="` + strconv.FormatFloat(s.Price, 'f', 2, 64) + `"><textarea name="description">` + s.Description + `</textarea><input type="text" name="duration" value="` + s.Duration + `"><select name="client_id"><option value="` + strconv.FormatInt(s.ClientID, 10) + `">` + s.Name + `</option>` + clientOptions + `</select><button type="submit">Save</button></form></td></tr>`))
|
||||||
}
|
}
|
||||||
w.Write([]byte(`</tbody></table></body></html>`))
|
w.Write([]byte(`</tbody></table></body></html>`))
|
||||||
}
|
}
|
||||||
@@ -55,7 +65,7 @@ func CreateService(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ViewService(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
|
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)
|
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 {
|
if err != nil {
|
||||||
@@ -68,14 +78,19 @@ func ViewService(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateService(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()
|
r.ParseForm()
|
||||||
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
clientID, _ := strconv.ParseInt(r.FormValue("client_id"), 10, 64)
|
||||||
price, _ := strconv.ParseFloat(r.FormValue("price"), 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) {
|
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)
|
DB.Exec("DELETE FROM services WHERE service_id = ?", id)
|
||||||
}
|
}
|
||||||
@@ -58,10 +58,11 @@ func Layout(title, content string) *template.Template {
|
|||||||
<li><a href="/scheduling">Scheduling</a></li>
|
<li><a href="/scheduling">Scheduling</a></li>
|
||||||
<li><a href="/payments">Payments</a></li>
|
<li><a href="/payments">Payments</a></li>
|
||||||
<li><a href="/questions">Questions</a></li>
|
<li><a href="/questions">Questions</a></li>
|
||||||
|
<li><a href="/answers">Answers</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{ template "content" . }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`))
|
</html>`))
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"go-crm/internal/db"
|
"go-crm/internal/db"
|
||||||
"go-crm/internal/handlers"
|
"go-crm/internal/handlers"
|
||||||
@@ -31,7 +32,15 @@ func main() {
|
|||||||
templates.Init()
|
templates.Init()
|
||||||
|
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
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(`<!DOCTYPE html><html><head><title>Home</title></head><body><h1>Welcome to CRM</h1><nav><a href="/clients">Clients</a> | <a href="/customers">Customers</a> | <a href="/services">Services</a> | <a href="/questions">Questions</a> | <a href="/answers">Answers</a></nav></body></html>`))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/auth", func(r chi.Router) {
|
r.Route("/auth", func(r chi.Router) {
|
||||||
@@ -74,7 +83,7 @@ func main() {
|
|||||||
r.Delete("/{id}", handlers.DeleteSchedule)
|
r.Delete("/{id}", handlers.DeleteSchedule)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/payments", func(r chi.Router) {
|
r.Route("/payments", func(r chi.Router) {
|
||||||
r.Get("/", handlers.ListPayments)
|
r.Get("/", handlers.ListPayments)
|
||||||
r.Post("/", handlers.CreatePayment)
|
r.Post("/", handlers.CreatePayment)
|
||||||
r.Get("/{id}", handlers.ViewPayment)
|
r.Get("/{id}", handlers.ViewPayment)
|
||||||
|
|||||||
1
apps/go-crm/tmp/build-errors.log
Normal file
1
apps/go-crm/tmp/build-errors.log
Normal file
@@ -0,0 +1 @@
|
|||||||
|
exit status 1exit status 1
|
||||||
1
apps/go-crm/tmp/build-errors.toml
Normal file
1
apps/go-crm/tmp/build-errors.toml
Normal file
@@ -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
|
||||||
BIN
apps/go-crm/tmp/main
Executable file
BIN
apps/go-crm/tmp/main
Executable file
Binary file not shown.
Reference in New Issue
Block a user