Add Go app
Add start-development.sh
This commit is contained in:
parent
e3442c29cc
commit
4f54a93c62
|
|
@ -32,7 +32,15 @@ services:
|
|||
- action: rebuild
|
||||
path: ./app
|
||||
ignore:
|
||||
- ./app/webroot
|
||||
- ./app/webroot/pdf
|
||||
- ./app/webroot/attachments_files
|
||||
- ./app/tmp
|
||||
- action: sync
|
||||
path: ./app/webroot/css
|
||||
target: /var/www/cmc-sales/app/webroot/css
|
||||
- action: sync
|
||||
path: ./app/webroot/js
|
||||
target: /var/www/cmc-sales/app/webroot/js
|
||||
|
||||
db:
|
||||
image: mariadb:latest
|
||||
|
|
@ -66,6 +74,8 @@ services:
|
|||
condition: service_started
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./app/webroot/pdf:/root/webroot/pdf
|
||||
networks:
|
||||
- cmc-network
|
||||
restart: unless-stopped
|
||||
|
|
@ -76,6 +86,14 @@ services:
|
|||
ignore:
|
||||
- ./go-app/bin
|
||||
- ./go-app/.env
|
||||
- ./go-app/tmp
|
||||
- "**/.*" # Ignore hidden files
|
||||
- action: sync
|
||||
path: ./go-app/templates
|
||||
target: /app/templates
|
||||
- action: sync
|
||||
path: ./go-app/static
|
||||
target: /app/static
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
|
|
|||
|
|
@ -5,5 +5,8 @@ DB_USER=cmc
|
|||
DB_PASSWORD=xVRQI&cA?7AU=hqJ!%au
|
||||
DB_NAME=cmc
|
||||
|
||||
# Root database password (for dbshell-root)
|
||||
DB_ROOT_PASSWORD=secureRootPassword
|
||||
|
||||
# Server configuration
|
||||
PORT=8080
|
||||
|
|
@ -43,4 +43,24 @@ docker-build: ## Build Docker image
|
|||
|
||||
.PHONY: docker-run
|
||||
docker-run: ## Run application in Docker
|
||||
docker run --rm -p 8080:8080 --network=host cmc-go:latest
|
||||
docker run --rm -p 8080:8080 --network=host cmc-go:latest
|
||||
|
||||
.PHONY: dbshell
|
||||
dbshell: ## Connect to MariaDB database interactively
|
||||
@echo "Connecting to MariaDB..."
|
||||
@if [ -z "$$DB_PASSWORD" ]; then \
|
||||
echo "Reading password from docker-compose environment..."; \
|
||||
docker compose exec db mariadb -u cmc -p cmc; \
|
||||
else \
|
||||
docker compose exec -e MYSQL_PWD="$$DB_PASSWORD" db mariadb -u cmc cmc; \
|
||||
fi
|
||||
|
||||
.PHONY: dbshell-root
|
||||
dbshell-root: ## Connect to MariaDB as root user
|
||||
@echo "Connecting to MariaDB as root..."
|
||||
@if [ -z "$$DB_ROOT_PASSWORD" ]; then \
|
||||
echo "Please set DB_ROOT_PASSWORD environment variable"; \
|
||||
exit 1; \
|
||||
else \
|
||||
docker compose exec -e MYSQL_PWD="$$DB_ROOT_PASSWORD" db mariadb -u root; \
|
||||
fi
|
||||
|
|
@ -57,14 +57,23 @@ func main() {
|
|||
productHandler := handlers.NewProductHandler(queries)
|
||||
purchaseOrderHandler := handlers.NewPurchaseOrderHandler(queries)
|
||||
enquiryHandler := handlers.NewEnquiryHandler(queries)
|
||||
documentHandler := handlers.NewDocumentHandler(queries)
|
||||
pageHandler := handlers.NewPageHandler(queries, tmpl)
|
||||
addressHandler := handlers.NewAddressHandler(queries)
|
||||
attachmentHandler := handlers.NewAttachmentHandler(queries)
|
||||
countryHandler := handlers.NewCountryHandler(queries)
|
||||
statusHandler := handlers.NewStatusHandler(queries)
|
||||
lineItemHandler := handlers.NewLineItemHandler(queries)
|
||||
|
||||
// Setup routes
|
||||
r := mux.NewRouter()
|
||||
|
||||
|
||||
// Static files
|
||||
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
|
||||
// PDF files (matching CakePHP structure)
|
||||
r.PathPrefix("/pdf/").Handler(http.StripPrefix("/pdf/", http.FileServer(http.Dir("webroot/pdf"))))
|
||||
|
||||
// API routes
|
||||
api := r.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
|
|
@ -103,6 +112,55 @@ func main() {
|
|||
api.HandleFunc("/enquiries/{id}/mark-submitted", enquiryHandler.MarkSubmitted).Methods("PUT")
|
||||
api.HandleFunc("/enquiries/search", enquiryHandler.Search).Methods("GET")
|
||||
|
||||
// Document routes
|
||||
api.HandleFunc("/documents", documentHandler.List).Methods("GET")
|
||||
api.HandleFunc("/documents", documentHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/documents/{id}", documentHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT")
|
||||
api.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT")
|
||||
api.HandleFunc("/documents/search", documentHandler.Search).Methods("GET")
|
||||
|
||||
// Address routes
|
||||
api.HandleFunc("/addresses", addressHandler.List).Methods("GET")
|
||||
api.HandleFunc("/addresses", addressHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/addresses/{id}", addressHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/addresses/{id}", addressHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/addresses/{id}", addressHandler.Delete).Methods("DELETE")
|
||||
api.HandleFunc("/addresses/customer/{customerID}", addressHandler.CustomerAddresses).Methods("GET")
|
||||
|
||||
// Attachment routes
|
||||
api.HandleFunc("/attachments", attachmentHandler.List).Methods("GET")
|
||||
api.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET")
|
||||
api.HandleFunc("/attachments", attachmentHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/attachments/{id}", attachmentHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/attachments/{id}", attachmentHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE")
|
||||
|
||||
// Country routes
|
||||
api.HandleFunc("/countries", countryHandler.List).Methods("GET")
|
||||
api.HandleFunc("/countries", countryHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/countries/{id}", countryHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/countries/{id}", countryHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/countries/{id}", countryHandler.Delete).Methods("DELETE")
|
||||
api.HandleFunc("/countries/complete", countryHandler.CompleteCountry).Methods("GET")
|
||||
|
||||
// Status routes
|
||||
api.HandleFunc("/statuses", statusHandler.List).Methods("GET")
|
||||
api.HandleFunc("/statuses", statusHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/statuses/{id}", statusHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/statuses/{id}", statusHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/statuses/{id}", statusHandler.Delete).Methods("DELETE")
|
||||
api.HandleFunc("/statuses/json/{selectedId}", statusHandler.JsonList).Methods("GET")
|
||||
|
||||
// Line Item routes
|
||||
api.HandleFunc("/line-items", lineItemHandler.List).Methods("GET")
|
||||
api.HandleFunc("/line-items", lineItemHandler.Create).Methods("POST")
|
||||
api.HandleFunc("/line-items/{id}", lineItemHandler.Get).Methods("GET")
|
||||
api.HandleFunc("/line-items/{id}", lineItemHandler.Update).Methods("PUT")
|
||||
api.HandleFunc("/line-items/{id}", lineItemHandler.Delete).Methods("DELETE")
|
||||
api.HandleFunc("/line-items/document/{documentID}/table", lineItemHandler.GetTable).Methods("GET")
|
||||
|
||||
// Health check
|
||||
api.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
|
@ -111,28 +169,28 @@ func main() {
|
|||
|
||||
// Page routes
|
||||
r.HandleFunc("/", pageHandler.Home).Methods("GET")
|
||||
|
||||
|
||||
// Customer pages
|
||||
r.HandleFunc("/customers", pageHandler.CustomersIndex).Methods("GET")
|
||||
r.HandleFunc("/customers/new", pageHandler.CustomersNew).Methods("GET")
|
||||
r.HandleFunc("/customers/search", pageHandler.CustomersSearch).Methods("GET")
|
||||
r.HandleFunc("/customers/{id}", pageHandler.CustomersShow).Methods("GET")
|
||||
r.HandleFunc("/customers/{id}/edit", pageHandler.CustomersEdit).Methods("GET")
|
||||
|
||||
|
||||
// Product pages
|
||||
r.HandleFunc("/products", pageHandler.ProductsIndex).Methods("GET")
|
||||
r.HandleFunc("/products/new", pageHandler.ProductsNew).Methods("GET")
|
||||
r.HandleFunc("/products/search", pageHandler.ProductsSearch).Methods("GET")
|
||||
r.HandleFunc("/products/{id}", pageHandler.ProductsShow).Methods("GET")
|
||||
r.HandleFunc("/products/{id}/edit", pageHandler.ProductsEdit).Methods("GET")
|
||||
|
||||
|
||||
// Purchase Order pages
|
||||
r.HandleFunc("/purchase-orders", pageHandler.PurchaseOrdersIndex).Methods("GET")
|
||||
r.HandleFunc("/purchase-orders/new", pageHandler.PurchaseOrdersNew).Methods("GET")
|
||||
r.HandleFunc("/purchase-orders/search", pageHandler.PurchaseOrdersSearch).Methods("GET")
|
||||
r.HandleFunc("/purchase-orders/{id}", pageHandler.PurchaseOrdersShow).Methods("GET")
|
||||
r.HandleFunc("/purchase-orders/{id}/edit", pageHandler.PurchaseOrdersEdit).Methods("GET")
|
||||
|
||||
|
||||
// Enquiry pages
|
||||
r.HandleFunc("/enquiries", pageHandler.EnquiriesIndex).Methods("GET")
|
||||
r.HandleFunc("/enquiries/new", pageHandler.EnquiriesNew).Methods("GET")
|
||||
|
|
@ -140,19 +198,67 @@ func main() {
|
|||
r.HandleFunc("/enquiries/{id}", pageHandler.EnquiriesShow).Methods("GET")
|
||||
r.HandleFunc("/enquiries/{id}/edit", pageHandler.EnquiriesEdit).Methods("GET")
|
||||
|
||||
// Document pages
|
||||
r.HandleFunc("/documents", pageHandler.DocumentsIndex).Methods("GET")
|
||||
r.HandleFunc("/documents/search", pageHandler.DocumentsSearch).Methods("GET")
|
||||
r.HandleFunc("/documents/view/{id}", pageHandler.DocumentsView).Methods("GET")
|
||||
r.HandleFunc("/documents/{id}", pageHandler.DocumentsShow).Methods("GET")
|
||||
r.HandleFunc("/documents/pdf/{id}", documentHandler.GeneratePDF).Methods("GET")
|
||||
|
||||
// Address routes (matching CakePHP)
|
||||
r.HandleFunc("/addresses", addressHandler.List).Methods("GET")
|
||||
r.HandleFunc("/addresses/view/{id}", addressHandler.Get).Methods("GET")
|
||||
r.HandleFunc("/addresses/add/{customerid}", addressHandler.Create).Methods("GET", "POST")
|
||||
r.HandleFunc("/addresses/add_another/{increment}", addressHandler.AddAnother).Methods("GET")
|
||||
r.HandleFunc("/addresses/remove_another/{increment}", addressHandler.RemoveAnother).Methods("GET")
|
||||
r.HandleFunc("/addresses/edit/{id}", addressHandler.Update).Methods("GET", "POST")
|
||||
r.HandleFunc("/addresses/customer_addresses/{customerID}", addressHandler.CustomerAddresses).Methods("GET")
|
||||
|
||||
// Attachment routes (matching CakePHP)
|
||||
r.HandleFunc("/attachments", attachmentHandler.List).Methods("GET")
|
||||
r.HandleFunc("/attachments/view/{id}", attachmentHandler.Get).Methods("GET")
|
||||
r.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET")
|
||||
r.HandleFunc("/attachments/add", attachmentHandler.Create).Methods("GET", "POST")
|
||||
r.HandleFunc("/attachments/edit/{id}", attachmentHandler.Update).Methods("GET", "POST")
|
||||
r.HandleFunc("/attachments/delete/{id}", attachmentHandler.Delete).Methods("POST")
|
||||
|
||||
// Country routes (matching CakePHP)
|
||||
r.HandleFunc("/countries", countryHandler.List).Methods("GET")
|
||||
r.HandleFunc("/countries/view/{id}", countryHandler.Get).Methods("GET")
|
||||
r.HandleFunc("/countries/add", countryHandler.Create).Methods("GET", "POST")
|
||||
r.HandleFunc("/countries/edit/{id}", countryHandler.Update).Methods("GET", "POST")
|
||||
r.HandleFunc("/countries/delete/{id}", countryHandler.Delete).Methods("POST")
|
||||
r.HandleFunc("/countries/complete_country", countryHandler.CompleteCountry).Methods("GET")
|
||||
|
||||
// Status routes (matching CakePHP)
|
||||
r.HandleFunc("/statuses", statusHandler.List).Methods("GET")
|
||||
r.HandleFunc("/statuses/view/{id}", statusHandler.Get).Methods("GET")
|
||||
r.HandleFunc("/statuses/add", statusHandler.Create).Methods("GET", "POST")
|
||||
r.HandleFunc("/statuses/edit/{id}", statusHandler.Update).Methods("GET", "POST")
|
||||
r.HandleFunc("/statuses/delete/{id}", statusHandler.Delete).Methods("POST")
|
||||
r.HandleFunc("/statuses/json_list/{selectedId}", statusHandler.JsonList).Methods("GET")
|
||||
|
||||
// Line Item routes (matching CakePHP)
|
||||
r.HandleFunc("/line_items/ajax_add", lineItemHandler.AjaxAdd).Methods("POST")
|
||||
r.HandleFunc("/line_items/ajax_edit", lineItemHandler.AjaxEdit).Methods("POST")
|
||||
r.HandleFunc("/line_items/ajax_delete/{id}", lineItemHandler.AjaxDelete).Methods("POST")
|
||||
r.HandleFunc("/line_items/get_table/{documentID}", lineItemHandler.GetTable).Methods("GET")
|
||||
r.HandleFunc("/line_items/edit/{id}", lineItemHandler.Update).Methods("GET", "POST")
|
||||
r.HandleFunc("/line_items/add/{documentID}", lineItemHandler.Create).Methods("GET", "POST")
|
||||
|
||||
// HTMX endpoints
|
||||
r.HandleFunc("/customers", customerHandler.Create).Methods("POST")
|
||||
r.HandleFunc("/customers/{id}", customerHandler.Update).Methods("PUT")
|
||||
r.HandleFunc("/customers/{id}", customerHandler.Delete).Methods("DELETE")
|
||||
|
||||
|
||||
r.HandleFunc("/products", productHandler.Create).Methods("POST")
|
||||
r.HandleFunc("/products/{id}", productHandler.Update).Methods("PUT")
|
||||
r.HandleFunc("/products/{id}", productHandler.Delete).Methods("DELETE")
|
||||
|
||||
|
||||
r.HandleFunc("/purchase-orders", purchaseOrderHandler.Create).Methods("POST")
|
||||
r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Update).Methods("PUT")
|
||||
r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Delete).Methods("DELETE")
|
||||
|
||||
|
||||
r.HandleFunc("/enquiries", enquiryHandler.Create).Methods("POST")
|
||||
r.HandleFunc("/enquiries/{id}", enquiryHandler.Update).Methods("PUT")
|
||||
r.HandleFunc("/enquiries/{id}", enquiryHandler.Delete).Methods("DELETE")
|
||||
|
|
@ -160,6 +266,11 @@ func main() {
|
|||
r.HandleFunc("/enquiries/{id}/status", enquiryHandler.UpdateStatus).Methods("PUT")
|
||||
r.HandleFunc("/enquiries/{id}/mark-submitted", enquiryHandler.MarkSubmitted).Methods("PUT")
|
||||
|
||||
r.HandleFunc("/documents", documentHandler.Create).Methods("POST")
|
||||
r.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT")
|
||||
r.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT")
|
||||
r.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT")
|
||||
|
||||
// Start server
|
||||
port := getEnv("PORT", "8080")
|
||||
log.Printf("Starting server on port %s", port)
|
||||
|
|
@ -173,4 +284,4 @@ func getEnv(key, defaultValue string) string {
|
|||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ require (
|
|||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,18 @@
|
|||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
|||
259
go-app/internal/cmc/db/addresses.sql.go
Normal file
259
go-app/internal/cmc/db/addresses.sql.go
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: addresses.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createAddress = `-- name: CreateAddress :execresult
|
||||
INSERT INTO addresses (
|
||||
name, address, city, state_id, country_id,
|
||||
customer_id, type, postcode
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateAddressParams struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
StateID int32 `json:"state_id"`
|
||||
CountryID int32 `json:"country_id"`
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
Type string `json:"type"`
|
||||
Postcode string `json:"postcode"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAddress(ctx context.Context, arg CreateAddressParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createAddress,
|
||||
arg.Name,
|
||||
arg.Address,
|
||||
arg.City,
|
||||
arg.StateID,
|
||||
arg.CountryID,
|
||||
arg.CustomerID,
|
||||
arg.Type,
|
||||
arg.Postcode,
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAddress = `-- name: DeleteAddress :exec
|
||||
DELETE FROM addresses
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAddress(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAddress, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAddress = `-- name: GetAddress :one
|
||||
SELECT id, name, address, city, state_id, country_id, customer_id, type, postcode FROM addresses
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetAddress(ctx context.Context, id int32) (Address, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAddress, id)
|
||||
var i Address
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Address,
|
||||
&i.City,
|
||||
&i.StateID,
|
||||
&i.CountryID,
|
||||
&i.CustomerID,
|
||||
&i.Type,
|
||||
&i.Postcode,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getCustomerAddresses = `-- name: GetCustomerAddresses :many
|
||||
SELECT a.id, a.name, a.address, a.city, a.state_id, a.country_id, a.customer_id, a.type, a.postcode, s.name as state_name, s.shortform as state_shortform, c.name as country_name
|
||||
FROM addresses a
|
||||
LEFT JOIN states s ON a.state_id = s.id
|
||||
LEFT JOIN countries c ON a.country_id = c.id
|
||||
WHERE a.customer_id = ?
|
||||
ORDER BY a.name
|
||||
`
|
||||
|
||||
type GetCustomerAddressesRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
StateID int32 `json:"state_id"`
|
||||
CountryID int32 `json:"country_id"`
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
Type string `json:"type"`
|
||||
Postcode string `json:"postcode"`
|
||||
StateName sql.NullString `json:"state_name"`
|
||||
StateShortform sql.NullString `json:"state_shortform"`
|
||||
CountryName sql.NullString `json:"country_name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetCustomerAddresses(ctx context.Context, customerID int32) ([]GetCustomerAddressesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getCustomerAddresses, customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetCustomerAddressesRow{}
|
||||
for rows.Next() {
|
||||
var i GetCustomerAddressesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Address,
|
||||
&i.City,
|
||||
&i.StateID,
|
||||
&i.CountryID,
|
||||
&i.CustomerID,
|
||||
&i.Type,
|
||||
&i.Postcode,
|
||||
&i.StateName,
|
||||
&i.StateShortform,
|
||||
&i.CountryName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAddresses = `-- name: ListAddresses :many
|
||||
SELECT id, name, address, city, state_id, country_id, customer_id, type, postcode FROM addresses
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListAddressesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListAddresses(ctx context.Context, arg ListAddressesParams) ([]Address, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listAddresses, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Address{}
|
||||
for rows.Next() {
|
||||
var i Address
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Address,
|
||||
&i.City,
|
||||
&i.StateID,
|
||||
&i.CountryID,
|
||||
&i.CustomerID,
|
||||
&i.Type,
|
||||
&i.Postcode,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAddressesByCustomer = `-- name: ListAddressesByCustomer :many
|
||||
SELECT id, name, address, city, state_id, country_id, customer_id, type, postcode FROM addresses
|
||||
WHERE customer_id = ?
|
||||
ORDER BY name
|
||||
`
|
||||
|
||||
func (q *Queries) ListAddressesByCustomer(ctx context.Context, customerID int32) ([]Address, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listAddressesByCustomer, customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Address{}
|
||||
for rows.Next() {
|
||||
var i Address
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Address,
|
||||
&i.City,
|
||||
&i.StateID,
|
||||
&i.CountryID,
|
||||
&i.CustomerID,
|
||||
&i.Type,
|
||||
&i.Postcode,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateAddress = `-- name: UpdateAddress :exec
|
||||
UPDATE addresses
|
||||
SET name = ?,
|
||||
address = ?,
|
||||
city = ?,
|
||||
state_id = ?,
|
||||
country_id = ?,
|
||||
customer_id = ?,
|
||||
type = ?,
|
||||
postcode = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateAddressParams struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
City string `json:"city"`
|
||||
StateID int32 `json:"state_id"`
|
||||
CountryID int32 `json:"country_id"`
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
Type string `json:"type"`
|
||||
Postcode string `json:"postcode"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAddress(ctx context.Context, arg UpdateAddressParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAddress,
|
||||
arg.Name,
|
||||
arg.Address,
|
||||
arg.City,
|
||||
arg.StateID,
|
||||
arg.CountryID,
|
||||
arg.CustomerID,
|
||||
arg.Type,
|
||||
arg.Postcode,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
232
go-app/internal/cmc/db/attachments.sql.go
Normal file
232
go-app/internal/cmc/db/attachments.sql.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: attachments.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createAttachment = `-- name: CreateAttachment :execresult
|
||||
INSERT INTO attachments (
|
||||
principle_id, created, modified, name, filename,
|
||||
file, type, size, description, archived
|
||||
) VALUES (
|
||||
?, NOW(), NOW(), ?, ?, ?, ?, ?, ?, 0
|
||||
)
|
||||
`
|
||||
|
||||
type CreateAttachmentParams struct {
|
||||
PrincipleID int32 `json:"principle_id"`
|
||||
Name string `json:"name"`
|
||||
Filename string `json:"filename"`
|
||||
File string `json:"file"`
|
||||
Type string `json:"type"`
|
||||
Size int32 `json:"size"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAttachment(ctx context.Context, arg CreateAttachmentParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createAttachment,
|
||||
arg.PrincipleID,
|
||||
arg.Name,
|
||||
arg.Filename,
|
||||
arg.File,
|
||||
arg.Type,
|
||||
arg.Size,
|
||||
arg.Description,
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAttachment = `-- name: DeleteAttachment :exec
|
||||
UPDATE attachments
|
||||
SET archived = 1,
|
||||
modified = NOW()
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteAttachment(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteAttachment, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getAttachment = `-- name: GetAttachment :one
|
||||
SELECT id, principle_id, created, modified, name, filename, file, type, size, description, archived FROM attachments
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetAttachment(ctx context.Context, id int32) (Attachment, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAttachment, id)
|
||||
var i Attachment
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.PrincipleID,
|
||||
&i.Created,
|
||||
&i.Modified,
|
||||
&i.Name,
|
||||
&i.Filename,
|
||||
&i.File,
|
||||
&i.Type,
|
||||
&i.Size,
|
||||
&i.Description,
|
||||
&i.Archived,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listArchivedAttachments = `-- name: ListArchivedAttachments :many
|
||||
SELECT id, principle_id, created, modified, name, filename, file, type, size, description, archived FROM attachments
|
||||
WHERE archived = 1
|
||||
ORDER BY created DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListArchivedAttachmentsParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListArchivedAttachments(ctx context.Context, arg ListArchivedAttachmentsParams) ([]Attachment, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listArchivedAttachments, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Attachment{}
|
||||
for rows.Next() {
|
||||
var i Attachment
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.PrincipleID,
|
||||
&i.Created,
|
||||
&i.Modified,
|
||||
&i.Name,
|
||||
&i.Filename,
|
||||
&i.File,
|
||||
&i.Type,
|
||||
&i.Size,
|
||||
&i.Description,
|
||||
&i.Archived,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAttachments = `-- name: ListAttachments :many
|
||||
SELECT id, principle_id, created, modified, name, filename, file, type, size, description, archived FROM attachments
|
||||
WHERE archived = 0
|
||||
ORDER BY created DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListAttachmentsParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListAttachments(ctx context.Context, arg ListAttachmentsParams) ([]Attachment, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listAttachments, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Attachment{}
|
||||
for rows.Next() {
|
||||
var i Attachment
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.PrincipleID,
|
||||
&i.Created,
|
||||
&i.Modified,
|
||||
&i.Name,
|
||||
&i.Filename,
|
||||
&i.File,
|
||||
&i.Type,
|
||||
&i.Size,
|
||||
&i.Description,
|
||||
&i.Archived,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listAttachmentsByPrinciple = `-- name: ListAttachmentsByPrinciple :many
|
||||
SELECT id, principle_id, created, modified, name, filename, file, type, size, description, archived FROM attachments
|
||||
WHERE principle_id = ? AND archived = 0
|
||||
ORDER BY created DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListAttachmentsByPrinciple(ctx context.Context, principleID int32) ([]Attachment, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listAttachmentsByPrinciple, principleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Attachment{}
|
||||
for rows.Next() {
|
||||
var i Attachment
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.PrincipleID,
|
||||
&i.Created,
|
||||
&i.Modified,
|
||||
&i.Name,
|
||||
&i.Filename,
|
||||
&i.File,
|
||||
&i.Type,
|
||||
&i.Size,
|
||||
&i.Description,
|
||||
&i.Archived,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateAttachment = `-- name: UpdateAttachment :exec
|
||||
UPDATE attachments
|
||||
SET modified = NOW(),
|
||||
name = ?,
|
||||
description = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateAttachmentParams struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAttachment(ctx context.Context, arg UpdateAttachmentParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAttachment, arg.Name, arg.Description, arg.ID)
|
||||
return err
|
||||
}
|
||||
171
go-app/internal/cmc/db/boxes.sql.go
Normal file
171
go-app/internal/cmc/db/boxes.sql.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: boxes.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createBox = `-- name: CreateBox :execresult
|
||||
INSERT INTO boxes (
|
||||
shipment_id, length, width, height, weight
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateBoxParams struct {
|
||||
ShipmentID int32 `json:"shipment_id"`
|
||||
Length string `json:"length"`
|
||||
Width string `json:"width"`
|
||||
Height string `json:"height"`
|
||||
Weight string `json:"weight"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateBox(ctx context.Context, arg CreateBoxParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createBox,
|
||||
arg.ShipmentID,
|
||||
arg.Length,
|
||||
arg.Width,
|
||||
arg.Height,
|
||||
arg.Weight,
|
||||
)
|
||||
}
|
||||
|
||||
const deleteBox = `-- name: DeleteBox :exec
|
||||
DELETE FROM boxes
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteBox(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteBox, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getBox = `-- name: GetBox :one
|
||||
SELECT id, shipment_id, length, width, height, weight FROM boxes
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetBox(ctx context.Context, id int32) (Box, error) {
|
||||
row := q.db.QueryRowContext(ctx, getBox, id)
|
||||
var i Box
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ShipmentID,
|
||||
&i.Length,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.Weight,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listBoxes = `-- name: ListBoxes :many
|
||||
SELECT id, shipment_id, length, width, height, weight FROM boxes
|
||||
ORDER BY id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListBoxesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListBoxes(ctx context.Context, arg ListBoxesParams) ([]Box, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listBoxes, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Box{}
|
||||
for rows.Next() {
|
||||
var i Box
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ShipmentID,
|
||||
&i.Length,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.Weight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listBoxesByShipment = `-- name: ListBoxesByShipment :many
|
||||
SELECT id, shipment_id, length, width, height, weight FROM boxes
|
||||
WHERE shipment_id = ?
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListBoxesByShipment(ctx context.Context, shipmentID int32) ([]Box, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listBoxesByShipment, shipmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Box{}
|
||||
for rows.Next() {
|
||||
var i Box
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ShipmentID,
|
||||
&i.Length,
|
||||
&i.Width,
|
||||
&i.Height,
|
||||
&i.Weight,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateBox = `-- name: UpdateBox :exec
|
||||
UPDATE boxes
|
||||
SET length = ?,
|
||||
width = ?,
|
||||
height = ?,
|
||||
weight = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateBoxParams struct {
|
||||
Length string `json:"length"`
|
||||
Width string `json:"width"`
|
||||
Height string `json:"height"`
|
||||
Weight string `json:"weight"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateBox(ctx context.Context, arg UpdateBoxParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateBox,
|
||||
arg.Length,
|
||||
arg.Width,
|
||||
arg.Height,
|
||||
arg.Weight,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
118
go-app/internal/cmc/db/countries.sql.go
Normal file
118
go-app/internal/cmc/db/countries.sql.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: countries.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createCountry = `-- name: CreateCountry :execresult
|
||||
INSERT INTO countries (name) VALUES (?)
|
||||
`
|
||||
|
||||
func (q *Queries) CreateCountry(ctx context.Context, name string) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createCountry, name)
|
||||
}
|
||||
|
||||
const deleteCountry = `-- name: DeleteCountry :exec
|
||||
DELETE FROM countries WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteCountry(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteCountry, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getCountry = `-- name: GetCountry :one
|
||||
SELECT id, name FROM countries
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetCountry(ctx context.Context, id int32) (Country, error) {
|
||||
row := q.db.QueryRowContext(ctx, getCountry, id)
|
||||
var i Country
|
||||
err := row.Scan(&i.ID, &i.Name)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listCountries = `-- name: ListCountries :many
|
||||
SELECT id, name FROM countries
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListCountriesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListCountries(ctx context.Context, arg ListCountriesParams) ([]Country, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listCountries, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Country{}
|
||||
for rows.Next() {
|
||||
var i Country
|
||||
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const searchCountriesByName = `-- name: SearchCountriesByName :many
|
||||
SELECT id, name FROM countries
|
||||
WHERE name LIKE CONCAT('%', ?, '%')
|
||||
ORDER BY name
|
||||
LIMIT 10
|
||||
`
|
||||
|
||||
func (q *Queries) SearchCountriesByName(ctx context.Context, concat interface{}) ([]Country, error) {
|
||||
rows, err := q.db.QueryContext(ctx, searchCountriesByName, concat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Country{}
|
||||
for rows.Next() {
|
||||
var i Country
|
||||
if err := rows.Scan(&i.ID, &i.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateCountry = `-- name: UpdateCountry :exec
|
||||
UPDATE countries SET name = ? WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateCountryParams struct {
|
||||
Name string `json:"name"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCountry(ctx context.Context, arg UpdateCountryParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateCountry, arg.Name, arg.ID)
|
||||
return err
|
||||
}
|
||||
624
go-app/internal/cmc/db/documents.sql.go
Normal file
624
go-app/internal/cmc/db/documents.sql.go
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: documents.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
const archiveDocument = `-- name: ArchiveDocument :exec
|
||||
DELETE FROM documents
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) ArchiveDocument(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, archiveDocument, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const createDocument = `-- name: CreateDocument :execresult
|
||||
INSERT INTO documents (
|
||||
type,
|
||||
created,
|
||||
user_id,
|
||||
doc_page_count,
|
||||
cmc_reference,
|
||||
pdf_filename,
|
||||
pdf_created_at,
|
||||
pdf_created_by_user_id,
|
||||
shipping_details,
|
||||
revision,
|
||||
bill_to,
|
||||
ship_to,
|
||||
email_sent_at,
|
||||
email_sent_by_user_id
|
||||
) VALUES (
|
||||
?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateDocumentParams struct {
|
||||
Type DocumentsType `json:"type"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ShippingDetails sql.NullString `json:"shipping_details"`
|
||||
Revision int32 `json:"revision"`
|
||||
BillTo sql.NullString `json:"bill_to"`
|
||||
ShipTo sql.NullString `json:"ship_to"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateDocument(ctx context.Context, arg CreateDocumentParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createDocument,
|
||||
arg.Type,
|
||||
arg.UserID,
|
||||
arg.DocPageCount,
|
||||
arg.CmcReference,
|
||||
arg.PdfFilename,
|
||||
arg.PdfCreatedAt,
|
||||
arg.PdfCreatedByUserID,
|
||||
arg.ShippingDetails,
|
||||
arg.Revision,
|
||||
arg.BillTo,
|
||||
arg.ShipTo,
|
||||
arg.EmailSentAt,
|
||||
arg.EmailSentByUserID,
|
||||
)
|
||||
}
|
||||
|
||||
const getDocumentByID = `-- name: GetDocumentByID :one
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.shipping_details,
|
||||
d.bill_to,
|
||||
d.ship_to,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE d.id = ?
|
||||
`
|
||||
|
||||
type GetDocumentByIDRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Type DocumentsType `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Revision int32 `json:"revision"`
|
||||
ShippingDetails sql.NullString `json:"shipping_details"`
|
||||
BillTo sql.NullString `json:"bill_to"`
|
||||
ShipTo sql.NullString `json:"ship_to"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
UserFirstName sql.NullString `json:"user_first_name"`
|
||||
UserLastName sql.NullString `json:"user_last_name"`
|
||||
UserUsername sql.NullString `json:"user_username"`
|
||||
PdfCreatorFirstName sql.NullString `json:"pdf_creator_first_name"`
|
||||
PdfCreatorLastName sql.NullString `json:"pdf_creator_last_name"`
|
||||
PdfCreatorUsername sql.NullString `json:"pdf_creator_username"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
EnquiryTitle sql.NullString `json:"enquiry_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDocumentByID(ctx context.Context, id int32) (GetDocumentByIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getDocumentByID, id)
|
||||
var i GetDocumentByIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Created,
|
||||
&i.UserID,
|
||||
&i.DocPageCount,
|
||||
&i.PdfFilename,
|
||||
&i.PdfCreatedAt,
|
||||
&i.PdfCreatedByUserID,
|
||||
&i.CmcReference,
|
||||
&i.Revision,
|
||||
&i.ShippingDetails,
|
||||
&i.BillTo,
|
||||
&i.ShipTo,
|
||||
&i.EmailSentAt,
|
||||
&i.EmailSentByUserID,
|
||||
&i.UserFirstName,
|
||||
&i.UserLastName,
|
||||
&i.UserUsername,
|
||||
&i.PdfCreatorFirstName,
|
||||
&i.PdfCreatorLastName,
|
||||
&i.PdfCreatorUsername,
|
||||
&i.CustomerName,
|
||||
&i.EnquiryTitle,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getPurchaseOrderByDocumentID = `-- name: GetPurchaseOrderByDocumentID :one
|
||||
SELECT
|
||||
po.id,
|
||||
po.title,
|
||||
po.principle_id,
|
||||
po.principle_reference,
|
||||
po.issue_date,
|
||||
po.ordered_from,
|
||||
po.dispatch_by,
|
||||
po.deliver_to,
|
||||
po.shipping_instructions
|
||||
FROM purchase_orders po
|
||||
JOIN documents d ON d.cmc_reference = po.title
|
||||
WHERE d.id = ? AND d.type = 'purchaseOrder'
|
||||
`
|
||||
|
||||
type GetPurchaseOrderByDocumentIDRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
PrincipleID int32 `json:"principle_id"`
|
||||
PrincipleReference string `json:"principle_reference"`
|
||||
IssueDate time.Time `json:"issue_date"`
|
||||
OrderedFrom string `json:"ordered_from"`
|
||||
DispatchBy string `json:"dispatch_by"`
|
||||
DeliverTo string `json:"deliver_to"`
|
||||
ShippingInstructions string `json:"shipping_instructions"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPurchaseOrderByDocumentID(ctx context.Context, id int32) (GetPurchaseOrderByDocumentIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getPurchaseOrderByDocumentID, id)
|
||||
var i GetPurchaseOrderByDocumentIDRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Title,
|
||||
&i.PrincipleID,
|
||||
&i.PrincipleReference,
|
||||
&i.IssueDate,
|
||||
&i.OrderedFrom,
|
||||
&i.DispatchBy,
|
||||
&i.DeliverTo,
|
||||
&i.ShippingInstructions,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listDocuments = `-- name: ListDocuments :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000
|
||||
`
|
||||
|
||||
type ListDocumentsRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Type DocumentsType `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Revision int32 `json:"revision"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
UserFirstName sql.NullString `json:"user_first_name"`
|
||||
UserLastName sql.NullString `json:"user_last_name"`
|
||||
UserUsername sql.NullString `json:"user_username"`
|
||||
PdfCreatorFirstName sql.NullString `json:"pdf_creator_first_name"`
|
||||
PdfCreatorLastName sql.NullString `json:"pdf_creator_last_name"`
|
||||
PdfCreatorUsername sql.NullString `json:"pdf_creator_username"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListDocuments(ctx context.Context) ([]ListDocumentsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDocuments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListDocumentsRow{}
|
||||
for rows.Next() {
|
||||
var i ListDocumentsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Created,
|
||||
&i.UserID,
|
||||
&i.DocPageCount,
|
||||
&i.PdfFilename,
|
||||
&i.PdfCreatedAt,
|
||||
&i.PdfCreatedByUserID,
|
||||
&i.CmcReference,
|
||||
&i.Revision,
|
||||
&i.EmailSentAt,
|
||||
&i.EmailSentByUserID,
|
||||
&i.UserFirstName,
|
||||
&i.UserLastName,
|
||||
&i.UserUsername,
|
||||
&i.PdfCreatorFirstName,
|
||||
&i.PdfCreatorLastName,
|
||||
&i.PdfCreatorUsername,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDocumentsByType = `-- name: ListDocumentsByType :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE d.type = ?
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000
|
||||
`
|
||||
|
||||
type ListDocumentsByTypeRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Type DocumentsType `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Revision int32 `json:"revision"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
UserFirstName sql.NullString `json:"user_first_name"`
|
||||
UserLastName sql.NullString `json:"user_last_name"`
|
||||
UserUsername sql.NullString `json:"user_username"`
|
||||
PdfCreatorFirstName sql.NullString `json:"pdf_creator_first_name"`
|
||||
PdfCreatorLastName sql.NullString `json:"pdf_creator_last_name"`
|
||||
PdfCreatorUsername sql.NullString `json:"pdf_creator_username"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
EnquiryTitle sql.NullString `json:"enquiry_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListDocumentsByType(ctx context.Context, type_ DocumentsType) ([]ListDocumentsByTypeRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listDocumentsByType, type_)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ListDocumentsByTypeRow{}
|
||||
for rows.Next() {
|
||||
var i ListDocumentsByTypeRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Created,
|
||||
&i.UserID,
|
||||
&i.DocPageCount,
|
||||
&i.PdfFilename,
|
||||
&i.PdfCreatedAt,
|
||||
&i.PdfCreatedByUserID,
|
||||
&i.CmcReference,
|
||||
&i.Revision,
|
||||
&i.EmailSentAt,
|
||||
&i.EmailSentByUserID,
|
||||
&i.UserFirstName,
|
||||
&i.UserLastName,
|
||||
&i.UserUsername,
|
||||
&i.PdfCreatorFirstName,
|
||||
&i.PdfCreatorLastName,
|
||||
&i.PdfCreatorUsername,
|
||||
&i.CustomerName,
|
||||
&i.EnquiryTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const searchDocuments = `-- name: SearchDocuments :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE (
|
||||
d.pdf_filename LIKE ? OR
|
||||
d.cmc_reference LIKE ? OR
|
||||
COALESCE(ec.name, ic.name) LIKE ? OR
|
||||
e.title LIKE ? OR
|
||||
u.username LIKE ?
|
||||
)
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000
|
||||
`
|
||||
|
||||
type SearchDocumentsParams struct {
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type SearchDocumentsRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Type DocumentsType `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
Revision int32 `json:"revision"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
UserFirstName sql.NullString `json:"user_first_name"`
|
||||
UserLastName sql.NullString `json:"user_last_name"`
|
||||
UserUsername sql.NullString `json:"user_username"`
|
||||
PdfCreatorFirstName sql.NullString `json:"pdf_creator_first_name"`
|
||||
PdfCreatorLastName sql.NullString `json:"pdf_creator_last_name"`
|
||||
PdfCreatorUsername sql.NullString `json:"pdf_creator_username"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
EnquiryTitle sql.NullString `json:"enquiry_title"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchDocuments(ctx context.Context, arg SearchDocumentsParams) ([]SearchDocumentsRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, searchDocuments,
|
||||
arg.PdfFilename,
|
||||
arg.CmcReference,
|
||||
arg.Name,
|
||||
arg.Title,
|
||||
arg.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SearchDocumentsRow{}
|
||||
for rows.Next() {
|
||||
var i SearchDocumentsRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Type,
|
||||
&i.Created,
|
||||
&i.UserID,
|
||||
&i.DocPageCount,
|
||||
&i.PdfFilename,
|
||||
&i.PdfCreatedAt,
|
||||
&i.PdfCreatedByUserID,
|
||||
&i.CmcReference,
|
||||
&i.Revision,
|
||||
&i.EmailSentAt,
|
||||
&i.EmailSentByUserID,
|
||||
&i.UserFirstName,
|
||||
&i.UserLastName,
|
||||
&i.UserUsername,
|
||||
&i.PdfCreatorFirstName,
|
||||
&i.PdfCreatorLastName,
|
||||
&i.PdfCreatorUsername,
|
||||
&i.CustomerName,
|
||||
&i.EnquiryTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const unarchiveDocument = `-- name: UnarchiveDocument :exec
|
||||
SELECT 1 WHERE ? = ?
|
||||
`
|
||||
|
||||
type UnarchiveDocumentParams struct {
|
||||
Column1 interface{} `json:"column_1"`
|
||||
Column2 interface{} `json:"column_2"`
|
||||
}
|
||||
|
||||
// Note: Unarchiving not supported as documents table doesn't have an archived column
|
||||
// This is a no-op for compatibility
|
||||
func (q *Queries) UnarchiveDocument(ctx context.Context, arg UnarchiveDocumentParams) error {
|
||||
_, err := q.db.ExecContext(ctx, unarchiveDocument, arg.Column1, arg.Column2)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateDocument = `-- name: UpdateDocument :exec
|
||||
UPDATE documents
|
||||
SET
|
||||
type = ?,
|
||||
user_id = ?,
|
||||
doc_page_count = ?,
|
||||
cmc_reference = ?,
|
||||
pdf_filename = ?,
|
||||
pdf_created_at = ?,
|
||||
pdf_created_by_user_id = ?,
|
||||
shipping_details = ?,
|
||||
revision = ?,
|
||||
bill_to = ?,
|
||||
ship_to = ?,
|
||||
email_sent_at = ?,
|
||||
email_sent_by_user_id = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateDocumentParams struct {
|
||||
Type DocumentsType `json:"type"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ShippingDetails sql.NullString `json:"shipping_details"`
|
||||
Revision int32 `json:"revision"`
|
||||
BillTo sql.NullString `json:"bill_to"`
|
||||
ShipTo sql.NullString `json:"ship_to"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateDocument(ctx context.Context, arg UpdateDocumentParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateDocument,
|
||||
arg.Type,
|
||||
arg.UserID,
|
||||
arg.DocPageCount,
|
||||
arg.CmcReference,
|
||||
arg.PdfFilename,
|
||||
arg.PdfCreatedAt,
|
||||
arg.PdfCreatedByUserID,
|
||||
arg.ShippingDetails,
|
||||
arg.Revision,
|
||||
arg.BillTo,
|
||||
arg.ShipTo,
|
||||
arg.EmailSentAt,
|
||||
arg.EmailSentByUserID,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateDocumentPDFInfo = `-- name: UpdateDocumentPDFInfo :exec
|
||||
UPDATE documents
|
||||
SET
|
||||
pdf_filename = ?,
|
||||
pdf_created_at = ?,
|
||||
pdf_created_by_user_id = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateDocumentPDFInfoParams struct {
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateDocumentPDFInfo(ctx context.Context, arg UpdateDocumentPDFInfoParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateDocumentPDFInfo,
|
||||
arg.PdfFilename,
|
||||
arg.PdfCreatedAt,
|
||||
arg.PdfCreatedByUserID,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
461
go-app/internal/cmc/db/line_items.sql.go
Normal file
461
go-app/internal/cmc/db/line_items.sql.go
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: line_items.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createLineItem = `-- name: CreateLineItem :execresult
|
||||
INSERT INTO line_items (
|
||||
item_number, ` + "`" + `option` + "`" + `, quantity, title, description,
|
||||
document_id, product_id, has_text_prices, has_price,
|
||||
unit_price_string, gross_price_string, costing_id,
|
||||
gross_unit_price, net_unit_price, discount_percent,
|
||||
discount_amount_unit, discount_amount_total,
|
||||
gross_price, net_price
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateLineItemParams struct {
|
||||
ItemNumber string `json:"item_number"`
|
||||
Option bool `json:"option"`
|
||||
Quantity string `json:"quantity"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
ProductID sql.NullInt32 `json:"product_id"`
|
||||
HasTextPrices bool `json:"has_text_prices"`
|
||||
HasPrice int8 `json:"has_price"`
|
||||
UnitPriceString sql.NullString `json:"unit_price_string"`
|
||||
GrossPriceString sql.NullString `json:"gross_price_string"`
|
||||
CostingID sql.NullInt32 `json:"costing_id"`
|
||||
GrossUnitPrice sql.NullString `json:"gross_unit_price"`
|
||||
NetUnitPrice sql.NullString `json:"net_unit_price"`
|
||||
DiscountPercent sql.NullString `json:"discount_percent"`
|
||||
DiscountAmountUnit sql.NullString `json:"discount_amount_unit"`
|
||||
DiscountAmountTotal sql.NullString `json:"discount_amount_total"`
|
||||
GrossPrice sql.NullString `json:"gross_price"`
|
||||
NetPrice sql.NullString `json:"net_price"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLineItem(ctx context.Context, arg CreateLineItemParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createLineItem,
|
||||
arg.ItemNumber,
|
||||
arg.Option,
|
||||
arg.Quantity,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.DocumentID,
|
||||
arg.ProductID,
|
||||
arg.HasTextPrices,
|
||||
arg.HasPrice,
|
||||
arg.UnitPriceString,
|
||||
arg.GrossPriceString,
|
||||
arg.CostingID,
|
||||
arg.GrossUnitPrice,
|
||||
arg.NetUnitPrice,
|
||||
arg.DiscountPercent,
|
||||
arg.DiscountAmountUnit,
|
||||
arg.DiscountAmountTotal,
|
||||
arg.GrossPrice,
|
||||
arg.NetPrice,
|
||||
)
|
||||
}
|
||||
|
||||
const deleteLineItem = `-- name: DeleteLineItem :exec
|
||||
DELETE FROM line_items
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteLineItem(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteLineItem, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getLineItem = `-- name: GetLineItem :one
|
||||
SELECT id, item_number, ` + "`" + `option` + "`" + `, quantity, title, description, document_id, product_id, has_text_prices, has_price, unit_price_string, gross_price_string, costing_id, gross_unit_price, net_unit_price, discount_percent, discount_amount_unit, discount_amount_total, gross_price, net_price FROM line_items
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLineItem(ctx context.Context, id int32) (LineItem, error) {
|
||||
row := q.db.QueryRowContext(ctx, getLineItem, id)
|
||||
var i LineItem
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ItemNumber,
|
||||
&i.Option,
|
||||
&i.Quantity,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DocumentID,
|
||||
&i.ProductID,
|
||||
&i.HasTextPrices,
|
||||
&i.HasPrice,
|
||||
&i.UnitPriceString,
|
||||
&i.GrossPriceString,
|
||||
&i.CostingID,
|
||||
&i.GrossUnitPrice,
|
||||
&i.NetUnitPrice,
|
||||
&i.DiscountPercent,
|
||||
&i.DiscountAmountUnit,
|
||||
&i.DiscountAmountTotal,
|
||||
&i.GrossPrice,
|
||||
&i.NetPrice,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getLineItemsByProduct = `-- name: GetLineItemsByProduct :many
|
||||
SELECT id, item_number, ` + "`" + `option` + "`" + `, quantity, title, description, document_id, product_id, has_text_prices, has_price, unit_price_string, gross_price_string, costing_id, gross_unit_price, net_unit_price, discount_percent, discount_amount_unit, discount_amount_total, gross_price, net_price FROM line_items
|
||||
WHERE product_id = ?
|
||||
ORDER BY item_number ASC
|
||||
`
|
||||
|
||||
func (q *Queries) GetLineItemsByProduct(ctx context.Context, productID sql.NullInt32) ([]LineItem, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLineItemsByProduct, productID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []LineItem{}
|
||||
for rows.Next() {
|
||||
var i LineItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ItemNumber,
|
||||
&i.Option,
|
||||
&i.Quantity,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DocumentID,
|
||||
&i.ProductID,
|
||||
&i.HasTextPrices,
|
||||
&i.HasPrice,
|
||||
&i.UnitPriceString,
|
||||
&i.GrossPriceString,
|
||||
&i.CostingID,
|
||||
&i.GrossUnitPrice,
|
||||
&i.NetUnitPrice,
|
||||
&i.DiscountPercent,
|
||||
&i.DiscountAmountUnit,
|
||||
&i.DiscountAmountTotal,
|
||||
&i.GrossPrice,
|
||||
&i.NetPrice,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getLineItemsTable = `-- name: GetLineItemsTable :many
|
||||
SELECT li.id, li.item_number, li.` + "`" + `option` + "`" + `, li.quantity, li.title, li.description, li.document_id, li.product_id, li.has_text_prices, li.has_price, li.unit_price_string, li.gross_price_string, li.costing_id, li.gross_unit_price, li.net_unit_price, li.discount_percent, li.discount_amount_unit, li.discount_amount_total, li.gross_price, li.net_price, p.title as product_title, p.model_number
|
||||
FROM line_items li
|
||||
LEFT JOIN products p ON li.product_id = p.id
|
||||
WHERE li.document_id = ?
|
||||
ORDER BY li.item_number ASC
|
||||
`
|
||||
|
||||
type GetLineItemsTableRow struct {
|
||||
ID int32 `json:"id"`
|
||||
ItemNumber string `json:"item_number"`
|
||||
Option bool `json:"option"`
|
||||
Quantity string `json:"quantity"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
ProductID sql.NullInt32 `json:"product_id"`
|
||||
HasTextPrices bool `json:"has_text_prices"`
|
||||
HasPrice int8 `json:"has_price"`
|
||||
UnitPriceString sql.NullString `json:"unit_price_string"`
|
||||
GrossPriceString sql.NullString `json:"gross_price_string"`
|
||||
CostingID sql.NullInt32 `json:"costing_id"`
|
||||
GrossUnitPrice sql.NullString `json:"gross_unit_price"`
|
||||
NetUnitPrice sql.NullString `json:"net_unit_price"`
|
||||
DiscountPercent sql.NullString `json:"discount_percent"`
|
||||
DiscountAmountUnit sql.NullString `json:"discount_amount_unit"`
|
||||
DiscountAmountTotal sql.NullString `json:"discount_amount_total"`
|
||||
GrossPrice sql.NullString `json:"gross_price"`
|
||||
NetPrice sql.NullString `json:"net_price"`
|
||||
ProductTitle sql.NullString `json:"product_title"`
|
||||
ModelNumber sql.NullString `json:"model_number"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetLineItemsTable(ctx context.Context, documentID int32) ([]GetLineItemsTableRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getLineItemsTable, documentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetLineItemsTableRow{}
|
||||
for rows.Next() {
|
||||
var i GetLineItemsTableRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ItemNumber,
|
||||
&i.Option,
|
||||
&i.Quantity,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DocumentID,
|
||||
&i.ProductID,
|
||||
&i.HasTextPrices,
|
||||
&i.HasPrice,
|
||||
&i.UnitPriceString,
|
||||
&i.GrossPriceString,
|
||||
&i.CostingID,
|
||||
&i.GrossUnitPrice,
|
||||
&i.NetUnitPrice,
|
||||
&i.DiscountPercent,
|
||||
&i.DiscountAmountUnit,
|
||||
&i.DiscountAmountTotal,
|
||||
&i.GrossPrice,
|
||||
&i.NetPrice,
|
||||
&i.ProductTitle,
|
||||
&i.ModelNumber,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMaxItemNumber = `-- name: GetMaxItemNumber :one
|
||||
SELECT COALESCE(MAX(item_number), 0) as max_item_number
|
||||
FROM line_items
|
||||
WHERE document_id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetMaxItemNumber(ctx context.Context, documentID int32) (interface{}, error) {
|
||||
row := q.db.QueryRowContext(ctx, getMaxItemNumber, documentID)
|
||||
var max_item_number interface{}
|
||||
err := row.Scan(&max_item_number)
|
||||
return max_item_number, err
|
||||
}
|
||||
|
||||
const listLineItems = `-- name: ListLineItems :many
|
||||
SELECT id, item_number, ` + "`" + `option` + "`" + `, quantity, title, description, document_id, product_id, has_text_prices, has_price, unit_price_string, gross_price_string, costing_id, gross_unit_price, net_unit_price, discount_percent, discount_amount_unit, discount_amount_total, gross_price, net_price FROM line_items
|
||||
ORDER BY item_number ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListLineItemsParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLineItems(ctx context.Context, arg ListLineItemsParams) ([]LineItem, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listLineItems, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []LineItem{}
|
||||
for rows.Next() {
|
||||
var i LineItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ItemNumber,
|
||||
&i.Option,
|
||||
&i.Quantity,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DocumentID,
|
||||
&i.ProductID,
|
||||
&i.HasTextPrices,
|
||||
&i.HasPrice,
|
||||
&i.UnitPriceString,
|
||||
&i.GrossPriceString,
|
||||
&i.CostingID,
|
||||
&i.GrossUnitPrice,
|
||||
&i.NetUnitPrice,
|
||||
&i.DiscountPercent,
|
||||
&i.DiscountAmountUnit,
|
||||
&i.DiscountAmountTotal,
|
||||
&i.GrossPrice,
|
||||
&i.NetPrice,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLineItemsByDocument = `-- name: ListLineItemsByDocument :many
|
||||
SELECT id, item_number, ` + "`" + `option` + "`" + `, quantity, title, description, document_id, product_id, has_text_prices, has_price, unit_price_string, gross_price_string, costing_id, gross_unit_price, net_unit_price, discount_percent, discount_amount_unit, discount_amount_total, gross_price, net_price FROM line_items
|
||||
WHERE document_id = ?
|
||||
ORDER BY item_number ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListLineItemsByDocument(ctx context.Context, documentID int32) ([]LineItem, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listLineItemsByDocument, documentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []LineItem{}
|
||||
for rows.Next() {
|
||||
var i LineItem
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ItemNumber,
|
||||
&i.Option,
|
||||
&i.Quantity,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.DocumentID,
|
||||
&i.ProductID,
|
||||
&i.HasTextPrices,
|
||||
&i.HasPrice,
|
||||
&i.UnitPriceString,
|
||||
&i.GrossPriceString,
|
||||
&i.CostingID,
|
||||
&i.GrossUnitPrice,
|
||||
&i.NetUnitPrice,
|
||||
&i.DiscountPercent,
|
||||
&i.DiscountAmountUnit,
|
||||
&i.DiscountAmountTotal,
|
||||
&i.GrossPrice,
|
||||
&i.NetPrice,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateLineItem = `-- name: UpdateLineItem :exec
|
||||
UPDATE line_items
|
||||
SET item_number = ?,
|
||||
` + "`" + `option` + "`" + ` = ?,
|
||||
quantity = ?,
|
||||
title = ?,
|
||||
description = ?,
|
||||
document_id = ?,
|
||||
product_id = ?,
|
||||
has_text_prices = ?,
|
||||
has_price = ?,
|
||||
unit_price_string = ?,
|
||||
gross_price_string = ?,
|
||||
costing_id = ?,
|
||||
gross_unit_price = ?,
|
||||
net_unit_price = ?,
|
||||
discount_percent = ?,
|
||||
discount_amount_unit = ?,
|
||||
discount_amount_total = ?,
|
||||
gross_price = ?,
|
||||
net_price = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateLineItemParams struct {
|
||||
ItemNumber string `json:"item_number"`
|
||||
Option bool `json:"option"`
|
||||
Quantity string `json:"quantity"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
ProductID sql.NullInt32 `json:"product_id"`
|
||||
HasTextPrices bool `json:"has_text_prices"`
|
||||
HasPrice int8 `json:"has_price"`
|
||||
UnitPriceString sql.NullString `json:"unit_price_string"`
|
||||
GrossPriceString sql.NullString `json:"gross_price_string"`
|
||||
CostingID sql.NullInt32 `json:"costing_id"`
|
||||
GrossUnitPrice sql.NullString `json:"gross_unit_price"`
|
||||
NetUnitPrice sql.NullString `json:"net_unit_price"`
|
||||
DiscountPercent sql.NullString `json:"discount_percent"`
|
||||
DiscountAmountUnit sql.NullString `json:"discount_amount_unit"`
|
||||
DiscountAmountTotal sql.NullString `json:"discount_amount_total"`
|
||||
GrossPrice sql.NullString `json:"gross_price"`
|
||||
NetPrice sql.NullString `json:"net_price"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateLineItem(ctx context.Context, arg UpdateLineItemParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateLineItem,
|
||||
arg.ItemNumber,
|
||||
arg.Option,
|
||||
arg.Quantity,
|
||||
arg.Title,
|
||||
arg.Description,
|
||||
arg.DocumentID,
|
||||
arg.ProductID,
|
||||
arg.HasTextPrices,
|
||||
arg.HasPrice,
|
||||
arg.UnitPriceString,
|
||||
arg.GrossPriceString,
|
||||
arg.CostingID,
|
||||
arg.GrossUnitPrice,
|
||||
arg.NetUnitPrice,
|
||||
arg.DiscountPercent,
|
||||
arg.DiscountAmountUnit,
|
||||
arg.DiscountAmountTotal,
|
||||
arg.GrossPrice,
|
||||
arg.NetPrice,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateLineItemPrices = `-- name: UpdateLineItemPrices :exec
|
||||
UPDATE line_items
|
||||
SET gross_unit_price = ?,
|
||||
net_unit_price = ?,
|
||||
gross_price = ?,
|
||||
net_price = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateLineItemPricesParams struct {
|
||||
GrossUnitPrice sql.NullString `json:"gross_unit_price"`
|
||||
NetUnitPrice sql.NullString `json:"net_unit_price"`
|
||||
GrossPrice sql.NullString `json:"gross_price"`
|
||||
NetPrice sql.NullString `json:"net_price"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateLineItemPrices(ctx context.Context, arg UpdateLineItemPricesParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateLineItemPrices,
|
||||
arg.GrossUnitPrice,
|
||||
arg.NetUnitPrice,
|
||||
arg.GrossPrice,
|
||||
arg.NetPrice,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -11,6 +11,51 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
type DocumentsType string
|
||||
|
||||
const (
|
||||
DocumentsTypeQuote DocumentsType = "quote"
|
||||
DocumentsTypeInvoice DocumentsType = "invoice"
|
||||
DocumentsTypePurchaseOrder DocumentsType = "purchaseOrder"
|
||||
DocumentsTypeOrderAck DocumentsType = "orderAck"
|
||||
DocumentsTypePackingList DocumentsType = "packingList"
|
||||
)
|
||||
|
||||
func (e *DocumentsType) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = DocumentsType(s)
|
||||
case string:
|
||||
*e = DocumentsType(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for DocumentsType: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullDocumentsType struct {
|
||||
DocumentsType DocumentsType `json:"documents_type"`
|
||||
Valid bool `json:"valid"` // Valid is true if DocumentsType is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullDocumentsType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.DocumentsType, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.DocumentsType.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullDocumentsType) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.DocumentsType), nil
|
||||
}
|
||||
|
||||
type UsersAccessLevel string
|
||||
|
||||
const (
|
||||
|
|
@ -98,6 +143,48 @@ func (ns NullUsersType) Value() (driver.Value, error) {
|
|||
return string(ns.UsersType), nil
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
ID int32 `json:"id"`
|
||||
// Descriptive Name for this address
|
||||
Name string `json:"name"`
|
||||
// street or unit number and street name
|
||||
Address string `json:"address"`
|
||||
// Suburb / City
|
||||
City string `json:"city"`
|
||||
// State foreign Key
|
||||
StateID int32 `json:"state_id"`
|
||||
// Country foreign Key
|
||||
CountryID int32 `json:"country_id"`
|
||||
// Customer foreign key
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
// either bill / ship / both
|
||||
Type string `json:"type"`
|
||||
Postcode string `json:"postcode"`
|
||||
}
|
||||
|
||||
type Attachment struct {
|
||||
ID int32 `json:"id"`
|
||||
PrincipleID int32 `json:"principle_id"`
|
||||
Created time.Time `json:"created"`
|
||||
Modified time.Time `json:"modified"`
|
||||
Name string `json:"name"`
|
||||
Filename string `json:"filename"`
|
||||
File string `json:"file"`
|
||||
Type string `json:"type"`
|
||||
Size int32 `json:"size"`
|
||||
Description string `json:"description"`
|
||||
Archived bool `json:"archived"`
|
||||
}
|
||||
|
||||
type Box struct {
|
||||
ID int32 `json:"id"`
|
||||
ShipmentID int32 `json:"shipment_id"`
|
||||
Length string `json:"length"`
|
||||
Width string `json:"width"`
|
||||
Height string `json:"height"`
|
||||
Weight string `json:"weight"`
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
@ -118,6 +205,25 @@ type Customer struct {
|
|||
CountryID int32 `json:"country_id"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID int32 `json:"id"`
|
||||
Type DocumentsType `json:"type"`
|
||||
Created time.Time `json:"created"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
// Either the Enquiry number, Invoice no, order ack. Convenient place to store this to save on queries
|
||||
CmcReference string `json:"cmc_reference"`
|
||||
PdfFilename string `json:"pdf_filename"`
|
||||
PdfCreatedAt time.Time `json:"pdf_created_at"`
|
||||
PdfCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ShippingDetails sql.NullString `json:"shipping_details"`
|
||||
Revision int32 `json:"revision"`
|
||||
BillTo sql.NullString `json:"bill_to"`
|
||||
ShipTo sql.NullString `json:"ship_to"`
|
||||
EmailSentAt time.Time `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
}
|
||||
|
||||
type Enquiry struct {
|
||||
ID int32 `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
|
|
@ -148,6 +254,37 @@ type Enquiry struct {
|
|||
Archived int8 `json:"archived"`
|
||||
}
|
||||
|
||||
type Invoice struct {
|
||||
ID int32 `json:"id"`
|
||||
// CMC Invoice Number String
|
||||
Title string `json:"title"`
|
||||
CustomerID int32 `json:"customer_id"`
|
||||
}
|
||||
|
||||
type LineItem struct {
|
||||
ID int32 `json:"id"`
|
||||
ItemNumber string `json:"item_number"`
|
||||
Option bool `json:"option"`
|
||||
Quantity string `json:"quantity"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
ProductID sql.NullInt32 `json:"product_id"`
|
||||
HasTextPrices bool `json:"has_text_prices"`
|
||||
HasPrice int8 `json:"has_price"`
|
||||
UnitPriceString sql.NullString `json:"unit_price_string"`
|
||||
GrossPriceString sql.NullString `json:"gross_price_string"`
|
||||
CostingID sql.NullInt32 `json:"costing_id"`
|
||||
// Either fill this in or have a costing_id associated with this record
|
||||
GrossUnitPrice sql.NullString `json:"gross_unit_price"`
|
||||
NetUnitPrice sql.NullString `json:"net_unit_price"`
|
||||
DiscountPercent sql.NullString `json:"discount_percent"`
|
||||
DiscountAmountUnit sql.NullString `json:"discount_amount_unit"`
|
||||
DiscountAmountTotal sql.NullString `json:"discount_amount_total"`
|
||||
GrossPrice sql.NullString `json:"gross_price"`
|
||||
NetPrice sql.NullString `json:"net_price"`
|
||||
}
|
||||
|
||||
type Principle struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
|
|||
143
go-app/internal/cmc/db/principles.sql.go
Normal file
143
go-app/internal/cmc/db/principles.sql.go
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: principles.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createPrinciple = `-- name: CreatePrinciple :execresult
|
||||
INSERT INTO principles (name, short_name, code) VALUES (?, ?, ?)
|
||||
`
|
||||
|
||||
type CreatePrincipleParams struct {
|
||||
Name string `json:"name"`
|
||||
ShortName sql.NullString `json:"short_name"`
|
||||
Code int32 `json:"code"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreatePrinciple(ctx context.Context, arg CreatePrincipleParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createPrinciple, arg.Name, arg.ShortName, arg.Code)
|
||||
}
|
||||
|
||||
const getPrinciple = `-- name: GetPrinciple :one
|
||||
SELECT id, name, short_name, code FROM principles
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetPrinciple(ctx context.Context, id int32) (Principle, error) {
|
||||
row := q.db.QueryRowContext(ctx, getPrinciple, id)
|
||||
var i Principle
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.ShortName,
|
||||
&i.Code,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getPrincipleProducts = `-- name: GetPrincipleProducts :many
|
||||
SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products
|
||||
WHERE principle_id = ?
|
||||
ORDER BY title
|
||||
`
|
||||
|
||||
func (q *Queries) GetPrincipleProducts(ctx context.Context, principleID int32) ([]Product, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getPrincipleProducts, principleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Product{}
|
||||
for rows.Next() {
|
||||
var i Product
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.PrincipleID,
|
||||
&i.ProductCategoryID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.ModelNumber,
|
||||
&i.ModelNumberFormat,
|
||||
&i.Notes,
|
||||
&i.Stock,
|
||||
&i.ItemCode,
|
||||
&i.ItemDescription,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listPrinciples = `-- name: ListPrinciples :many
|
||||
SELECT id, name, short_name, code FROM principles
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListPrinciplesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListPrinciples(ctx context.Context, arg ListPrinciplesParams) ([]Principle, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listPrinciples, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Principle{}
|
||||
for rows.Next() {
|
||||
var i Principle
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.ShortName,
|
||||
&i.Code,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updatePrinciple = `-- name: UpdatePrinciple :exec
|
||||
UPDATE principles SET name = ?, short_name = ?, code = ? WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdatePrincipleParams struct {
|
||||
Name string `json:"name"`
|
||||
ShortName sql.NullString `json:"short_name"`
|
||||
Code int32 `json:"code"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePrinciple(ctx context.Context, arg UpdatePrincipleParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updatePrinciple,
|
||||
arg.Name,
|
||||
arg.ShortName,
|
||||
arg.Code,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
@ -10,49 +10,119 @@ import (
|
|||
)
|
||||
|
||||
type Querier interface {
|
||||
ArchiveDocument(ctx context.Context, id int32) error
|
||||
ArchiveEnquiry(ctx context.Context, id int32) error
|
||||
ArchiveUser(ctx context.Context, id int32) error
|
||||
CountEnquiries(ctx context.Context) (int64, error)
|
||||
CountEnquiriesByPrinciple(ctx context.Context, principleCode int32) (int64, error)
|
||||
CountEnquiriesByPrincipleAndState(ctx context.Context, arg CountEnquiriesByPrincipleAndStateParams) (int64, error)
|
||||
CountEnquiriesByStatus(ctx context.Context, statusID int32) (int64, error)
|
||||
CreateAddress(ctx context.Context, arg CreateAddressParams) (sql.Result, error)
|
||||
CreateAttachment(ctx context.Context, arg CreateAttachmentParams) (sql.Result, error)
|
||||
CreateBox(ctx context.Context, arg CreateBoxParams) (sql.Result, error)
|
||||
CreateCountry(ctx context.Context, name string) (sql.Result, error)
|
||||
CreateCustomer(ctx context.Context, arg CreateCustomerParams) (sql.Result, error)
|
||||
CreateDocument(ctx context.Context, arg CreateDocumentParams) (sql.Result, error)
|
||||
CreateEnquiry(ctx context.Context, arg CreateEnquiryParams) (sql.Result, error)
|
||||
CreateLineItem(ctx context.Context, arg CreateLineItemParams) (sql.Result, error)
|
||||
CreatePrinciple(ctx context.Context, arg CreatePrincipleParams) (sql.Result, error)
|
||||
CreateProduct(ctx context.Context, arg CreateProductParams) (sql.Result, error)
|
||||
CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (sql.Result, error)
|
||||
CreateState(ctx context.Context, arg CreateStateParams) (sql.Result, error)
|
||||
CreateStatus(ctx context.Context, arg CreateStatusParams) (sql.Result, error)
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error)
|
||||
DeleteAddress(ctx context.Context, id int32) error
|
||||
DeleteAttachment(ctx context.Context, id int32) error
|
||||
DeleteBox(ctx context.Context, id int32) error
|
||||
DeleteCountry(ctx context.Context, id int32) error
|
||||
DeleteCustomer(ctx context.Context, id int32) error
|
||||
DeleteLineItem(ctx context.Context, id int32) error
|
||||
DeleteProduct(ctx context.Context, id int32) error
|
||||
DeletePurchaseOrder(ctx context.Context, id int32) error
|
||||
DeleteState(ctx context.Context, id int32) error
|
||||
DeleteStatus(ctx context.Context, id int32) error
|
||||
GetAddress(ctx context.Context, id int32) (Address, error)
|
||||
GetAllCountries(ctx context.Context) ([]Country, error)
|
||||
GetAllPrinciples(ctx context.Context) ([]Principle, error)
|
||||
GetAllStates(ctx context.Context) ([]State, error)
|
||||
GetAllStatuses(ctx context.Context) ([]GetAllStatusesRow, error)
|
||||
GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error)
|
||||
GetAttachment(ctx context.Context, id int32) (Attachment, error)
|
||||
GetBox(ctx context.Context, id int32) (Box, error)
|
||||
GetCountry(ctx context.Context, id int32) (Country, error)
|
||||
GetCustomer(ctx context.Context, id int32) (Customer, error)
|
||||
GetCustomerAddresses(ctx context.Context, customerID int32) ([]GetCustomerAddressesRow, error)
|
||||
GetCustomerByABN(ctx context.Context, abn sql.NullString) (Customer, error)
|
||||
GetDocumentByID(ctx context.Context, id int32) (GetDocumentByIDRow, error)
|
||||
GetEnquiriesByCustomer(ctx context.Context, arg GetEnquiriesByCustomerParams) ([]GetEnquiriesByCustomerRow, error)
|
||||
GetEnquiriesByUser(ctx context.Context, arg GetEnquiriesByUserParams) ([]GetEnquiriesByUserRow, error)
|
||||
GetEnquiry(ctx context.Context, id int32) (GetEnquiryRow, error)
|
||||
GetLineItem(ctx context.Context, id int32) (LineItem, error)
|
||||
GetLineItemsByProduct(ctx context.Context, productID sql.NullInt32) ([]LineItem, error)
|
||||
GetLineItemsTable(ctx context.Context, documentID int32) ([]GetLineItemsTableRow, error)
|
||||
GetMaxItemNumber(ctx context.Context, documentID int32) (interface{}, error)
|
||||
GetPrinciple(ctx context.Context, id int32) (Principle, error)
|
||||
GetPrincipleProducts(ctx context.Context, principleID int32) ([]Product, error)
|
||||
GetProduct(ctx context.Context, id int32) (Product, error)
|
||||
GetProductByItemCode(ctx context.Context, itemCode string) (Product, error)
|
||||
GetProductsByCategory(ctx context.Context, arg GetProductsByCategoryParams) ([]Product, error)
|
||||
GetPurchaseOrder(ctx context.Context, id int32) (PurchaseOrder, error)
|
||||
GetPurchaseOrderByDocumentID(ctx context.Context, id int32) (GetPurchaseOrderByDocumentIDRow, error)
|
||||
GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error)
|
||||
GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error)
|
||||
GetState(ctx context.Context, id int32) (State, error)
|
||||
GetStatus(ctx context.Context, id int32) (Status, error)
|
||||
GetUser(ctx context.Context, id int32) (GetUserRow, error)
|
||||
GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error)
|
||||
ListAddresses(ctx context.Context, arg ListAddressesParams) ([]Address, error)
|
||||
ListAddressesByCustomer(ctx context.Context, customerID int32) ([]Address, error)
|
||||
ListArchivedAttachments(ctx context.Context, arg ListArchivedAttachmentsParams) ([]Attachment, error)
|
||||
ListArchivedEnquiries(ctx context.Context, arg ListArchivedEnquiriesParams) ([]ListArchivedEnquiriesRow, error)
|
||||
ListAttachments(ctx context.Context, arg ListAttachmentsParams) ([]Attachment, error)
|
||||
ListAttachmentsByPrinciple(ctx context.Context, principleID int32) ([]Attachment, error)
|
||||
ListBoxes(ctx context.Context, arg ListBoxesParams) ([]Box, error)
|
||||
ListBoxesByShipment(ctx context.Context, shipmentID int32) ([]Box, error)
|
||||
ListCountries(ctx context.Context, arg ListCountriesParams) ([]Country, error)
|
||||
ListCustomers(ctx context.Context, arg ListCustomersParams) ([]Customer, error)
|
||||
ListDocuments(ctx context.Context) ([]ListDocumentsRow, error)
|
||||
ListDocumentsByType(ctx context.Context, type_ DocumentsType) ([]ListDocumentsByTypeRow, error)
|
||||
ListEnquiries(ctx context.Context, arg ListEnquiriesParams) ([]ListEnquiriesRow, error)
|
||||
ListLineItems(ctx context.Context, arg ListLineItemsParams) ([]LineItem, error)
|
||||
ListLineItemsByDocument(ctx context.Context, documentID int32) ([]LineItem, error)
|
||||
ListPrinciples(ctx context.Context, arg ListPrinciplesParams) ([]Principle, error)
|
||||
ListProducts(ctx context.Context, arg ListProductsParams) ([]Product, error)
|
||||
ListPurchaseOrders(ctx context.Context, arg ListPurchaseOrdersParams) ([]PurchaseOrder, error)
|
||||
ListStates(ctx context.Context, arg ListStatesParams) ([]State, error)
|
||||
ListStatuses(ctx context.Context, arg ListStatusesParams) ([]Status, error)
|
||||
MarkEnquirySubmitted(ctx context.Context, arg MarkEnquirySubmittedParams) error
|
||||
SearchCountriesByName(ctx context.Context, concat interface{}) ([]Country, error)
|
||||
SearchCustomersByName(ctx context.Context, arg SearchCustomersByNameParams) ([]Customer, error)
|
||||
SearchDocuments(ctx context.Context, arg SearchDocumentsParams) ([]SearchDocumentsRow, error)
|
||||
SearchEnquiries(ctx context.Context, arg SearchEnquiriesParams) ([]SearchEnquiriesRow, error)
|
||||
SearchProductsByTitle(ctx context.Context, arg SearchProductsByTitleParams) ([]Product, error)
|
||||
SearchPurchaseOrdersByTitle(ctx context.Context, arg SearchPurchaseOrdersByTitleParams) ([]PurchaseOrder, error)
|
||||
// Note: Unarchiving not supported as documents table doesn't have an archived column
|
||||
// This is a no-op for compatibility
|
||||
UnarchiveDocument(ctx context.Context, arg UnarchiveDocumentParams) error
|
||||
UnarchiveEnquiry(ctx context.Context, id int32) error
|
||||
UnarchiveUser(ctx context.Context, id int32) error
|
||||
UpdateAddress(ctx context.Context, arg UpdateAddressParams) error
|
||||
UpdateAttachment(ctx context.Context, arg UpdateAttachmentParams) error
|
||||
UpdateBox(ctx context.Context, arg UpdateBoxParams) error
|
||||
UpdateCountry(ctx context.Context, arg UpdateCountryParams) error
|
||||
UpdateCustomer(ctx context.Context, arg UpdateCustomerParams) error
|
||||
UpdateDocument(ctx context.Context, arg UpdateDocumentParams) error
|
||||
UpdateDocumentPDFInfo(ctx context.Context, arg UpdateDocumentPDFInfoParams) error
|
||||
UpdateEnquiry(ctx context.Context, arg UpdateEnquiryParams) error
|
||||
UpdateEnquiryStatus(ctx context.Context, arg UpdateEnquiryStatusParams) error
|
||||
UpdateLineItem(ctx context.Context, arg UpdateLineItemParams) error
|
||||
UpdateLineItemPrices(ctx context.Context, arg UpdateLineItemPricesParams) error
|
||||
UpdatePrinciple(ctx context.Context, arg UpdatePrincipleParams) error
|
||||
UpdateProduct(ctx context.Context, arg UpdateProductParams) error
|
||||
UpdatePurchaseOrder(ctx context.Context, arg UpdatePurchaseOrderParams) error
|
||||
UpdateState(ctx context.Context, arg UpdateStateParams) error
|
||||
UpdateStatus(ctx context.Context, arg UpdateStatusParams) error
|
||||
UpdateUser(ctx context.Context, arg UpdateUserParams) error
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
|
|
|||
120
go-app/internal/cmc/db/states.sql.go
Normal file
120
go-app/internal/cmc/db/states.sql.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: states.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createState = `-- name: CreateState :execresult
|
||||
INSERT INTO states (
|
||||
name, shortform, enqform
|
||||
) VALUES (
|
||||
?, ?, ?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateStateParams struct {
|
||||
Name string `json:"name"`
|
||||
Shortform sql.NullString `json:"shortform"`
|
||||
Enqform sql.NullString `json:"enqform"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateState(ctx context.Context, arg CreateStateParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createState, arg.Name, arg.Shortform, arg.Enqform)
|
||||
}
|
||||
|
||||
const deleteState = `-- name: DeleteState :exec
|
||||
DELETE FROM states
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteState(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteState, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getState = `-- name: GetState :one
|
||||
SELECT id, name, shortform, enqform FROM states
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetState(ctx context.Context, id int32) (State, error) {
|
||||
row := q.db.QueryRowContext(ctx, getState, id)
|
||||
var i State
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Shortform,
|
||||
&i.Enqform,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listStates = `-- name: ListStates :many
|
||||
SELECT id, name, shortform, enqform FROM states
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListStatesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListStates(ctx context.Context, arg ListStatesParams) ([]State, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listStates, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []State{}
|
||||
for rows.Next() {
|
||||
var i State
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Shortform,
|
||||
&i.Enqform,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateState = `-- name: UpdateState :exec
|
||||
UPDATE states
|
||||
SET name = ?,
|
||||
shortform = ?,
|
||||
enqform = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateStateParams struct {
|
||||
Name string `json:"name"`
|
||||
Shortform sql.NullString `json:"shortform"`
|
||||
Enqform sql.NullString `json:"enqform"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateState(ctx context.Context, arg UpdateStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateState,
|
||||
arg.Name,
|
||||
arg.Shortform,
|
||||
arg.Enqform,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
94
go-app/internal/cmc/db/statuses.sql.go
Normal file
94
go-app/internal/cmc/db/statuses.sql.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: statuses.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createStatus = `-- name: CreateStatus :execresult
|
||||
INSERT INTO statuses (name, color) VALUES (?, ?)
|
||||
`
|
||||
|
||||
type CreateStatusParams struct {
|
||||
Name string `json:"name"`
|
||||
Color sql.NullString `json:"color"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateStatus(ctx context.Context, arg CreateStatusParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createStatus, arg.Name, arg.Color)
|
||||
}
|
||||
|
||||
const deleteStatus = `-- name: DeleteStatus :exec
|
||||
DELETE FROM statuses WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteStatus(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteStatus, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getStatus = `-- name: GetStatus :one
|
||||
SELECT id, name, color FROM statuses
|
||||
WHERE id = ? LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetStatus(ctx context.Context, id int32) (Status, error) {
|
||||
row := q.db.QueryRowContext(ctx, getStatus, id)
|
||||
var i Status
|
||||
err := row.Scan(&i.ID, &i.Name, &i.Color)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listStatuses = `-- name: ListStatuses :many
|
||||
SELECT id, name, color FROM statuses
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
|
||||
type ListStatusesParams struct {
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListStatuses(ctx context.Context, arg ListStatusesParams) ([]Status, error) {
|
||||
rows, err := q.db.QueryContext(ctx, listStatuses, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Status{}
|
||||
for rows.Next() {
|
||||
var i Status
|
||||
if err := rows.Scan(&i.ID, &i.Name, &i.Color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateStatus = `-- name: UpdateStatus :exec
|
||||
UPDATE statuses SET name = ?, color = ? WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateStatusParams struct {
|
||||
Name string `json:"name"`
|
||||
Color sql.NullString `json:"color"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateStatus, arg.Name, arg.Color, arg.ID)
|
||||
return err
|
||||
}
|
||||
211
go-app/internal/cmc/db/users.sql.go
Normal file
211
go-app/internal/cmc/db/users.sql.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: users.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const archiveUser = `-- name: ArchiveUser :exec
|
||||
UPDATE users
|
||||
SET archived = 1
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) ArchiveUser(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, archiveUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const createUser = `-- name: CreateUser :execresult
|
||||
INSERT INTO users (
|
||||
username, first_name, last_name, email, type, enabled, archived
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, 0)
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Type UsersType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (sql.Result, error) {
|
||||
return q.db.ExecContext(ctx, createUser,
|
||||
arg.Username,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.Type,
|
||||
arg.Enabled,
|
||||
)
|
||||
}
|
||||
|
||||
const getAllUsers = `-- name: GetAllUsers :many
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE archived = 0
|
||||
ORDER BY first_name, last_name
|
||||
`
|
||||
|
||||
type GetAllUsersRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Type UsersType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Archived sql.NullBool `json:"archived"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetAllUsers(ctx context.Context) ([]GetAllUsersRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetAllUsersRow{}
|
||||
for rows.Next() {
|
||||
var i GetAllUsersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Type,
|
||||
&i.Enabled,
|
||||
&i.Archived,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getUser = `-- name: GetUser :one
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type GetUserRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Type UsersType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Archived sql.NullBool `json:"archived"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUser(ctx context.Context, id int32) (GetUserRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUser, id)
|
||||
var i GetUserRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Type,
|
||||
&i.Enabled,
|
||||
&i.Archived,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE username = ? AND archived = 0
|
||||
`
|
||||
|
||||
type GetUserByUsernameRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Type UsersType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Archived sql.NullBool `json:"archived"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getUserByUsername, username)
|
||||
var i GetUserByUsernameRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Type,
|
||||
&i.Enabled,
|
||||
&i.Archived,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const unarchiveUser = `-- name: UnarchiveUser :exec
|
||||
UPDATE users
|
||||
SET archived = 0
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) UnarchiveUser(ctx context.Context, id int32) error {
|
||||
_, err := q.db.ExecContext(ctx, unarchiveUser, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
username = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
email = ?,
|
||||
type = ?,
|
||||
enabled = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateUserParams struct {
|
||||
Username string `json:"username"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email"`
|
||||
Type UsersType `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ID int32 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateUser,
|
||||
arg.Username,
|
||||
arg.FirstName,
|
||||
arg.LastName,
|
||||
arg.Email,
|
||||
arg.Type,
|
||||
arg.Enabled,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
305
go-app/internal/cmc/handlers/addresses.go
Normal file
305
go-app/internal/cmc/handlers/addresses.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type AddressHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewAddressHandler(queries *db.Queries) *AddressHandler {
|
||||
return &AddressHandler{queries: queries}
|
||||
}
|
||||
|
||||
func (h *AddressHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
addresses, err := h.queries.ListAddresses(r.Context(), db.ListAddressesParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(addresses)
|
||||
}
|
||||
|
||||
func (h *AddressHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid address ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
address, err := h.queries.GetAddress(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Address not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(address)
|
||||
}
|
||||
|
||||
func (h *AddressHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
// Parse form data for HTMX requests
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get customer ID from URL or form
|
||||
var customerID int32
|
||||
if cid := vars["customerid"]; cid != "" {
|
||||
if id, err := strconv.Atoi(cid); err == nil {
|
||||
customerID = int32(id)
|
||||
}
|
||||
} else if cid := r.FormValue("customer_id"); cid != "" {
|
||||
if id, err := strconv.Atoi(cid); err == nil {
|
||||
customerID = int32(id)
|
||||
}
|
||||
}
|
||||
|
||||
params := db.CreateAddressParams{
|
||||
Name: r.FormValue("name"),
|
||||
Address: r.FormValue("address"),
|
||||
City: r.FormValue("city"),
|
||||
StateID: 1, // Default state
|
||||
CountryID: 1, // Default country
|
||||
CustomerID: customerID,
|
||||
Type: r.FormValue("type"),
|
||||
Postcode: r.FormValue("postcode"),
|
||||
}
|
||||
|
||||
// Parse state_id and country_id if provided
|
||||
if sid := r.FormValue("state_id"); sid != "" {
|
||||
if id, err := strconv.Atoi(sid); err == nil {
|
||||
params.StateID = int32(id)
|
||||
}
|
||||
}
|
||||
if cid := r.FormValue("country_id"); cid != "" {
|
||||
if id, err := strconv.Atoi(cid); err == nil {
|
||||
params.CountryID = int32(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a JSON request
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateAddress(r.Context(), params)
|
||||
if err != nil {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-danger">Error creating address</div>`))
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTMX request, return the new address row
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
// Return a simple HTML response for now
|
||||
w.Write([]byte(`<div class="notification is-success">Address created successfully</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AddressHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid address ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params db.UpdateAddressParams
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
params = db.UpdateAddressParams{
|
||||
Name: r.FormValue("name"),
|
||||
Address: r.FormValue("address"),
|
||||
City: r.FormValue("city"),
|
||||
Type: r.FormValue("type"),
|
||||
Postcode: r.FormValue("postcode"),
|
||||
}
|
||||
|
||||
// Parse IDs
|
||||
if sid := r.FormValue("state_id"); sid != "" {
|
||||
if stateID, err := strconv.Atoi(sid); err == nil {
|
||||
params.StateID = int32(stateID)
|
||||
}
|
||||
}
|
||||
if cid := r.FormValue("country_id"); cid != "" {
|
||||
if countryID, err := strconv.Atoi(cid); err == nil {
|
||||
params.CountryID = int32(countryID)
|
||||
}
|
||||
}
|
||||
if cid := r.FormValue("customer_id"); cid != "" {
|
||||
if customerID, err := strconv.Atoi(cid); err == nil {
|
||||
params.CustomerID = int32(customerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params.ID = int32(id)
|
||||
|
||||
if err := h.queries.UpdateAddress(r.Context(), params); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *AddressHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid address ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.queries.DeleteAddress(r.Context(), int32(id)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *AddressHandler) CustomerAddresses(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
customerID, err := strconv.Atoi(vars["customerID"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid customer ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
addresses, err := h.queries.GetCustomerAddresses(r.Context(), int32(customerID))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If AJAX request, return HTML
|
||||
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
// For now, return a simple HTML list
|
||||
w.Write([]byte("<select name='address_id' class='form-control'>"))
|
||||
for _, addr := range addresses {
|
||||
w.Write([]byte(fmt.Sprintf("<option value='%d'>%s - %s</option>", addr.ID, addr.Name, addr.Address)))
|
||||
}
|
||||
w.Write([]byte("</select>"))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(addresses)
|
||||
}
|
||||
|
||||
func (h *AddressHandler) AddAnother(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
increment := vars["increment"]
|
||||
|
||||
// Return an HTML form for adding another address
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
html := fmt.Sprintf(`
|
||||
<div id="address-%s" class="address-form">
|
||||
<h4>Address %s</h4>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" name="addresses[%s][name]" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Address</label>
|
||||
<textarea name="addresses[%s][address]" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>City</label>
|
||||
<input type="text" name="addresses[%s][city]" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Postcode</label>
|
||||
<input type="text" name="addresses[%s][postcode]" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<select name="addresses[%s][type]" class="form-control">
|
||||
<option value="both">Both</option>
|
||||
<option value="bill">Billing</option>
|
||||
<option value="ship">Shipping</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" onclick="removeAddress('%s')" class="btn btn-danger">Remove</button>
|
||||
</div>
|
||||
`, increment, increment, increment, increment, increment, increment, increment, increment)
|
||||
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func (h *AddressHandler) RemoveAnother(w http.ResponseWriter, r *http.Request) {
|
||||
// This typically returns empty content or a success message
|
||||
// The actual removal is handled by JavaScript on the frontend
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
251
go-app/internal/cmc/handlers/attachments.go
Normal file
251
go-app/internal/cmc/handlers/attachments.go
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type AttachmentHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewAttachmentHandler(queries *db.Queries) *AttachmentHandler {
|
||||
return &AttachmentHandler{queries: queries}
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
attachments, err := h.queries.ListAttachments(r.Context(), db.ListAttachmentsParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(attachments)
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) Archived(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
attachments, err := h.queries.ListArchivedAttachments(r.Context(), db.ListArchivedAttachmentsParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(attachments)
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid attachment ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
attachment, err := h.queries.GetAttachment(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Attachment not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(attachment)
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(32 << 20) // 32MB max
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get file from form
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "No file uploaded", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Generate unique filename
|
||||
ext := filepath.Ext(handler.Filename)
|
||||
filename := fmt.Sprintf("%d_%s%s", time.Now().Unix(), handler.Filename[:len(handler.Filename)-len(ext)], ext)
|
||||
|
||||
// Create attachments directory if it doesn't exist
|
||||
attachDir := "webroot/attachments_files"
|
||||
if err := os.MkdirAll(attachDir, 0755); err != nil {
|
||||
http.Error(w, "Failed to create attachments directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Save file to disk
|
||||
filepath := filepath.Join(attachDir, filename)
|
||||
dst, err := os.Create(filepath)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
http.Error(w, "Failed to save file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse principle_id
|
||||
principleID := 1 // Default
|
||||
if pid := r.FormValue("principle_id"); pid != "" {
|
||||
if id, err := strconv.Atoi(pid); err == nil {
|
||||
principleID = id
|
||||
}
|
||||
}
|
||||
|
||||
// Create database record
|
||||
params := db.CreateAttachmentParams{
|
||||
PrincipleID: int32(principleID),
|
||||
Name: r.FormValue("name"),
|
||||
Filename: handler.Filename,
|
||||
File: filename,
|
||||
Type: handler.Header.Get("Content-Type"),
|
||||
Size: int32(handler.Size),
|
||||
Description: r.FormValue("description"),
|
||||
}
|
||||
|
||||
if params.Name == "" {
|
||||
params.Name = handler.Filename
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateAttachment(r.Context(), params)
|
||||
if err != nil {
|
||||
// Clean up file on error
|
||||
os.Remove(filepath)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTMX request, return success message
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-success">Attachment uploaded successfully</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid attachment ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params db.UpdateAttachmentParams
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
params = db.UpdateAttachmentParams{
|
||||
Name: r.FormValue("name"),
|
||||
Description: r.FormValue("description"),
|
||||
}
|
||||
}
|
||||
|
||||
params.ID = int32(id)
|
||||
|
||||
if err := h.queries.UpdateAttachment(r.Context(), params); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *AttachmentHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid attachment ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Soft delete (archive)
|
||||
if err := h.queries.DeleteAttachment(r.Context(), int32(id)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
209
go-app/internal/cmc/handlers/countries.go
Normal file
209
go-app/internal/cmc/handlers/countries.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type CountryHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewCountryHandler(queries *db.Queries) *CountryHandler {
|
||||
return &CountryHandler{queries: queries}
|
||||
}
|
||||
|
||||
func (h *CountryHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
countries, err := h.queries.ListCountries(r.Context(), db.ListCountriesParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(countries)
|
||||
}
|
||||
|
||||
func (h *CountryHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid country ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
country, err := h.queries.GetCountry(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Country not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(country)
|
||||
}
|
||||
|
||||
func (h *CountryHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data for HTMX requests
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := r.FormValue("name")
|
||||
|
||||
// Check if this is a JSON request
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name = params.Name
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateCountry(r.Context(), name)
|
||||
if err != nil {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-danger">Error creating country</div>`))
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTMX request, return success message
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-success">Country created successfully</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CountryHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid country ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var name string
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name = params.Name
|
||||
} else {
|
||||
// Handle form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name = r.FormValue("name")
|
||||
}
|
||||
|
||||
if err := h.queries.UpdateCountry(r.Context(), db.UpdateCountryParams{
|
||||
Name: name,
|
||||
ID: int32(id),
|
||||
}); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *CountryHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid country ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.queries.DeleteCountry(r.Context(), int32(id)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *CountryHandler) CompleteCountry(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("term")
|
||||
if query == "" {
|
||||
query = r.URL.Query().Get("q")
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]interface{}{})
|
||||
return
|
||||
}
|
||||
|
||||
countries, err := h.queries.SearchCountriesByName(r.Context(), query)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Format for autocomplete
|
||||
var results []map[string]interface{}
|
||||
for _, country := range countries {
|
||||
results = append(results, map[string]interface{}{
|
||||
"id": country.ID,
|
||||
"label": country.Name,
|
||||
"value": country.Name,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
518
go-app/internal/cmc/handlers/document.go
Normal file
518
go-app/internal/cmc/handlers/document.go
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/pdf"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type DocumentHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewDocumentHandler(queries *db.Queries) *DocumentHandler {
|
||||
return &DocumentHandler{queries: queries}
|
||||
}
|
||||
|
||||
// List handles GET /api/documents
|
||||
func (h *DocumentHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check for type filter
|
||||
docType := r.URL.Query().Get("type")
|
||||
|
||||
var documents interface{}
|
||||
var err error
|
||||
|
||||
if docType != "" {
|
||||
// Convert string to DocumentsType enum
|
||||
documents, err = h.queries.ListDocumentsByType(ctx, db.DocumentsType(docType))
|
||||
} else {
|
||||
documents, err = h.queries.ListDocuments(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error fetching documents: %v", err)
|
||||
http.Error(w, "Failed to fetch documents", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(documents)
|
||||
}
|
||||
|
||||
// Helper functions to convert between pointer and null types
|
||||
func nullInt32FromPtr(p *int32) sql.NullInt32 {
|
||||
if p == nil {
|
||||
return sql.NullInt32{Valid: false}
|
||||
}
|
||||
return sql.NullInt32{Int32: *p, Valid: true}
|
||||
}
|
||||
|
||||
func nullStringFromPtr(p *string) sql.NullString {
|
||||
if p == nil {
|
||||
return sql.NullString{Valid: false}
|
||||
}
|
||||
return sql.NullString{String: *p, Valid: true}
|
||||
}
|
||||
|
||||
func nullTimeFromPtr(p *time.Time) sql.NullTime {
|
||||
if p == nil {
|
||||
return sql.NullTime{Valid: false}
|
||||
}
|
||||
return sql.NullTime{Time: *p, Valid: true}
|
||||
}
|
||||
|
||||
// Get handles GET /api/documents/{id}
|
||||
func (h *DocumentHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
document, err := h.queries.GetDocumentByID(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching document %d: %v", id, err)
|
||||
http.Error(w, "Document not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(document)
|
||||
}
|
||||
|
||||
// Create handles POST /api/documents
|
||||
func (h *DocumentHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
CMCReference string `json:"cmc_reference"`
|
||||
PDFFilename string `json:"pdf_filename"`
|
||||
PDFCreatedAt string `json:"pdf_created_at"`
|
||||
PDFCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ShippingDetails *string `json:"shipping_details,omitempty"`
|
||||
Revision *int32 `json:"revision,omitempty"`
|
||||
BillTo *string `json:"bill_to,omitempty"`
|
||||
ShipTo *string `json:"ship_to,omitempty"`
|
||||
EmailSentAt string `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if req.Type == "" {
|
||||
http.Error(w, "Type is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.UserID == 0 {
|
||||
http.Error(w, "User ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CMCReference == "" {
|
||||
http.Error(w, "CMC Reference is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse timestamps
|
||||
pdfCreatedAt, err := time.Parse("2006-01-02 15:04:05", req.PDFCreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid pdf_created_at format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
emailSentAt, err := time.Parse("2006-01-02 15:04:05", req.EmailSentAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid email_sent_at format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
revision := int32(0)
|
||||
if req.Revision != nil {
|
||||
revision = *req.Revision
|
||||
}
|
||||
|
||||
params := db.CreateDocumentParams{
|
||||
Type: db.DocumentsType(req.Type),
|
||||
UserID: req.UserID,
|
||||
DocPageCount: req.DocPageCount,
|
||||
CmcReference: req.CMCReference,
|
||||
PdfFilename: req.PDFFilename,
|
||||
PdfCreatedAt: pdfCreatedAt,
|
||||
PdfCreatedByUserID: req.PDFCreatedByUserID,
|
||||
ShippingDetails: nullStringFromPtr(req.ShippingDetails),
|
||||
Revision: revision,
|
||||
BillTo: nullStringFromPtr(req.BillTo),
|
||||
ShipTo: nullStringFromPtr(req.ShipTo),
|
||||
EmailSentAt: emailSentAt,
|
||||
EmailSentByUserID: req.EmailSentByUserID,
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateDocument(ctx, params)
|
||||
if err != nil {
|
||||
log.Printf("Error creating document: %v", err)
|
||||
http.Error(w, "Failed to create document", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Document created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/documents/{id}
|
||||
func (h *DocumentHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
UserID int32 `json:"user_id"`
|
||||
DocPageCount int32 `json:"doc_page_count"`
|
||||
CMCReference string `json:"cmc_reference"`
|
||||
PDFFilename string `json:"pdf_filename"`
|
||||
PDFCreatedAt string `json:"pdf_created_at"`
|
||||
PDFCreatedByUserID int32 `json:"pdf_created_by_user_id"`
|
||||
ShippingDetails *string `json:"shipping_details,omitempty"`
|
||||
Revision *int32 `json:"revision,omitempty"`
|
||||
BillTo *string `json:"bill_to,omitempty"`
|
||||
ShipTo *string `json:"ship_to,omitempty"`
|
||||
EmailSentAt string `json:"email_sent_at"`
|
||||
EmailSentByUserID int32 `json:"email_sent_by_user_id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Parse timestamps
|
||||
pdfCreatedAt, err := time.Parse("2006-01-02 15:04:05", req.PDFCreatedAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid pdf_created_at format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
emailSentAt, err := time.Parse("2006-01-02 15:04:05", req.EmailSentAt)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid email_sent_at format", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
revision := int32(0)
|
||||
if req.Revision != nil {
|
||||
revision = *req.Revision
|
||||
}
|
||||
|
||||
params := db.UpdateDocumentParams{
|
||||
Type: db.DocumentsType(req.Type),
|
||||
UserID: req.UserID,
|
||||
DocPageCount: req.DocPageCount,
|
||||
CmcReference: req.CMCReference,
|
||||
PdfFilename: req.PDFFilename,
|
||||
PdfCreatedAt: pdfCreatedAt,
|
||||
PdfCreatedByUserID: req.PDFCreatedByUserID,
|
||||
ShippingDetails: nullStringFromPtr(req.ShippingDetails),
|
||||
Revision: revision,
|
||||
BillTo: nullStringFromPtr(req.BillTo),
|
||||
ShipTo: nullStringFromPtr(req.ShipTo),
|
||||
EmailSentAt: emailSentAt,
|
||||
EmailSentByUserID: req.EmailSentByUserID,
|
||||
ID: int32(id),
|
||||
}
|
||||
|
||||
err = h.queries.UpdateDocument(ctx, params)
|
||||
if err != nil {
|
||||
log.Printf("Error updating document %d: %v", id, err)
|
||||
http.Error(w, "Failed to update document", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Document updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Archive handles PUT /api/documents/{id}/archive
|
||||
func (h *DocumentHandler) Archive(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = h.queries.ArchiveDocument(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error archiving document %d: %v", id, err)
|
||||
http.Error(w, "Failed to archive document", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Document archived successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Unarchive handles PUT /api/documents/{id}/unarchive
|
||||
func (h *DocumentHandler) Unarchive(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
err = h.queries.UnarchiveDocument(ctx, db.UnarchiveDocumentParams{
|
||||
Column1: int32(id),
|
||||
Column2: int32(id),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error unarchiving document %d: %v", id, err)
|
||||
http.Error(w, "Failed to unarchive document", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Document unarchived successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Search handles GET /api/documents/search?q=query
|
||||
func (h *DocumentHandler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
http.Error(w, "Search query is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
searchPattern := "%" + query + "%"
|
||||
documents, err := h.queries.SearchDocuments(ctx, db.SearchDocumentsParams{
|
||||
PdfFilename: searchPattern,
|
||||
CmcReference: searchPattern,
|
||||
Name: searchPattern,
|
||||
Title: searchPattern,
|
||||
Username: searchPattern,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error searching documents: %v", err)
|
||||
http.Error(w, "Failed to search documents", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(documents)
|
||||
}
|
||||
|
||||
// GeneratePDF handles GET /documents/pdf/{id}
|
||||
func (h *DocumentHandler) GeneratePDF(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Get document
|
||||
document, err := h.queries.GetDocumentByID(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching document %d: %v", id, err)
|
||||
http.Error(w, "Document not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Get output directory from environment or use default
|
||||
outputDir := os.Getenv("PDF_OUTPUT_DIR")
|
||||
if outputDir == "" {
|
||||
outputDir = "webroot/pdf"
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
log.Printf("Error creating PDF directory: %v", err)
|
||||
http.Error(w, "Failed to create PDF directory", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var filename string
|
||||
|
||||
// Generate PDF based on document type
|
||||
log.Printf("Generating PDF for document %d, type: %s, outputDir: %s", id, document.Type, outputDir)
|
||||
switch document.Type {
|
||||
case db.DocumentsTypeQuote:
|
||||
// Get quote-specific data
|
||||
// TODO: Get quote data from database
|
||||
|
||||
// Get enquiry data
|
||||
// TODO: Get enquiry from quote
|
||||
|
||||
// Get customer data
|
||||
// TODO: Get customer from enquiry
|
||||
|
||||
// Get user data
|
||||
user, err := h.queries.GetUser(ctx, document.UserID)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching user: %v", err)
|
||||
http.Error(w, "Failed to fetch user data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get line items
|
||||
lineItems, err := h.queries.GetLineItemsTable(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching line items: %v", err)
|
||||
http.Error(w, "Failed to fetch line items", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// For now, create a simplified quote PDF
|
||||
data := &pdf.QuotePDFData{
|
||||
Document: &document,
|
||||
User: &user,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: "$", // Default to AUD
|
||||
ShowGST: true, // Default to showing GST
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
log.Printf("Calling GenerateQuotePDF with outputDir: %s", outputDir)
|
||||
filename, err = pdf.GenerateQuotePDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("Error generating quote PDF: %v", err)
|
||||
http.Error(w, "Failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("Successfully generated PDF: %s", filename)
|
||||
|
||||
case db.DocumentsTypeInvoice:
|
||||
// Get invoice-specific data
|
||||
// TODO: Implement invoice PDF generation
|
||||
http.Error(w, "Invoice PDF generation not yet implemented", http.StatusNotImplemented)
|
||||
return
|
||||
|
||||
case db.DocumentsTypePurchaseOrder:
|
||||
// Get purchase order data
|
||||
po, err := h.queries.GetPurchaseOrderByDocumentID(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching purchase order: %v", err)
|
||||
http.Error(w, "Failed to fetch purchase order data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get principle data
|
||||
principle, err := h.queries.GetPrinciple(ctx, po.PrincipleID)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching principle: %v", err)
|
||||
http.Error(w, "Failed to fetch principle data", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get line items
|
||||
lineItems, err := h.queries.GetLineItemsTable(ctx, int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching line items: %v", err)
|
||||
http.Error(w, "Failed to fetch line items", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create purchase order PDF data
|
||||
data := &pdf.PurchaseOrderPDFData{
|
||||
Document: &document,
|
||||
PurchaseOrder: &po,
|
||||
Principle: &principle,
|
||||
LineItems: lineItems,
|
||||
CurrencySymbol: "$", // Default to AUD
|
||||
ShowGST: true, // Default to showing GST for Australian principles
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
filename, err = pdf.GeneratePurchaseOrderPDF(data, outputDir)
|
||||
if err != nil {
|
||||
log.Printf("Error generating purchase order PDF: %v", err)
|
||||
http.Error(w, "Failed to generate PDF", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(w, "Unsupported document type for PDF generation", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Update document with PDF filename and timestamp
|
||||
now := time.Now()
|
||||
err = h.queries.UpdateDocumentPDFInfo(ctx, db.UpdateDocumentPDFInfoParams{
|
||||
PdfFilename: filename,
|
||||
PdfCreatedAt: now,
|
||||
PdfCreatedByUserID: 1, // TODO: Get current user ID
|
||||
ID: int32(id),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Error updating document PDF info: %v", err)
|
||||
// Don't fail the request, PDF was generated successfully
|
||||
}
|
||||
|
||||
// Return success response with redirect to document view
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
window.location.replace("/documents/view/%d");
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>PDF generated successfully. <a href="/documents/view/%d">Click here</a> if you are not redirected.</p>
|
||||
</body>
|
||||
</html>
|
||||
`, id, id)
|
||||
}
|
||||
544
go-app/internal/cmc/handlers/line_items.go
Normal file
544
go-app/internal/cmc/handlers/line_items.go
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type LineItemHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewLineItemHandler(queries *db.Queries) *LineItemHandler {
|
||||
return &LineItemHandler{queries: queries}
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
lineItems, err := h.queries.ListLineItems(r.Context(), db.ListLineItemsParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(lineItems)
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid line item ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
lineItem, err := h.queries.GetLineItem(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Line item not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Add CORS headers for API access
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(lineItem)
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) AjaxAdd(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("NO-DATA"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse required fields
|
||||
documentID, err := strconv.Atoi(r.FormValue("document_id"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get next item number
|
||||
maxItemInterface, err := h.queries.GetMaxItemNumber(r.Context(), int32(documentID))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
maxItemNumber := float64(0)
|
||||
if maxItemInterface != nil {
|
||||
switch v := maxItemInterface.(type) {
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
maxItemNumber = parsed
|
||||
}
|
||||
case float64:
|
||||
maxItemNumber = v
|
||||
case int64:
|
||||
maxItemNumber = float64(v)
|
||||
}
|
||||
}
|
||||
nextItemNumber := fmt.Sprintf("%.2f", maxItemNumber+1.0)
|
||||
|
||||
// Parse optional fields
|
||||
var productID sql.NullInt32
|
||||
if pid := r.FormValue("product_id"); pid != "" {
|
||||
if id, err := strconv.Atoi(pid); err == nil {
|
||||
productID = sql.NullInt32{Int32: int32(id), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
var costingID sql.NullInt32
|
||||
if cid := r.FormValue("costing_id"); cid != "" {
|
||||
if id, err := strconv.Atoi(cid); err == nil {
|
||||
costingID = sql.NullInt32{Int32: int32(id), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse boolean fields
|
||||
option := r.FormValue("option") == "1" || r.FormValue("option") == "true"
|
||||
hasTextPrices := r.FormValue("has_text_prices") == "1" || r.FormValue("has_text_prices") == "true"
|
||||
|
||||
hasPrice := int8(1)
|
||||
if hp := r.FormValue("has_price"); hp == "0" || hp == "false" {
|
||||
hasPrice = 0
|
||||
}
|
||||
|
||||
params := db.CreateLineItemParams{
|
||||
ItemNumber: nextItemNumber,
|
||||
Option: option,
|
||||
Quantity: r.FormValue("quantity"),
|
||||
Title: r.FormValue("title"),
|
||||
Description: r.FormValue("description"),
|
||||
DocumentID: int32(documentID),
|
||||
ProductID: productID,
|
||||
HasTextPrices: hasTextPrices,
|
||||
HasPrice: hasPrice,
|
||||
UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""},
|
||||
GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""},
|
||||
CostingID: costingID,
|
||||
GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""},
|
||||
NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""},
|
||||
DiscountPercent: sql.NullString{String: r.FormValue("discount_percent"), Valid: r.FormValue("discount_percent") != ""},
|
||||
DiscountAmountUnit: sql.NullString{String: r.FormValue("discount_amount_unit"), Valid: r.FormValue("discount_amount_unit") != ""},
|
||||
DiscountAmountTotal: sql.NullString{String: r.FormValue("discount_amount_total"), Valid: r.FormValue("discount_amount_total") != ""},
|
||||
GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""},
|
||||
NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""},
|
||||
}
|
||||
|
||||
_, err = h.queries.CreateLineItem(r.Context(), params)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Update invoice totals like the original CakePHP code
|
||||
// h.updateInvoice(documentID)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("SUCCESS"))
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) AjaxEdit(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("NO-DATA"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse ID
|
||||
id, err := strconv.Atoi(r.FormValue("id"))
|
||||
if err != nil {
|
||||
log.Printf("Error updating line item ID: %d %s\n", id, err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
documentID, err := strconv.Atoi(r.FormValue("document_id"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse optional fields
|
||||
var productID sql.NullInt32
|
||||
if pid := r.FormValue("product_id"); pid != "" {
|
||||
if prodID, err := strconv.Atoi(pid); err == nil {
|
||||
productID = sql.NullInt32{Int32: int32(prodID), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
var costingID sql.NullInt32
|
||||
if cid := r.FormValue("costing_id"); cid != "" {
|
||||
if cID, err := strconv.Atoi(cid); err == nil {
|
||||
costingID = sql.NullInt32{Int32: int32(cID), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse boolean fields
|
||||
option := r.FormValue("option") == "1" || r.FormValue("option") == "true"
|
||||
hasTextPrices := r.FormValue("has_text_prices") == "1" || r.FormValue("has_text_prices") == "true"
|
||||
|
||||
hasPrice := int8(1)
|
||||
if hp := r.FormValue("has_price"); hp == "0" || hp == "false" {
|
||||
hasPrice = 0
|
||||
}
|
||||
|
||||
params := db.UpdateLineItemParams{
|
||||
ID: int32(id),
|
||||
ItemNumber: r.FormValue("item_number"),
|
||||
Option: option,
|
||||
Quantity: r.FormValue("quantity"),
|
||||
Title: r.FormValue("title"),
|
||||
Description: r.FormValue("description"),
|
||||
DocumentID: int32(documentID),
|
||||
ProductID: productID,
|
||||
HasTextPrices: hasTextPrices,
|
||||
HasPrice: hasPrice,
|
||||
UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""},
|
||||
GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""},
|
||||
CostingID: costingID,
|
||||
GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""},
|
||||
NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""},
|
||||
DiscountPercent: sql.NullString{String: r.FormValue("discount_percent"), Valid: r.FormValue("discount_percent") != ""},
|
||||
DiscountAmountUnit: sql.NullString{String: r.FormValue("discount_amount_unit"), Valid: r.FormValue("discount_amount_unit") != ""},
|
||||
DiscountAmountTotal: sql.NullString{String: r.FormValue("discount_amount_total"), Valid: r.FormValue("discount_amount_total") != ""},
|
||||
GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""},
|
||||
NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""},
|
||||
}
|
||||
|
||||
err = h.queries.UpdateLineItem(r.Context(), params)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Update invoice totals like the original CakePHP code
|
||||
// h.updateInvoice(documentID)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("SUCCESS"))
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) AjaxDelete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the line item to find document_id for invoice update
|
||||
_, err = h.queries.GetLineItem(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
err = h.queries.DeleteLineItem(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("FAILURE"))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Update invoice totals like the original CakePHP code
|
||||
// h.updateInvoice(lineItem.DocumentID)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("SUCCESS"))
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) GetTable(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
documentID, err := strconv.Atoi(vars["documentID"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
lineItems, err := h.queries.GetLineItemsTable(r.Context(), int32(documentID))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If AJAX request, return HTML table rows only (for refreshing)
|
||||
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
// Generate HTML table rows
|
||||
html := ""
|
||||
|
||||
if len(lineItems) == 0 {
|
||||
html = `<tr id="no-line-items"><td colspan="7" class="has-text-centered has-text-grey">No line items yet</td></tr>`
|
||||
} else {
|
||||
for _, item := range lineItems {
|
||||
unitPrice := "-"
|
||||
if item.GrossUnitPrice.Valid {
|
||||
unitPrice = "$" + item.GrossUnitPrice.String
|
||||
} else if item.UnitPriceString.Valid {
|
||||
unitPrice = item.UnitPriceString.String
|
||||
}
|
||||
|
||||
totalPrice := "-"
|
||||
if item.GrossPrice.Valid {
|
||||
totalPrice = "$" + item.GrossPrice.String
|
||||
} else if item.GrossPriceString.Valid {
|
||||
totalPrice = item.GrossPriceString.String
|
||||
}
|
||||
|
||||
html += fmt.Sprintf(`
|
||||
<tr id="line-item-%d">
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td>%s</td>
|
||||
<td>
|
||||
<button class="button is-small is-primary" onclick="editLineItem(%d)">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button class="button is-small is-danger" onclick="deleteLineItem(%d)">
|
||||
<span class="icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>`,
|
||||
item.ID, item.ItemNumber, item.Title, item.Description, item.Quantity,
|
||||
unitPrice, totalPrice, item.ID, item.ID)
|
||||
}
|
||||
}
|
||||
|
||||
w.Write([]byte(html))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(lineItems)
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
documentID, err := strconv.Atoi(vars["documentID"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse form data for HTMX requests
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get next item number
|
||||
maxItemInterface, err := h.queries.GetMaxItemNumber(r.Context(), int32(documentID))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
maxItemNumber := float64(0)
|
||||
if maxItemInterface != nil {
|
||||
switch v := maxItemInterface.(type) {
|
||||
case string:
|
||||
if parsed, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
maxItemNumber = parsed
|
||||
}
|
||||
case float64:
|
||||
maxItemNumber = v
|
||||
case int64:
|
||||
maxItemNumber = float64(v)
|
||||
}
|
||||
}
|
||||
nextItemNumber := fmt.Sprintf("%.2f", maxItemNumber+1.0)
|
||||
|
||||
// Parse optional fields
|
||||
var productID sql.NullInt32
|
||||
if pid := r.FormValue("product_id"); pid != "" {
|
||||
if id, err := strconv.Atoi(pid); err == nil {
|
||||
productID = sql.NullInt32{Int32: int32(id), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
params := db.CreateLineItemParams{
|
||||
ItemNumber: nextItemNumber,
|
||||
Option: r.FormValue("option") == "1",
|
||||
Quantity: r.FormValue("quantity"),
|
||||
Title: r.FormValue("title"),
|
||||
Description: r.FormValue("description"),
|
||||
DocumentID: int32(documentID),
|
||||
ProductID: productID,
|
||||
HasTextPrices: r.FormValue("has_text_prices") == "1",
|
||||
HasPrice: 1, // Default to has price
|
||||
UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""},
|
||||
GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""},
|
||||
GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""},
|
||||
NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""},
|
||||
GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""},
|
||||
NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""},
|
||||
}
|
||||
|
||||
// Check if this is a JSON request
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateLineItem(r.Context(), params)
|
||||
if err != nil {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-danger">Error creating line item</div>`))
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTMX request, return success message
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-success">Line item created successfully</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid line item ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params db.UpdateLineItemParams
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var productID sql.NullInt32
|
||||
if pid := r.FormValue("product_id"); pid != "" {
|
||||
if prodID, err := strconv.Atoi(pid); err == nil {
|
||||
productID = sql.NullInt32{Int32: int32(prodID), Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
documentID, _ := strconv.Atoi(r.FormValue("document_id"))
|
||||
|
||||
params = db.UpdateLineItemParams{
|
||||
ItemNumber: r.FormValue("item_number"),
|
||||
Option: r.FormValue("option") == "1",
|
||||
Quantity: r.FormValue("quantity"),
|
||||
Title: r.FormValue("title"),
|
||||
Description: r.FormValue("description"),
|
||||
DocumentID: int32(documentID),
|
||||
ProductID: productID,
|
||||
HasTextPrices: r.FormValue("has_text_prices") == "1",
|
||||
HasPrice: 1,
|
||||
UnitPriceString: sql.NullString{String: r.FormValue("unit_price_string"), Valid: r.FormValue("unit_price_string") != ""},
|
||||
GrossPriceString: sql.NullString{String: r.FormValue("gross_price_string"), Valid: r.FormValue("gross_price_string") != ""},
|
||||
GrossUnitPrice: sql.NullString{String: r.FormValue("gross_unit_price"), Valid: r.FormValue("gross_unit_price") != ""},
|
||||
NetUnitPrice: sql.NullString{String: r.FormValue("net_unit_price"), Valid: r.FormValue("net_unit_price") != ""},
|
||||
GrossPrice: sql.NullString{String: r.FormValue("gross_price"), Valid: r.FormValue("gross_price") != ""},
|
||||
NetPrice: sql.NullString{String: r.FormValue("net_price"), Valid: r.FormValue("net_price") != ""},
|
||||
}
|
||||
}
|
||||
|
||||
params.ID = int32(id)
|
||||
|
||||
if err := h.queries.UpdateLineItem(r.Context(), params); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *LineItemHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid line item ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.queries.DeleteLineItem(r.Context(), int32(id)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
|
@ -10,8 +11,8 @@ import (
|
|||
)
|
||||
|
||||
type PageHandler struct {
|
||||
queries *db.Queries
|
||||
tmpl *templates.TemplateManager
|
||||
queries *db.Queries
|
||||
tmpl *templates.TemplateManager
|
||||
}
|
||||
|
||||
func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager) *PageHandler {
|
||||
|
|
@ -26,7 +27,7 @@ func (h *PageHandler) Home(w http.ResponseWriter, r *http.Request) {
|
|||
data := map[string]interface{}{
|
||||
"Title": "Dashboard",
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -40,10 +41,10 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) {
|
|||
page = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
limit := 20
|
||||
offset := (page - 1) * limit
|
||||
|
||||
|
||||
customers, err := h.queries.ListCustomers(r.Context(), db.ListCustomersParams{
|
||||
Limit: int32(limit + 1), // Get one extra to check if there are more
|
||||
Offset: int32(offset),
|
||||
|
|
@ -52,12 +53,12 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
hasMore := len(customers) > limit
|
||||
if hasMore {
|
||||
customers = customers[:limit]
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Customers": customers,
|
||||
"Page": page,
|
||||
|
|
@ -65,7 +66,7 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) {
|
|||
"NextPage": page + 1,
|
||||
"HasMore": hasMore,
|
||||
}
|
||||
|
||||
|
||||
// Check if this is an HTMX request
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
if err := h.tmpl.Render(w, "customers/table.html", data); err != nil {
|
||||
|
|
@ -73,7 +74,7 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "customers/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -83,7 +84,7 @@ func (h *PageHandler) CustomersNew(w http.ResponseWriter, r *http.Request) {
|
|||
data := map[string]interface{}{
|
||||
"Customer": db.Customer{},
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "customers/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -96,17 +97,17 @@ func (h *PageHandler) CustomersEdit(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid customer ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
customer, err := h.queries.GetCustomer(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Customer not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Customer": customer,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "customers/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -119,17 +120,17 @@ func (h *PageHandler) CustomersShow(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid customer ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
customer, err := h.queries.GetCustomer(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Customer not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Customer": customer,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "customers/show.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -143,13 +144,13 @@ func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) {
|
|||
page = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
limit := 20
|
||||
offset := (page - 1) * limit
|
||||
|
||||
|
||||
var customers []db.Customer
|
||||
var err error
|
||||
|
||||
|
||||
if query == "" {
|
||||
customers, err = h.queries.ListCustomers(r.Context(), db.ListCustomersParams{
|
||||
Limit: int32(limit + 1),
|
||||
|
|
@ -163,17 +164,17 @@ func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) {
|
|||
Offset: int32(offset),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
hasMore := len(customers) > limit
|
||||
if hasMore {
|
||||
customers = customers[:limit]
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Customers": customers,
|
||||
"Page": page,
|
||||
|
|
@ -181,7 +182,7 @@ func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) {
|
|||
"NextPage": page + 1,
|
||||
"HasMore": hasMore,
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.tmpl.Render(w, "customers/table.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
@ -194,7 +195,7 @@ func (h *PageHandler) ProductsIndex(w http.ResponseWriter, r *http.Request) {
|
|||
data := map[string]interface{}{
|
||||
"Products": []db.Product{}, // Placeholder
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "products/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -204,7 +205,7 @@ func (h *PageHandler) ProductsNew(w http.ResponseWriter, r *http.Request) {
|
|||
data := map[string]interface{}{
|
||||
"Product": db.Product{},
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "products/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -217,17 +218,17 @@ func (h *PageHandler) ProductsShow(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid product ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
product, err := h.queries.GetProduct(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Product not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Product": product,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "products/show.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -240,17 +241,17 @@ func (h *PageHandler) ProductsEdit(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid product ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
product, err := h.queries.GetProduct(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Product not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Product": product,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "products/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -261,7 +262,7 @@ func (h *PageHandler) ProductsSearch(w http.ResponseWriter, r *http.Request) {
|
|||
data := map[string]interface{}{
|
||||
"Products": []db.Product{},
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.tmpl.Render(w, "products/table.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
@ -273,7 +274,7 @@ func (h *PageHandler) PurchaseOrdersIndex(w http.ResponseWriter, r *http.Request
|
|||
data := map[string]interface{}{
|
||||
"PurchaseOrders": []db.PurchaseOrder{},
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "purchase-orders/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -283,7 +284,7 @@ func (h *PageHandler) PurchaseOrdersNew(w http.ResponseWriter, r *http.Request)
|
|||
data := map[string]interface{}{
|
||||
"PurchaseOrder": db.PurchaseOrder{},
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -296,17 +297,17 @@ func (h *PageHandler) PurchaseOrdersShow(w http.ResponseWriter, r *http.Request)
|
|||
http.Error(w, "Invalid purchase order ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Purchase order not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"PurchaseOrder": purchaseOrder,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "purchase-orders/show.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -319,17 +320,17 @@ func (h *PageHandler) PurchaseOrdersEdit(w http.ResponseWriter, r *http.Request)
|
|||
http.Error(w, "Invalid purchase order ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Purchase order not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"PurchaseOrder": purchaseOrder,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -339,7 +340,7 @@ func (h *PageHandler) PurchaseOrdersSearch(w http.ResponseWriter, r *http.Reques
|
|||
data := map[string]interface{}{
|
||||
"PurchaseOrders": []db.PurchaseOrder{},
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.tmpl.Render(w, "purchase-orders/table.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
@ -354,14 +355,14 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) {
|
|||
page = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
limit := 150
|
||||
offset := (page - 1) * limit
|
||||
|
||||
|
||||
var enquiries interface{}
|
||||
var err error
|
||||
var hasMore bool
|
||||
|
||||
|
||||
// Check if we want archived enquiries
|
||||
if r.URL.Query().Get("archived") == "true" {
|
||||
archivedEnquiries, err := h.queries.ListArchivedEnquiries(r.Context(), db.ListArchivedEnquiriesParams{
|
||||
|
|
@ -388,19 +389,19 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) {
|
|||
enquiries = activeEnquiries
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get status list for dropdown and CSS classes
|
||||
statuses, err := h.queries.GetAllStatuses(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Enquiries": enquiries,
|
||||
"Statuses": statuses,
|
||||
|
|
@ -409,7 +410,7 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) {
|
|||
"NextPage": page + 1,
|
||||
"HasMore": hasMore,
|
||||
}
|
||||
|
||||
|
||||
// Check if this is an HTMX request
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil {
|
||||
|
|
@ -417,7 +418,7 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "enquiries/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -430,33 +431,33 @@ func (h *PageHandler) EnquiriesNew(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
principles, err := h.queries.GetAllPrinciples(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
states, err := h.queries.GetAllStates(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
countries, err := h.queries.GetAllCountries(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Enquiry": db.Enquiry{},
|
||||
"Statuses": statuses,
|
||||
"Enquiry": db.Enquiry{},
|
||||
"Statuses": statuses,
|
||||
"Principles": principles,
|
||||
"States": states,
|
||||
"Countries": countries,
|
||||
"States": states,
|
||||
"Countries": countries,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -469,17 +470,17 @@ func (h *PageHandler) EnquiriesShow(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid enquiry ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Enquiry not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Enquiry": enquiry,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "enquiries/show.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -492,46 +493,46 @@ func (h *PageHandler) EnquiriesEdit(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Invalid enquiry ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Enquiry not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Get required form data
|
||||
statuses, err := h.queries.GetAllStatuses(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
principles, err := h.queries.GetAllPrinciples(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
states, err := h.queries.GetAllStates(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
countries, err := h.queries.GetAllCountries(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Enquiry": enquiry,
|
||||
"Statuses": statuses,
|
||||
"Enquiry": enquiry,
|
||||
"Statuses": statuses,
|
||||
"Principles": principles,
|
||||
"States": states,
|
||||
"Countries": countries,
|
||||
"States": states,
|
||||
"Countries": countries,
|
||||
}
|
||||
|
||||
|
||||
if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
|
@ -545,13 +546,13 @@ func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) {
|
|||
page = val
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
limit := 150
|
||||
offset := (page - 1) * limit
|
||||
|
||||
|
||||
var enquiries interface{}
|
||||
var hasMore bool
|
||||
|
||||
|
||||
if query == "" {
|
||||
// If no search query, return regular list
|
||||
regularEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{
|
||||
|
|
@ -562,7 +563,7 @@ func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
hasMore = len(regularEnquiries) > limit
|
||||
if hasMore {
|
||||
regularEnquiries = regularEnquiries[:limit]
|
||||
|
|
@ -580,14 +581,14 @@ func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
hasMore = len(searchResults) > limit
|
||||
if hasMore {
|
||||
searchResults = searchResults[:limit]
|
||||
}
|
||||
enquiries = searchResults
|
||||
}
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Enquiries": enquiries,
|
||||
"Page": page,
|
||||
|
|
@ -595,9 +596,182 @@ func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) {
|
|||
"NextPage": page + 1,
|
||||
"HasMore": hasMore,
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Document page handlers
|
||||
func (h *PageHandler) DocumentsIndex(w http.ResponseWriter, r *http.Request) {
|
||||
page := 1
|
||||
if p := r.URL.Query().Get("page"); p != "" {
|
||||
if val, err := strconv.Atoi(p); err == nil && val > 0 {
|
||||
page = val
|
||||
}
|
||||
}
|
||||
|
||||
// Get document type filter
|
||||
docType := r.URL.Query().Get("type")
|
||||
|
||||
var documents interface{}
|
||||
var err error
|
||||
|
||||
if docType != "" {
|
||||
documents, err = h.queries.ListDocumentsByType(r.Context(), db.DocumentsType(docType))
|
||||
} else {
|
||||
documents, err = h.queries.ListDocuments(r.Context())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get users list for display names (if needed)
|
||||
users, err := h.queries.GetAllUsers(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Documents": documents,
|
||||
"Users": users,
|
||||
"Page": page,
|
||||
"DocType": docType,
|
||||
}
|
||||
|
||||
// Check if this is an HTMX request
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tmpl.Render(w, "documents/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PageHandler) DocumentsShow(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
document, err := h.queries.GetDocumentByID(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error fetching document %d: %v", id, err)
|
||||
http.Error(w, "Document not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Document": document,
|
||||
}
|
||||
|
||||
if err := h.tmpl.Render(w, "documents/show.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PageHandler) DocumentsSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("search")
|
||||
|
||||
var documents interface{}
|
||||
var err error
|
||||
|
||||
if query == "" {
|
||||
// If no search query, return regular list
|
||||
documents, err = h.queries.ListDocuments(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
searchPattern := "%" + query + "%"
|
||||
documents, err = h.queries.SearchDocuments(r.Context(), db.SearchDocumentsParams{
|
||||
PdfFilename: searchPattern,
|
||||
CmcReference: searchPattern,
|
||||
Name: searchPattern,
|
||||
Title: searchPattern,
|
||||
Username: searchPattern,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Documents": documents,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
document, err := h.queries.GetDocumentByID(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Document not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Load line items for this document
|
||||
lineItems, err := h.queries.ListLineItemsByDocument(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
log.Printf("Error loading line items for document %d: %v", id, err)
|
||||
// Don't fail the entire page if line items can't be loaded
|
||||
lineItems = []db.LineItem{}
|
||||
}
|
||||
|
||||
// Prepare data based on document type
|
||||
data := map[string]interface{}{
|
||||
"Document": document,
|
||||
"DocType": string(document.Type),
|
||||
"LineItems": lineItems,
|
||||
}
|
||||
|
||||
// Add document type specific data
|
||||
switch document.Type {
|
||||
case db.DocumentsTypeQuote:
|
||||
// For quotes, we might need to load enquiry data
|
||||
if document.CmcReference != "" {
|
||||
// The CmcReference for quotes is the enquiry title
|
||||
data["EnquiryTitle"] = document.CmcReference
|
||||
}
|
||||
|
||||
case db.DocumentsTypeInvoice:
|
||||
// For invoices, load job and customer data if needed
|
||||
data["ShowPaymentButton"] = true
|
||||
|
||||
case db.DocumentsTypePurchaseOrder:
|
||||
// For purchase orders, load principle data if needed
|
||||
|
||||
case db.DocumentsTypeOrderAck:
|
||||
// For order acknowledgements, load job data if needed
|
||||
|
||||
case db.DocumentsTypePackingList:
|
||||
// For packing lists, load job data if needed
|
||||
}
|
||||
|
||||
// Render the appropriate template
|
||||
if err := h.tmpl.Render(w, "documents/view.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
206
go-app/internal/cmc/handlers/statuses.go
Normal file
206
go-app/internal/cmc/handlers/statuses.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type StatusHandler struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewStatusHandler(queries *db.Queries) *StatusHandler {
|
||||
return &StatusHandler{queries: queries}
|
||||
}
|
||||
|
||||
func (h *StatusHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 50
|
||||
offset := 0
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if val, err := strconv.Atoi(l); err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if val, err := strconv.Atoi(o); err == nil {
|
||||
offset = val
|
||||
}
|
||||
}
|
||||
|
||||
statuses, err := h.queries.ListStatuses(r.Context(), db.ListStatusesParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(statuses)
|
||||
}
|
||||
|
||||
func (h *StatusHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid status ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.queries.GetStatus(r.Context(), int32(id))
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
http.Error(w, "Status not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
func (h *StatusHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse form data for HTMX requests
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
params := db.CreateStatusParams{
|
||||
Name: r.FormValue("name"),
|
||||
Color: sql.NullString{String: r.FormValue("color"), Valid: r.FormValue("color") != ""},
|
||||
}
|
||||
|
||||
// Check if this is a JSON request
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.queries.CreateStatus(r.Context(), params)
|
||||
if err != nil {
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-danger">Error creating status</div>`))
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// If HTMX request, return success message
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<div class="notification is-success">Status created successfully</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// JSON response for API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *StatusHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid status ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var params db.UpdateStatusParams
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle form data
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
params = db.UpdateStatusParams{
|
||||
Name: r.FormValue("name"),
|
||||
Color: sql.NullString{String: r.FormValue("color"), Valid: r.FormValue("color") != ""},
|
||||
}
|
||||
}
|
||||
|
||||
params.ID = int32(id)
|
||||
|
||||
if err := h.queries.UpdateStatus(r.Context(), params); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *StatusHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid status ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.queries.DeleteStatus(r.Context(), int32(id)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *StatusHandler) JsonList(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
selectedID := 0
|
||||
if sid := vars["selectedId"]; sid != "" {
|
||||
selectedID, _ = strconv.Atoi(sid)
|
||||
}
|
||||
|
||||
statuses, err := h.queries.ListStatuses(r.Context(), db.ListStatusesParams{
|
||||
Limit: 100,
|
||||
Offset: 0,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Format for JSON list with selected indicator
|
||||
var results []map[string]interface{}
|
||||
for _, status := range statuses {
|
||||
results = append(results, map[string]interface{}{
|
||||
"id": status.ID,
|
||||
"name": status.Name,
|
||||
"color": status.Color.String,
|
||||
"selected": status.ID == int32(selectedID),
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
310
go-app/internal/cmc/pdf/generator.go
Normal file
310
go-app/internal/cmc/pdf/generator.go
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package pdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
)
|
||||
|
||||
// Generator handles PDF generation for documents
|
||||
type Generator struct {
|
||||
pdf *gofpdf.Fpdf
|
||||
outputDir string
|
||||
headerText string
|
||||
footerText string
|
||||
docRef string
|
||||
currentPage int
|
||||
}
|
||||
|
||||
// NewGenerator creates a new PDF generator
|
||||
func NewGenerator(outputDir string) *Generator {
|
||||
pdf := gofpdf.New("P", "mm", "A4", "")
|
||||
|
||||
return &Generator{
|
||||
pdf: pdf,
|
||||
outputDir: outputDir,
|
||||
headerText: "CMC TECHNOLOGIES",
|
||||
footerText: "Copyright © %d CMC Technologies. All rights reserved.",
|
||||
}
|
||||
}
|
||||
|
||||
// AddPage adds a new page to the PDF
|
||||
func (g *Generator) AddPage() {
|
||||
g.pdf.AddPage()
|
||||
g.currentPage++
|
||||
}
|
||||
|
||||
// Page1Header adds the standard header for page 1
|
||||
func (g *Generator) Page1Header() {
|
||||
g.pdf.SetY(10)
|
||||
|
||||
// Set text color to blue
|
||||
g.pdf.SetTextColor(0, 0, 152)
|
||||
|
||||
// Add logo if available (assuming logo is in static directory)
|
||||
// logoPath := filepath.Join("static", "images", "cmclogosmall.png")
|
||||
// Try to add logo, but don't fail if it doesn't exist or isn't a proper image
|
||||
// g.pdf.ImageOptions(logoPath, 10, 10, 0, 28, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "http://www.cmctechnologies.com.au")
|
||||
|
||||
// Company name
|
||||
g.pdf.SetFont("Helvetica", "B", 30)
|
||||
g.pdf.SetX(40)
|
||||
g.pdf.CellFormat(0, 0, g.headerText, "", 1, "C", false, 0, "")
|
||||
|
||||
// Company details
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.SetY(22)
|
||||
g.pdf.SetX(40)
|
||||
g.pdf.CellFormat(0, 0, "PTY LIMITED ACN: 085 991 224 ABN: 47 085 991 224", "", 1, "C", false, 0, "")
|
||||
|
||||
// Draw horizontal line
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
g.pdf.Line(43, 24, 200, 24)
|
||||
|
||||
// Contact details
|
||||
g.pdf.SetTextColor(0, 0, 0)
|
||||
g.pdf.SetY(32)
|
||||
|
||||
// Left column - labels
|
||||
g.pdf.SetX(45)
|
||||
g.pdf.MultiCell(30, 5, "Phone:\nFax:\nEmail:\nWeb Site:", "", "L", false)
|
||||
|
||||
// Middle column - values
|
||||
g.pdf.SetY(32)
|
||||
g.pdf.SetX(65)
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.MultiCell(55, 5, "+61 2 9669 4000\n+61 2 9669 4111\nsales@cmctechnologies.com.au\nwww.cmctechnologies.net.au", "", "L", false)
|
||||
|
||||
// Right column - address
|
||||
g.pdf.SetY(32)
|
||||
g.pdf.SetX(150)
|
||||
g.pdf.MultiCell(52, 5, "Unit 19, 77 Bourke Rd\nAlexandria NSW 2015\nAUSTRALIA", "", "L", false)
|
||||
|
||||
// Engineering text
|
||||
g.pdf.SetTextColor(0, 0, 152)
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.SetY(37)
|
||||
g.pdf.SetX(10)
|
||||
g.pdf.MultiCell(30, 5, "Engineering &\nIndustrial\nInstrumentation", "", "L", false)
|
||||
}
|
||||
|
||||
// Page1Footer adds the standard footer
|
||||
func (g *Generator) Page1Footer() {
|
||||
g.pdf.SetY(-20)
|
||||
|
||||
// Footer line
|
||||
g.pdf.SetDrawColor(0, 0, 0)
|
||||
g.pdf.Line(10, g.pdf.GetY(), 200, g.pdf.GetY())
|
||||
|
||||
// Footer text
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
g.pdf.SetY(-18)
|
||||
g.pdf.CellFormat(0, 5, "CMC TECHNOLOGIES Provides Solutions in the Following Fields", "", 1, "C", false, 0, "")
|
||||
|
||||
// Color-coded services
|
||||
g.pdf.SetY(-13)
|
||||
g.pdf.SetX(10)
|
||||
|
||||
// First line of services
|
||||
services := []struct {
|
||||
text string
|
||||
r, g, b int
|
||||
}{
|
||||
{"EXPLOSION PREVENTION AND PROTECTION", 153, 0, 10},
|
||||
{"—", 0, 0, 0},
|
||||
{"FIRE PROTECTION", 255, 153, 0},
|
||||
{"—", 0, 0, 0},
|
||||
{"PRESSURE RELIEF", 255, 0, 25},
|
||||
{"—", 0, 0, 0},
|
||||
{"VISION IN THE PROCESS", 0, 128, 30},
|
||||
}
|
||||
|
||||
x := 15.0
|
||||
for _, service := range services {
|
||||
g.pdf.SetTextColor(service.r, service.g, service.b)
|
||||
width := g.pdf.GetStringWidth(service.text)
|
||||
g.pdf.SetX(x)
|
||||
g.pdf.CellFormat(width, 5, service.text, "", 0, "L", false, 0, "")
|
||||
x += width + 1
|
||||
}
|
||||
|
||||
// Second line of services
|
||||
g.pdf.SetY(-8)
|
||||
g.pdf.SetX(60)
|
||||
g.pdf.SetTextColor(47, 75, 224)
|
||||
g.pdf.CellFormat(0, 5, "FLOW MEASUREMENT", "", 0, "L", false, 0, "")
|
||||
g.pdf.SetX(110)
|
||||
g.pdf.SetTextColor(171, 49, 248)
|
||||
g.pdf.CellFormat(0, 5, "—PROCESS INSTRUMENTATION", "", 0, "L", false, 0, "")
|
||||
}
|
||||
|
||||
// DetailsBox adds the document details box (quote/invoice details)
|
||||
func (g *Generator) DetailsBox(docType, companyName, emailTo, attention, fromName, fromEmail, refNumber, yourRef, issueDate string) {
|
||||
g.pdf.SetY(60)
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.SetTextColor(0, 0, 0)
|
||||
|
||||
// Create details table
|
||||
lineHeight := 5.0
|
||||
col1Width := 40.0
|
||||
col2Width := 60.0
|
||||
col3Width := 40.0
|
||||
col4Width := 50.0
|
||||
|
||||
// Row 1
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col1Width, lineHeight, "COMPANY NAME:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col2Width, lineHeight, companyName, "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col3Width, lineHeight, docType+" NO.:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col4Width, lineHeight, refNumber, "1", 1, "L", false, 0, "")
|
||||
|
||||
// Row 2
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col1Width, lineHeight, "EMAIL TO:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col2Width, lineHeight, emailTo, "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col3Width, lineHeight, "YOUR REFERENCE:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col4Width, lineHeight, yourRef, "1", 1, "L", false, 0, "")
|
||||
|
||||
// Row 3
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col1Width, lineHeight, "ATTENTION:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col2Width, lineHeight, attention, "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col3Width, lineHeight, "ISSUE DATE:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col4Width, lineHeight, issueDate, "1", 1, "L", false, 0, "")
|
||||
|
||||
// Row 4
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col1Width, lineHeight, "FROM:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col2Width, lineHeight, fromName, "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col3Width, lineHeight, "PAGES:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
pageText := fmt.Sprintf("%d of {nb}", g.pdf.PageNo())
|
||||
g.pdf.CellFormat(col4Width, lineHeight, pageText, "1", 1, "L", false, 0, "")
|
||||
|
||||
// Row 5
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(col1Width, lineHeight, "EMAIL:", "1", 0, "L", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.CellFormat(col2Width, lineHeight, fromEmail, "1", 0, "L", false, 0, "")
|
||||
g.pdf.CellFormat(col3Width+col4Width, lineHeight, "", "1", 1, "L", false, 0, "")
|
||||
}
|
||||
|
||||
// AddContent adds HTML content to the PDF
|
||||
func (g *Generator) AddContent(content string) {
|
||||
g.pdf.SetFont("Helvetica", "", 10)
|
||||
g.pdf.SetTextColor(0, 0, 0)
|
||||
g.pdf.SetY(g.pdf.GetY() + 10)
|
||||
|
||||
// Basic HTML to PDF conversion
|
||||
// Note: gofpdf has limited HTML support, so we'll need to parse and convert manually
|
||||
// For now, we'll just add the content as plain text
|
||||
g.pdf.MultiCell(0, 5, content, "", "L", false)
|
||||
}
|
||||
|
||||
// AddLineItemsTable adds a table of line items
|
||||
func (g *Generator) AddLineItemsTable(items []LineItem, currencySymbol string, showGST bool) {
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.SetTextColor(0, 0, 0)
|
||||
|
||||
// Column widths
|
||||
itemWidth := 15.0
|
||||
qtyWidth := 15.0
|
||||
descWidth := 100.0
|
||||
unitWidth := 30.0
|
||||
totalWidth := 30.0
|
||||
|
||||
// Table header
|
||||
g.pdf.CellFormat(itemWidth, 7, "ITEM", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(qtyWidth, 7, "QTY", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(descWidth, 7, "DESCRIPTION", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(unitWidth, 7, "UNIT PRICE", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 7, "TOTAL", "1", 1, "C", false, 0, "")
|
||||
|
||||
// Table rows
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
|
||||
subtotal := 0.0
|
||||
for _, item := range items {
|
||||
// Check if we need a new page
|
||||
if g.pdf.GetY() > 250 {
|
||||
g.AddPage()
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
g.pdf.CellFormat(itemWidth, 7, "ITEM", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(qtyWidth, 7, "QTY", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(descWidth, 7, "DESCRIPTION", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(unitWidth, 7, "UNIT PRICE", "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 7, "TOTAL", "1", 1, "C", false, 0, "")
|
||||
g.pdf.SetFont("Helvetica", "", 9)
|
||||
}
|
||||
|
||||
g.pdf.CellFormat(itemWidth, 6, item.ItemNumber, "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(qtyWidth, 6, item.Quantity, "1", 0, "C", false, 0, "")
|
||||
g.pdf.CellFormat(descWidth, 6, item.Title, "1", 0, "L", false, 0, "")
|
||||
g.pdf.CellFormat(unitWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, item.UnitPrice), "1", 0, "R", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, item.TotalPrice), "1", 1, "R", false, 0, "")
|
||||
|
||||
subtotal += item.TotalPrice
|
||||
}
|
||||
|
||||
// Totals
|
||||
g.pdf.SetFont("Helvetica", "B", 10)
|
||||
|
||||
// Subtotal
|
||||
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
|
||||
g.pdf.CellFormat(unitWidth, 6, "SUBTOTAL:", "1", 0, "R", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, subtotal), "1", 1, "R", false, 0, "")
|
||||
|
||||
// GST if applicable
|
||||
if showGST {
|
||||
gst := subtotal * 0.10
|
||||
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
|
||||
g.pdf.CellFormat(unitWidth, 6, "GST (10%):", "1", 0, "R", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, gst), "1", 1, "R", false, 0, "")
|
||||
|
||||
// Total
|
||||
total := subtotal + gst
|
||||
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
|
||||
g.pdf.CellFormat(unitWidth, 6, "TOTAL DUE:", "1", 0, "R", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, total), "1", 1, "R", false, 0, "")
|
||||
} else {
|
||||
// Total without GST
|
||||
g.pdf.SetX(g.pdf.GetX() + itemWidth + qtyWidth + descWidth)
|
||||
g.pdf.CellFormat(unitWidth, 6, "TOTAL:", "1", 0, "R", false, 0, "")
|
||||
g.pdf.CellFormat(totalWidth, 6, fmt.Sprintf("%s%.2f", currencySymbol, subtotal), "1", 1, "R", false, 0, "")
|
||||
}
|
||||
}
|
||||
|
||||
// Save saves the PDF to a file
|
||||
func (g *Generator) Save(filename string) error {
|
||||
g.pdf.AliasNbPages("")
|
||||
outputPath := filepath.Join(g.outputDir, filename)
|
||||
fmt.Printf("Generator.Save: Saving PDF to path: %s\n", outputPath)
|
||||
err := g.pdf.OutputFileAndClose(outputPath)
|
||||
if err != nil {
|
||||
fmt.Printf("Generator.Save: Error saving PDF: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Generator.Save: PDF saved successfully to: %s\n", outputPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LineItem represents a line item for the PDF
|
||||
type LineItem struct {
|
||||
ItemNumber string
|
||||
Quantity string
|
||||
Title string
|
||||
UnitPrice float64
|
||||
TotalPrice float64
|
||||
}
|
||||
316
go-app/internal/cmc/pdf/templates.go
Normal file
316
go-app/internal/cmc/pdf/templates.go
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package pdf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
)
|
||||
|
||||
// QuotePDFData contains all data needed to generate a quote PDF
|
||||
type QuotePDFData struct {
|
||||
Document *db.GetDocumentByIDRow
|
||||
Quote interface{} // Quote specific data
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Contact interface{} // Contact data
|
||||
User *db.GetUserRow
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
CommercialComments string
|
||||
}
|
||||
|
||||
// GenerateQuotePDF generates a PDF for a quote
|
||||
func GenerateQuotePDF(data *QuotePDFData, outputDir string) (string, error) {
|
||||
fmt.Printf("GenerateQuotePDF called with outputDir: %s\n", outputDir)
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
// Extract data for details box
|
||||
companyName := data.Document.CustomerName
|
||||
emailTo := "" // TODO: Get from contact
|
||||
attention := "" // TODO: Get from contact
|
||||
fromName := fmt.Sprintf("%s %s", data.User.FirstName, data.User.LastName)
|
||||
fromEmail := data.User.Email
|
||||
enquiryNumber := ""
|
||||
if data.Document.EnquiryTitle.Valid {
|
||||
enquiryNumber = data.Document.EnquiryTitle.String
|
||||
}
|
||||
|
||||
if data.Document.Revision > 0 {
|
||||
enquiryNumber = fmt.Sprintf("%s.%d", enquiryNumber, data.Document.Revision)
|
||||
}
|
||||
|
||||
yourReference := fmt.Sprintf("Enquiry on %s", data.Document.Created.Format("2 Jan 2006"))
|
||||
issueDate := time.Now().Format("2 January 2006")
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("QUOTE", companyName, emailTo, attention, fromName, fromEmail, enquiryNumber, yourReference, issueDate)
|
||||
|
||||
// Add page content if any
|
||||
// TODO: Add document pages content
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
// Add pricing page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "PRICING & SPECIFICATIONS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
}
|
||||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
Title: item.Title,
|
||||
UnitPrice: unitPrice,
|
||||
TotalPrice: totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
// Add commercial comments if any
|
||||
if data.CommercialComments != "" {
|
||||
gen.pdf.Ln(10)
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(0, 5, "COMMERCIAL COMMENTS", "", 1, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 9)
|
||||
gen.pdf.MultiCell(0, 5, data.CommercialComments, "", "L", false)
|
||||
}
|
||||
|
||||
// TODO: Add terms and conditions page
|
||||
|
||||
// Generate filename
|
||||
filename := enquiryNumber
|
||||
if data.Document.Revision > 0 {
|
||||
filename = fmt.Sprintf("%s_%d.pdf", enquiryNumber, data.Document.Revision)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s.pdf", enquiryNumber)
|
||||
}
|
||||
|
||||
// Save PDF
|
||||
fmt.Printf("Saving PDF with filename: %s to outputDir: %s\n", filename, outputDir)
|
||||
err := gen.Save(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving PDF: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("PDF saved successfully: %s\n", filename)
|
||||
}
|
||||
return filename, err
|
||||
}
|
||||
|
||||
// InvoicePDFData contains all data needed to generate an invoice PDF
|
||||
type InvoicePDFData struct {
|
||||
Document *db.Document
|
||||
Invoice *db.Invoice
|
||||
Enquiry *db.Enquiry
|
||||
Customer *db.Customer
|
||||
Job interface{} // Job data
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
ShipVia string
|
||||
FOB string
|
||||
IssueDate time.Time
|
||||
}
|
||||
|
||||
// GenerateInvoicePDF generates a PDF for an invoice
|
||||
func GenerateInvoicePDF(data *InvoicePDFData, outputDir string) (string, error) {
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
// Extract data for details box
|
||||
companyName := data.Customer.Name
|
||||
emailTo := "" // TODO: Get from contact
|
||||
attention := "" // TODO: Get from contact
|
||||
fromName := "" // TODO: Get from user
|
||||
fromEmail := "" // TODO: Get from user
|
||||
invoiceNumber := data.Invoice.Title
|
||||
|
||||
yourReference := "" // TODO: Get reference
|
||||
issueDate := data.IssueDate.Format("2 January 2006")
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("INVOICE", companyName, emailTo, attention, fromName, fromEmail, invoiceNumber, yourReference, issueDate)
|
||||
|
||||
// Add shipping details
|
||||
gen.pdf.Ln(5)
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(30, 5, "Ship Via:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(60, 5, data.ShipVia, "", 1, "L", false, 0, "")
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(30, 5, "FOB:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(60, 5, data.FOB, "", 1, "L", false, 0, "")
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
// Add line items page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "INVOICE DETAILS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
}
|
||||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
Title: item.Title,
|
||||
UnitPrice: unitPrice,
|
||||
TotalPrice: totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
// Generate filename
|
||||
filename := fmt.Sprintf("%s.pdf", invoiceNumber)
|
||||
|
||||
// Save PDF
|
||||
err := gen.Save(filename)
|
||||
return filename, err
|
||||
}
|
||||
|
||||
// PurchaseOrderPDFData contains all data needed to generate a purchase order PDF
|
||||
type PurchaseOrderPDFData struct {
|
||||
Document *db.GetDocumentByIDRow
|
||||
PurchaseOrder *db.GetPurchaseOrderByDocumentIDRow
|
||||
Principle *db.Principle
|
||||
LineItems []db.GetLineItemsTableRow
|
||||
Currency interface{} // Currency data
|
||||
CurrencySymbol string
|
||||
ShowGST bool
|
||||
}
|
||||
|
||||
// GeneratePurchaseOrderPDF generates a PDF for a purchase order
|
||||
func GeneratePurchaseOrderPDF(data *PurchaseOrderPDFData, outputDir string) (string, error) {
|
||||
gen := NewGenerator(outputDir)
|
||||
|
||||
// First page with header
|
||||
gen.AddPage()
|
||||
gen.Page1Header()
|
||||
|
||||
// Extract data for details box
|
||||
companyName := data.Principle.Name
|
||||
emailTo := "" // TODO: Get from principle contact
|
||||
attention := "" // TODO: Get from principle contact
|
||||
fromName := "" // TODO: Get from user
|
||||
fromEmail := "" // TODO: Get from user
|
||||
poNumber := data.PurchaseOrder.Title
|
||||
|
||||
yourReference := data.PurchaseOrder.PrincipleReference
|
||||
issueDate := data.PurchaseOrder.IssueDate.Format("Monday, 2 January 2006")
|
||||
|
||||
// Add details box
|
||||
gen.DetailsBox("PURCHASE ORDER", companyName, emailTo, attention, fromName, fromEmail, poNumber, yourReference, issueDate)
|
||||
|
||||
// Add PO specific details
|
||||
gen.pdf.Ln(5)
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Ordered From:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.OrderedFrom, "", "L", false)
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Dispatch By:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.CellFormat(0, 5, data.PurchaseOrder.DispatchBy, "", 1, "L", false, 0, "")
|
||||
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(40, 5, "Deliver To:", "", 0, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.DeliverTo, "", "L", false)
|
||||
|
||||
if data.PurchaseOrder.ShippingInstructions != "" {
|
||||
gen.pdf.SetFont("Helvetica", "B", 10)
|
||||
gen.pdf.CellFormat(0, 5, "Shipping Instructions:", "", 1, "L", false, 0, "")
|
||||
gen.pdf.SetFont("Helvetica", "", 10)
|
||||
gen.pdf.MultiCell(0, 5, data.PurchaseOrder.ShippingInstructions, "", "L", false)
|
||||
}
|
||||
|
||||
gen.Page1Footer()
|
||||
|
||||
// Add line items page
|
||||
gen.AddPage()
|
||||
gen.pdf.SetFont("Helvetica", "B", 14)
|
||||
gen.pdf.CellFormat(0, 10, "ORDER DETAILS", "", 1, "C", false, 0, "")
|
||||
gen.pdf.Ln(5)
|
||||
|
||||
// Convert line items
|
||||
pdfItems := make([]LineItem, len(data.LineItems))
|
||||
for i, item := range data.LineItems {
|
||||
unitPrice := 0.0
|
||||
totalPrice := 0.0
|
||||
|
||||
// Parse prices
|
||||
if item.GrossUnitPrice.Valid {
|
||||
fmt.Sscanf(item.GrossUnitPrice.String, "%f", &unitPrice)
|
||||
}
|
||||
if item.GrossPrice.Valid {
|
||||
fmt.Sscanf(item.GrossPrice.String, "%f", &totalPrice)
|
||||
}
|
||||
|
||||
pdfItems[i] = LineItem{
|
||||
ItemNumber: item.ItemNumber,
|
||||
Quantity: item.Quantity,
|
||||
Title: item.Title,
|
||||
UnitPrice: unitPrice,
|
||||
TotalPrice: totalPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// Add line items table
|
||||
gen.AddLineItemsTable(pdfItems, data.CurrencySymbol, data.ShowGST)
|
||||
|
||||
// Generate filename
|
||||
filename := poNumber
|
||||
if data.Document.Revision > 0 {
|
||||
filename = fmt.Sprintf("%s-Rev%d.pdf", data.PurchaseOrder.Title, data.Document.Revision)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s.pdf", data.PurchaseOrder.Title)
|
||||
}
|
||||
|
||||
// Save PDF
|
||||
err := gen.Save(filename)
|
||||
return filename, err
|
||||
}
|
||||
|
|
@ -54,6 +54,15 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) {
|
|||
"enquiries/show.html",
|
||||
"enquiries/form.html",
|
||||
"enquiries/table.html",
|
||||
"documents/index.html",
|
||||
"documents/show.html",
|
||||
"documents/table.html",
|
||||
"documents/view.html",
|
||||
"documents/quote-view.html",
|
||||
"documents/invoice-view.html",
|
||||
"documents/purchase-order-view.html",
|
||||
"documents/orderack-view.html",
|
||||
"documents/packinglist-view.html",
|
||||
"index.html",
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +81,23 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// For documents view page, include all document type elements
|
||||
if page == "documents/view.html" {
|
||||
docElements := []string{
|
||||
"documents/quote-view.html",
|
||||
"documents/invoice-view.html",
|
||||
"documents/purchase-order-view.html",
|
||||
"documents/orderack-view.html",
|
||||
"documents/packinglist-view.html",
|
||||
}
|
||||
for _, elem := range docElements {
|
||||
elemPath := filepath.Join(templatesDir, elem)
|
||||
if _, err := os.Stat(elemPath); err == nil {
|
||||
files = append(files, elemPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New(filepath.Base(page)).Funcs(funcMap).ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
BIN
go-app/server
BIN
go-app/server
Binary file not shown.
45
go-app/sql/queries/addresses.sql
Normal file
45
go-app/sql/queries/addresses.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
-- name: GetAddress :one
|
||||
SELECT * FROM addresses
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListAddresses :many
|
||||
SELECT * FROM addresses
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: ListAddressesByCustomer :many
|
||||
SELECT * FROM addresses
|
||||
WHERE customer_id = ?
|
||||
ORDER BY name;
|
||||
|
||||
-- name: CreateAddress :execresult
|
||||
INSERT INTO addresses (
|
||||
name, address, city, state_id, country_id,
|
||||
customer_id, type, postcode
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?
|
||||
);
|
||||
|
||||
-- name: UpdateAddress :exec
|
||||
UPDATE addresses
|
||||
SET name = ?,
|
||||
address = ?,
|
||||
city = ?,
|
||||
state_id = ?,
|
||||
country_id = ?,
|
||||
customer_id = ?,
|
||||
type = ?,
|
||||
postcode = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteAddress :exec
|
||||
DELETE FROM addresses
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: GetCustomerAddresses :many
|
||||
SELECT a.*, s.name as state_name, s.shortform as state_shortform, c.name as country_name
|
||||
FROM addresses a
|
||||
LEFT JOIN states s ON a.state_id = s.id
|
||||
LEFT JOIN countries c ON a.country_id = c.id
|
||||
WHERE a.customer_id = ?
|
||||
ORDER BY a.name;
|
||||
41
go-app/sql/queries/attachments.sql
Normal file
41
go-app/sql/queries/attachments.sql
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
-- name: GetAttachment :one
|
||||
SELECT * FROM attachments
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListAttachments :many
|
||||
SELECT * FROM attachments
|
||||
WHERE archived = 0
|
||||
ORDER BY created DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: ListArchivedAttachments :many
|
||||
SELECT * FROM attachments
|
||||
WHERE archived = 1
|
||||
ORDER BY created DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CreateAttachment :execresult
|
||||
INSERT INTO attachments (
|
||||
principle_id, created, modified, name, filename,
|
||||
file, type, size, description, archived
|
||||
) VALUES (
|
||||
?, NOW(), NOW(), ?, ?, ?, ?, ?, ?, 0
|
||||
);
|
||||
|
||||
-- name: UpdateAttachment :exec
|
||||
UPDATE attachments
|
||||
SET modified = NOW(),
|
||||
name = ?,
|
||||
description = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteAttachment :exec
|
||||
UPDATE attachments
|
||||
SET archived = 1,
|
||||
modified = NOW()
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ListAttachmentsByPrinciple :many
|
||||
SELECT * FROM attachments
|
||||
WHERE principle_id = ? AND archived = 0
|
||||
ORDER BY created DESC;
|
||||
32
go-app/sql/queries/boxes.sql
Normal file
32
go-app/sql/queries/boxes.sql
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
-- name: GetBox :one
|
||||
SELECT * FROM boxes
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListBoxes :many
|
||||
SELECT * FROM boxes
|
||||
ORDER BY id DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: ListBoxesByShipment :many
|
||||
SELECT * FROM boxes
|
||||
WHERE shipment_id = ?
|
||||
ORDER BY id;
|
||||
|
||||
-- name: CreateBox :execresult
|
||||
INSERT INTO boxes (
|
||||
shipment_id, length, width, height, weight
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?
|
||||
);
|
||||
|
||||
-- name: UpdateBox :exec
|
||||
UPDATE boxes
|
||||
SET length = ?,
|
||||
width = ?,
|
||||
height = ?,
|
||||
weight = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteBox :exec
|
||||
DELETE FROM boxes
|
||||
WHERE id = ?;
|
||||
23
go-app/sql/queries/countries.sql
Normal file
23
go-app/sql/queries/countries.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- name: GetCountry :one
|
||||
SELECT * FROM countries
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListCountries :many
|
||||
SELECT * FROM countries
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CreateCountry :execresult
|
||||
INSERT INTO countries (name) VALUES (?);
|
||||
|
||||
-- name: UpdateCountry :exec
|
||||
UPDATE countries SET name = ? WHERE id = ?;
|
||||
|
||||
-- name: DeleteCountry :exec
|
||||
DELETE FROM countries WHERE id = ?;
|
||||
|
||||
-- name: SearchCountriesByName :many
|
||||
SELECT * FROM countries
|
||||
WHERE name LIKE CONCAT('%', ?, '%')
|
||||
ORDER BY name
|
||||
LIMIT 10;
|
||||
201
go-app/sql/queries/documents.sql
Normal file
201
go-app/sql/queries/documents.sql
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
-- name: ListDocuments :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000;
|
||||
|
||||
-- name: ListDocumentsByType :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE d.type = ?
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000;
|
||||
|
||||
-- name: GetDocumentByID :one
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.shipping_details,
|
||||
d.bill_to,
|
||||
d.ship_to,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE d.id = ?;
|
||||
|
||||
-- name: CreateDocument :execresult
|
||||
INSERT INTO documents (
|
||||
type,
|
||||
created,
|
||||
user_id,
|
||||
doc_page_count,
|
||||
cmc_reference,
|
||||
pdf_filename,
|
||||
pdf_created_at,
|
||||
pdf_created_by_user_id,
|
||||
shipping_details,
|
||||
revision,
|
||||
bill_to,
|
||||
ship_to,
|
||||
email_sent_at,
|
||||
email_sent_by_user_id
|
||||
) VALUES (
|
||||
?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
);
|
||||
|
||||
-- name: UpdateDocument :exec
|
||||
UPDATE documents
|
||||
SET
|
||||
type = ?,
|
||||
user_id = ?,
|
||||
doc_page_count = ?,
|
||||
cmc_reference = ?,
|
||||
pdf_filename = ?,
|
||||
pdf_created_at = ?,
|
||||
pdf_created_by_user_id = ?,
|
||||
shipping_details = ?,
|
||||
revision = ?,
|
||||
bill_to = ?,
|
||||
ship_to = ?,
|
||||
email_sent_at = ?,
|
||||
email_sent_by_user_id = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ArchiveDocument :exec
|
||||
DELETE FROM documents
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: UnarchiveDocument :exec
|
||||
-- Note: Unarchiving not supported as documents table doesn't have an archived column
|
||||
-- This is a no-op for compatibility
|
||||
SELECT 1 WHERE ? = ?;
|
||||
|
||||
-- name: SearchDocuments :many
|
||||
SELECT
|
||||
d.id,
|
||||
d.type,
|
||||
d.created,
|
||||
d.user_id,
|
||||
d.doc_page_count,
|
||||
d.pdf_filename,
|
||||
d.pdf_created_at,
|
||||
d.pdf_created_by_user_id,
|
||||
d.cmc_reference,
|
||||
d.revision,
|
||||
d.email_sent_at,
|
||||
d.email_sent_by_user_id,
|
||||
u.first_name as user_first_name,
|
||||
u.last_name as user_last_name,
|
||||
u.username as user_username,
|
||||
pdf_creator.first_name as pdf_creator_first_name,
|
||||
pdf_creator.last_name as pdf_creator_last_name,
|
||||
pdf_creator.username as pdf_creator_username,
|
||||
COALESCE(ec.name, ic.name, '') as customer_name,
|
||||
e.title as enquiry_title
|
||||
FROM documents d
|
||||
LEFT JOIN users u ON d.user_id = u.id
|
||||
LEFT JOIN users pdf_creator ON d.pdf_created_by_user_id = pdf_creator.id
|
||||
LEFT JOIN enquiries e ON d.type IN ('quote', 'orderAck') AND d.cmc_reference = e.title
|
||||
LEFT JOIN customers ec ON e.customer_id = ec.id
|
||||
LEFT JOIN invoices i ON d.type = 'invoice' AND d.cmc_reference = i.title
|
||||
LEFT JOIN customers ic ON i.customer_id = ic.id
|
||||
WHERE (
|
||||
d.pdf_filename LIKE ? OR
|
||||
d.cmc_reference LIKE ? OR
|
||||
COALESCE(ec.name, ic.name) LIKE ? OR
|
||||
e.title LIKE ? OR
|
||||
u.username LIKE ?
|
||||
)
|
||||
ORDER BY d.id DESC
|
||||
LIMIT 1000;
|
||||
|
||||
-- name: UpdateDocumentPDFInfo :exec
|
||||
UPDATE documents
|
||||
SET
|
||||
pdf_filename = ?,
|
||||
pdf_created_at = ?,
|
||||
pdf_created_by_user_id = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: GetPurchaseOrderByDocumentID :one
|
||||
SELECT
|
||||
po.id,
|
||||
po.title,
|
||||
po.principle_id,
|
||||
po.principle_reference,
|
||||
po.issue_date,
|
||||
po.ordered_from,
|
||||
po.dispatch_by,
|
||||
po.deliver_to,
|
||||
po.shipping_instructions
|
||||
FROM purchase_orders po
|
||||
JOIN documents d ON d.cmc_reference = po.title
|
||||
WHERE d.id = ? AND d.type = 'purchaseOrder';
|
||||
77
go-app/sql/queries/line_items.sql
Normal file
77
go-app/sql/queries/line_items.sql
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
-- name: GetLineItem :one
|
||||
SELECT * FROM line_items
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListLineItems :many
|
||||
SELECT * FROM line_items
|
||||
ORDER BY item_number ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: ListLineItemsByDocument :many
|
||||
SELECT * FROM line_items
|
||||
WHERE document_id = ?
|
||||
ORDER BY item_number ASC;
|
||||
|
||||
-- name: CreateLineItem :execresult
|
||||
INSERT INTO line_items (
|
||||
item_number, `option`, quantity, title, description,
|
||||
document_id, product_id, has_text_prices, has_price,
|
||||
unit_price_string, gross_price_string, costing_id,
|
||||
gross_unit_price, net_unit_price, discount_percent,
|
||||
discount_amount_unit, discount_amount_total,
|
||||
gross_price, net_price
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
);
|
||||
|
||||
-- name: UpdateLineItem :exec
|
||||
UPDATE line_items
|
||||
SET item_number = ?,
|
||||
`option` = ?,
|
||||
quantity = ?,
|
||||
title = ?,
|
||||
description = ?,
|
||||
document_id = ?,
|
||||
product_id = ?,
|
||||
has_text_prices = ?,
|
||||
has_price = ?,
|
||||
unit_price_string = ?,
|
||||
gross_price_string = ?,
|
||||
costing_id = ?,
|
||||
gross_unit_price = ?,
|
||||
net_unit_price = ?,
|
||||
discount_percent = ?,
|
||||
discount_amount_unit = ?,
|
||||
discount_amount_total = ?,
|
||||
gross_price = ?,
|
||||
net_price = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteLineItem :exec
|
||||
DELETE FROM line_items
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: GetLineItemsTable :many
|
||||
SELECT li.*, p.title as product_title, p.model_number
|
||||
FROM line_items li
|
||||
LEFT JOIN products p ON li.product_id = p.id
|
||||
WHERE li.document_id = ?
|
||||
ORDER BY li.item_number ASC;
|
||||
|
||||
-- name: GetLineItemsByProduct :many
|
||||
SELECT * FROM line_items
|
||||
WHERE product_id = ?
|
||||
ORDER BY item_number ASC;
|
||||
|
||||
-- name: GetMaxItemNumber :one
|
||||
SELECT COALESCE(MAX(item_number), 0) as max_item_number
|
||||
FROM line_items
|
||||
WHERE document_id = ?;
|
||||
|
||||
-- name: UpdateLineItemPrices :exec
|
||||
UPDATE line_items
|
||||
SET gross_unit_price = ?,
|
||||
net_unit_price = ?,
|
||||
gross_price = ?,
|
||||
net_price = ?
|
||||
WHERE id = ?;
|
||||
19
go-app/sql/queries/principles.sql
Normal file
19
go-app/sql/queries/principles.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- name: GetPrinciple :one
|
||||
SELECT * FROM principles
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListPrinciples :many
|
||||
SELECT * FROM principles
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CreatePrinciple :execresult
|
||||
INSERT INTO principles (name, short_name, code) VALUES (?, ?, ?);
|
||||
|
||||
-- name: UpdatePrinciple :exec
|
||||
UPDATE principles SET name = ?, short_name = ?, code = ? WHERE id = ?;
|
||||
|
||||
-- name: GetPrincipleProducts :many
|
||||
SELECT * FROM products
|
||||
WHERE principle_id = ?
|
||||
ORDER BY title;
|
||||
26
go-app/sql/queries/states.sql
Normal file
26
go-app/sql/queries/states.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
-- name: GetState :one
|
||||
SELECT * FROM states
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListStates :many
|
||||
SELECT * FROM states
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CreateState :execresult
|
||||
INSERT INTO states (
|
||||
name, shortform, enqform
|
||||
) VALUES (
|
||||
?, ?, ?
|
||||
);
|
||||
|
||||
-- name: UpdateState :exec
|
||||
UPDATE states
|
||||
SET name = ?,
|
||||
shortform = ?,
|
||||
enqform = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: DeleteState :exec
|
||||
DELETE FROM states
|
||||
WHERE id = ?;
|
||||
17
go-app/sql/queries/statuses.sql
Normal file
17
go-app/sql/queries/statuses.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- name: GetStatus :one
|
||||
SELECT * FROM statuses
|
||||
WHERE id = ? LIMIT 1;
|
||||
|
||||
-- name: ListStatuses :many
|
||||
SELECT * FROM statuses
|
||||
ORDER BY name
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- name: CreateStatus :execresult
|
||||
INSERT INTO statuses (name, color) VALUES (?, ?);
|
||||
|
||||
-- name: UpdateStatus :exec
|
||||
UPDATE statuses SET name = ?, color = ? WHERE id = ?;
|
||||
|
||||
-- name: DeleteStatus :exec
|
||||
DELETE FROM statuses WHERE id = ?;
|
||||
42
go-app/sql/queries/users.sql
Normal file
42
go-app/sql/queries/users.sql
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- name: GetAllUsers :many
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE archived = 0
|
||||
ORDER BY first_name, last_name;
|
||||
|
||||
-- name: GetUser :one
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT id, username, first_name, last_name, email, type, enabled, archived
|
||||
FROM users
|
||||
WHERE username = ? AND archived = 0;
|
||||
|
||||
-- name: CreateUser :execresult
|
||||
INSERT INTO users (
|
||||
username, first_name, last_name, email, type, enabled, archived
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, 0);
|
||||
|
||||
-- name: UpdateUser :exec
|
||||
UPDATE users
|
||||
SET
|
||||
username = ?,
|
||||
first_name = ?,
|
||||
last_name = ?,
|
||||
email = ?,
|
||||
type = ?,
|
||||
enabled = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: ArchiveUser :exec
|
||||
UPDATE users
|
||||
SET archived = 1
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: UnarchiveUser :exec
|
||||
UPDATE users
|
||||
SET archived = 0
|
||||
WHERE id = ?;
|
||||
7
go-app/sql/schema/005_invoices.sql
Normal file
7
go-app/sql/schema/005_invoices.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Invoices table schema (subset for document relationships)
|
||||
CREATE TABLE `invoices` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) NOT NULL COMMENT 'CMC Invoice Number String',
|
||||
`customer_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
13
go-app/sql/schema/006_addresses.sql
Normal file
13
go-app/sql/schema/006_addresses.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- Schema for addresses table
|
||||
CREATE TABLE IF NOT EXISTS `addresses` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL COMMENT 'Descriptive Name for this address',
|
||||
`address` text NOT NULL COMMENT 'street or unit number and street name',
|
||||
`city` varchar(255) NOT NULL COMMENT 'Suburb / City',
|
||||
`state_id` int(11) NOT NULL COMMENT 'State foreign Key',
|
||||
`country_id` int(11) NOT NULL COMMENT 'Country foreign Key',
|
||||
`customer_id` int(11) NOT NULL COMMENT 'Customer foreign key',
|
||||
`type` varchar(255) NOT NULL COMMENT 'either bill / ship / both',
|
||||
`postcode` varchar(50) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
15
go-app/sql/schema/007_attachments.sql
Normal file
15
go-app/sql/schema/007_attachments.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
-- Schema for attachments table
|
||||
CREATE TABLE IF NOT EXISTS `attachments` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`principle_id` int(11) NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`filename` varchar(255) NOT NULL,
|
||||
`file` varchar(255) NOT NULL,
|
||||
`type` varchar(255) NOT NULL,
|
||||
`size` int(11) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`archived` tinyint(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
10
go-app/sql/schema/008_boxes.sql
Normal file
10
go-app/sql/schema/008_boxes.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Schema for boxes table
|
||||
CREATE TABLE IF NOT EXISTS `boxes` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`shipment_id` int(11) NOT NULL,
|
||||
`length` decimal(5,1) NOT NULL,
|
||||
`width` decimal(5,1) NOT NULL,
|
||||
`height` decimal(5,1) NOT NULL,
|
||||
`weight` decimal(5,1) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
8
go-app/sql/schema/009_states.sql
Normal file
8
go-app/sql/schema/009_states.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Schema for states table
|
||||
CREATE TABLE IF NOT EXISTS `states` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`shortform` varchar(10) DEFAULT NULL,
|
||||
`enqform` varchar(10) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
7
go-app/sql/schema/010_statuses.sql
Normal file
7
go-app/sql/schema/010_statuses.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
-- Schema for statuses table
|
||||
CREATE TABLE IF NOT EXISTS `statuses` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`color` varchar(7) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
8
go-app/sql/schema/011_principles.sql
Normal file
8
go-app/sql/schema/011_principles.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Schema for principles table
|
||||
CREATE TABLE IF NOT EXISTS `principles` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`short_name` varchar(50) DEFAULT NULL,
|
||||
`code` int(3) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
6
go-app/sql/schema/012_countries.sql
Normal file
6
go-app/sql/schema/012_countries.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- Schema for countries table
|
||||
CREATE TABLE IF NOT EXISTS `countries` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
24
go-app/sql/schema/013_line_items.sql
Normal file
24
go-app/sql/schema/013_line_items.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- Schema for line_items table
|
||||
CREATE TABLE IF NOT EXISTS `line_items` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`item_number` decimal(10,2) NOT NULL,
|
||||
`option` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`quantity` decimal(10,2) NOT NULL,
|
||||
`title` varchar(500) NOT NULL,
|
||||
`description` text NOT NULL,
|
||||
`document_id` int(11) NOT NULL,
|
||||
`product_id` int(11) DEFAULT NULL,
|
||||
`has_text_prices` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`has_price` tinyint(4) NOT NULL DEFAULT 1,
|
||||
`unit_price_string` varchar(255) DEFAULT NULL,
|
||||
`gross_price_string` varchar(255) DEFAULT NULL,
|
||||
`costing_id` int(11) DEFAULT NULL,
|
||||
`gross_unit_price` decimal(10,2) DEFAULT NULL COMMENT 'Either fill this in or have a costing_id associated with this record',
|
||||
`net_unit_price` decimal(10,2) DEFAULT NULL,
|
||||
`discount_percent` decimal(10,2) DEFAULT NULL,
|
||||
`discount_amount_unit` decimal(10,2) DEFAULT NULL,
|
||||
`discount_amount_total` decimal(10,2) DEFAULT NULL,
|
||||
`gross_price` decimal(10,2) DEFAULT NULL,
|
||||
`net_price` decimal(10,2) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
23
go-app/sql/schema/documents.sql
Normal file
23
go-app/sql/schema/documents.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- Documents table schema (matches actual production database)
|
||||
CREATE TABLE `documents` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`type` enum('quote','invoice','purchaseOrder','orderAck','packingList') NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`doc_page_count` int(11) NOT NULL,
|
||||
`cmc_reference` varchar(255) NOT NULL COMMENT 'Either the Enquiry number, Invoice no, order ack. Convenient place to store this to save on queries',
|
||||
`pdf_filename` varchar(255) NOT NULL,
|
||||
`pdf_created_at` datetime NOT NULL,
|
||||
`pdf_created_by_user_id` int(11) NOT NULL,
|
||||
`shipping_details` text DEFAULT NULL,
|
||||
`revision` int(11) NOT NULL DEFAULT 0,
|
||||
`bill_to` text DEFAULT NULL,
|
||||
`ship_to` text DEFAULT NULL,
|
||||
`email_sent_at` datetime NOT NULL,
|
||||
`email_sent_by_user_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_documents_type` (`type`),
|
||||
KEY `idx_documents_created` (`created`),
|
||||
KEY `idx_documents_user_id` (`user_id`),
|
||||
KEY `idx_documents_cmc_reference` (`cmc_reference`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
-- Enquiries table schema
|
||||
CREATE TABLE `enquiries` (
|
||||
CREATE TABLE IF NOT EXISTS `enquiries` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime NOT NULL,
|
||||
`submitted` date DEFAULT NULL,
|
||||
|
|
@ -24,34 +24,4 @@ CREATE TABLE `enquiries` (
|
|||
`quote_count` int(11) NOT NULL DEFAULT 0,
|
||||
`archived` tinyint(4) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM AUTO_INCREMENT=17101 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
-- Related tables for foreign key references
|
||||
CREATE TABLE `statuses` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`color` varchar(7) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
CREATE TABLE `principles` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`short_name` varchar(50) DEFAULT NULL,
|
||||
`code` int(3) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
CREATE TABLE `states` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
`shortform` varchar(20) DEFAULT NULL,
|
||||
`enqform` varchar(10) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
||||
CREATE TABLE `countries` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
BIN
go-app/static/images/cmclogosmall.png
Normal file
BIN
go-app/static/images/cmclogosmall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
65
go-app/templates/documents/index.html
Normal file
65
go-app/templates/documents/index.html
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{{define "content"}}
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">Documents</h1>
|
||||
<p class="subtitle">Manage your documents (Quotes, Invoices, Purchase Orders, etc.)</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select id="type-filter" onchange="filterDocuments()">
|
||||
<option value="">All Types</option>
|
||||
<option value="quote" {{if eq .DocType "quote"}}selected{{end}}>Quotes</option>
|
||||
<option value="invoice" {{if eq .DocType "invoice"}}selected{{end}}>Invoices</option>
|
||||
<option value="purchaseOrder" {{if eq .DocType "purchaseOrder"}}selected{{end}}>Purchase Orders</option>
|
||||
<option value="orderAck" {{if eq .DocType "orderAck"}}selected{{end}}>Order Acknowledgements</option>
|
||||
<option value="packingList" {{if eq .DocType "packingList"}}selected{{end}}>Packing Lists</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input class="input" type="text" id="search-input" placeholder="Search by reference, filename, or user..."
|
||||
hx-get="/documents/search"
|
||||
hx-target="#document-table"
|
||||
hx-trigger="keyup changed delay:500ms, search"
|
||||
hx-include="#type-filter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="document-table">
|
||||
{{template "document-table" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function filterDocuments() {
|
||||
const typeFilter = document.getElementById('type-filter');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
|
||||
let url = '/documents';
|
||||
if (typeFilter.value) {
|
||||
url += '?type=' + encodeURIComponent(typeFilter.value);
|
||||
}
|
||||
|
||||
// Clear search when changing type filter
|
||||
searchInput.value = '';
|
||||
|
||||
// Update the table via HTMX
|
||||
htmx.ajax('GET', url, {
|
||||
target: '#document-table',
|
||||
headers: {'HX-Request': 'true'}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
175
go-app/templates/documents/invoice-view.html
Normal file
175
go-app/templates/documents/invoice-view.html
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
{{define "document-invoice-view"}}
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">
|
||||
Invoice: {{.Document.CmcReference}}
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Created {{.Document.Created.Format "2 January 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="buttons">
|
||||
{{if .ShowPaymentButton}}
|
||||
<button class="button is-success" onclick="enterPayment()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-dollar-sign"></i>
|
||||
</span>
|
||||
<span>Enter Payment</span>
|
||||
</button>
|
||||
{{end}}
|
||||
<button class="button is-primary" onclick="generatePDF()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Generate PDF</span>
|
||||
</button>
|
||||
<button class="button is-info" onclick="emailDocument()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span>Email Invoice</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Invoice Number:</th>
|
||||
<td>{{.Document.CmcReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By:</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
{{.Document.UserUsername.String}}
|
||||
{{else}}
|
||||
User #{{.Document.UserID}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td><span class="tag is-success">Invoice</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="notification is-light">
|
||||
<p class="has-text-weight-bold">Operations</p>
|
||||
<div class="buttons mt-2">
|
||||
<button class="button is-info is-small" onclick="createPackingList()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-box"></i>
|
||||
</span>
|
||||
<span>Create Packing List</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invoice Details -->
|
||||
<div class="box has-background-light">
|
||||
<h3 class="title is-5">Invoice Details</h3>
|
||||
<form>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Currency</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="currency">
|
||||
<option>AUD</option>
|
||||
<option>USD</option>
|
||||
<option>EUR</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Issue Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="issue_date" value="{{.Document.Created.Format "2006-01-02"}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Due Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="due_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Ship Via</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ship_via" placeholder="Shipping method">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">FOB</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="fob" placeholder="FOB terms">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Payment Terms</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="payment_terms" readonly placeholder="From customer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Shipping Details</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="shipping_details" rows="3">{{.Document.ShippingDetails.String}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">Save Invoice Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function enterPayment() {
|
||||
// TODO: Implement payment entry
|
||||
alert('Payment entry coming soon');
|
||||
}
|
||||
|
||||
function createPackingList() {
|
||||
if (confirm('Create a Packing List from this invoice?')) {
|
||||
// TODO: Implement packing list creation
|
||||
alert('Packing List creation coming soon');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
187
go-app/templates/documents/orderack-view.html
Normal file
187
go-app/templates/documents/orderack-view.html
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{{define "document-orderack-view"}}
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">
|
||||
Order Acknowledgement: {{.Document.CmcReference}}
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Created {{.Document.Created.Format "2 January 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" onclick="generatePDF()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Generate PDF</span>
|
||||
</button>
|
||||
<button class="button is-info" onclick="emailDocument()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span>Email Order Ack</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Order Ack Number:</th>
|
||||
<td>{{.Document.CmcReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By:</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
{{.Document.UserUsername.String}}
|
||||
{{else}}
|
||||
User #{{.Document.UserID}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td><span class="tag is-link">Order Acknowledgement</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="notification is-light">
|
||||
<p class="has-text-weight-bold">Operations</p>
|
||||
<div class="buttons mt-2">
|
||||
<button class="button is-success is-small" onclick="createInvoice()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
</span>
|
||||
<span>Create Invoice</span>
|
||||
</button>
|
||||
<button class="button is-info is-small" onclick="createPackingList()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-box"></i>
|
||||
</span>
|
||||
<span>Create Packing List</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Acknowledgement Details -->
|
||||
<div class="box has-background-light">
|
||||
<h3 class="title is-5">Order Acknowledgement Details</h3>
|
||||
<form>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Currency</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="currency">
|
||||
<option>AUD</option>
|
||||
<option>USD</option>
|
||||
<option>EUR</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Issue Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="issue_date" value="{{.Document.Created.Format "2006-01-02"}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Estimated Delivery</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="delivery_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Ship Via</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ship_via" placeholder="Shipping method">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">FOB</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="fob" placeholder="FOB terms">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Signature Required</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="signature_required">
|
||||
<option value="No">No</option>
|
||||
<option value="Yes">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Payment Terms</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="payment_terms" readonly placeholder="From customer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Shipping Details</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="shipping_details" rows="3">{{.Document.ShippingDetails.String}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">Save Order Ack Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function createInvoice() {
|
||||
if (confirm('Create an Invoice from this order acknowledgement?')) {
|
||||
// TODO: Implement invoice creation
|
||||
alert('Invoice creation coming soon');
|
||||
}
|
||||
}
|
||||
|
||||
function createPackingList() {
|
||||
if (confirm('Create a Packing List from this order acknowledgement?')) {
|
||||
// TODO: Implement packing list creation
|
||||
alert('Packing List creation coming soon');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
132
go-app/templates/documents/packinglist-view.html
Normal file
132
go-app/templates/documents/packinglist-view.html
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
{{define "document-packinglist-view"}}
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">
|
||||
Packing List: {{.Document.CmcReference}}
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Created {{.Document.Created.Format "2 January 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" onclick="generatePDF()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Generate PDF</span>
|
||||
</button>
|
||||
<button class="button is-info" onclick="emailDocument()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span>Email Packing List</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Packing List Number:</th>
|
||||
<td>{{.Document.CmcReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By:</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
{{.Document.UserUsername.String}}
|
||||
{{else}}
|
||||
User #{{.Document.UserID}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td><span class="tag is-info">Packing List</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Packing List Details -->
|
||||
<div class="box has-background-light">
|
||||
<h3 class="title is-5">Packing List Details</h3>
|
||||
<form>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Currency</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="currency">
|
||||
<option>AUD</option>
|
||||
<option>USD</option>
|
||||
<option>EUR</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Issue Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="issue_date" value="{{.Document.Created.Format "2006-01-02"}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Due Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="due_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">FOB</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="fob" placeholder="FOB terms">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Payment Terms</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="payment_terms" readonly placeholder="From customer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Shipping Details</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="shipping_details" rows="3">{{.Document.ShippingDetails.String}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">Save Packing List Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
185
go-app/templates/documents/purchase-order-view.html
Normal file
185
go-app/templates/documents/purchase-order-view.html
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
{{define "document-purchase-order-view"}}
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">
|
||||
Purchase Order: {{.Document.CmcReference}}
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Created {{.Document.Created.Format "2 January 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" onclick="generatePDF()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Generate PDF</span>
|
||||
</button>
|
||||
<button class="button is-info" onclick="emailDocument()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span>Email PO</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>PO Number:</th>
|
||||
<td>{{.Document.CmcReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By:</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
{{.Document.UserUsername.String}}
|
||||
{{else}}
|
||||
User #{{.Document.UserID}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td><span class="tag is-warning">Purchase Order</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Purchase Order Details -->
|
||||
<div class="box has-background-light">
|
||||
<h3 class="title is-5">Purchase Order Details</h3>
|
||||
<form>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Principle</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="principle_id">
|
||||
<option value="">Select Principle</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Principle Reference</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="principle_reference" placeholder="Reference number">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="description" placeholder="PO Description">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Currency</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select name="currency">
|
||||
<option>AUD</option>
|
||||
<option>USD</option>
|
||||
<option>EUR</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Dispatch Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="dispatch_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Arrival Date</label>
|
||||
<div class="control">
|
||||
<input class="input" type="date" name="arrival_date">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Ordered From</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="ordered_from">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Dispatch By</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="dispatch_by">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Deliver To</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="deliver_to">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Freight Forwarder</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="freight_forwarder">
|
||||
<option value="">Select Freight Forwarder</option>
|
||||
<option>DHL</option>
|
||||
<option>FedEx</option>
|
||||
<option>TNT</option>
|
||||
<option>Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Shipping Instructions</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="shipping_instructions" rows="3">{{.Document.ShippingDetails.String}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-primary">Save PO Details</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
119
go-app/templates/documents/quote-view.html
Normal file
119
go-app/templates/documents/quote-view.html
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
{{define "document-quote-view"}}
|
||||
<div class="box">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title">
|
||||
Quote: {{.Document.CmcReference}}
|
||||
{{if gt .Document.Revision 0}}
|
||||
<span class="tag is-warning">Rev {{.Document.Revision}}</span>
|
||||
{{end}}
|
||||
</h1>
|
||||
<p class="subtitle">
|
||||
Created {{.Document.Created.Format "2 January 2006"}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="buttons">
|
||||
<button class="button is-primary" onclick="generatePDF()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>Generate PDF</span>
|
||||
</button>
|
||||
<button class="button is-info" onclick="emailDocument()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<span>Email Quote</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Enquiry:</th>
|
||||
<td>
|
||||
{{if .EnquiryTitle}}
|
||||
<a href="/enquiries/view/{{.Document.CmcReference}}">{{.EnquiryTitle}}</a>
|
||||
{{else}}
|
||||
{{.Document.CmcReference}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By:</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
{{.Document.UserUsername.String}}
|
||||
{{else}}
|
||||
User #{{.Document.UserID}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status:</th>
|
||||
<td><span class="tag is-info">Quote</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="notification is-light">
|
||||
<p class="has-text-weight-bold">Operations</p>
|
||||
<div class="buttons mt-2">
|
||||
<button class="button is-success is-small" onclick="createOrderAck()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</span>
|
||||
<span>Create Order Acknowledgement</span>
|
||||
</button>
|
||||
<button class="button is-warning is-small" onclick="createInvoice()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
</span>
|
||||
<span>Create Invoice</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quote Details -->
|
||||
<div class="box has-background-light">
|
||||
<h3 class="title is-5">Quote Details</h3>
|
||||
<div class="content">
|
||||
<p class="has-text-grey">Quote page content and details would be displayed here.</p>
|
||||
<button class="button is-small">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
<span>Edit Quote Details</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function createOrderAck() {
|
||||
if (confirm('Create an Order Acknowledgement from this quote?')) {
|
||||
// TODO: Implement order acknowledgement creation
|
||||
alert('Order Acknowledgement creation coming soon');
|
||||
}
|
||||
}
|
||||
|
||||
function createInvoice() {
|
||||
if (confirm('Create an Invoice from this quote?')) {
|
||||
// TODO: Implement invoice creation
|
||||
alert('Invoice creation coming soon');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
144
go-app/templates/documents/show.html
Normal file
144
go-app/templates/documents/show.html
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
{{define "content"}}
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/documents">Documents</a></li>
|
||||
<li class="is-active"><a href="#" aria-current="page">Document #{{.Document.ID}}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<h1 class="title">Document #{{.Document.ID}}</h1>
|
||||
<h2 class="subtitle">
|
||||
<span class="tag {{if eq .Document.Type "quote"}}is-info{{else if eq .Document.Type "invoice"}}is-success{{else if eq .Document.Type "purchaseOrder"}}is-warning{{else if eq .Document.Type "orderAck"}}is-link{{else}}is-light{{end}}">
|
||||
{{if eq .Document.Type "quote"}}Quote
|
||||
{{else if eq .Document.Type "invoice"}}Invoice
|
||||
{{else if eq .Document.Type "purchaseOrder"}}Purchase Order
|
||||
{{else if eq .Document.Type "orderAck"}}Order Acknowledgement
|
||||
{{else if eq .Document.Type "packingList"}}Packing List
|
||||
{{else}}{{.Document.Type}}{{end}}
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
{{if .Document.PdfFilename}}
|
||||
<a href="/pdf/{{.Document.PdfFilename}}" target="_blank" class="button is-primary">
|
||||
<span class="icon">
|
||||
<i class="fas fa-file-pdf"></i>
|
||||
</span>
|
||||
<span>View PDF</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="box">
|
||||
<h3 class="title is-5">Document Information</h3>
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td>{{.Document.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{{.Document.Type}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<td>{{.Document.Created.Format "2 January 2006 at 15:04"}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created By</th>
|
||||
<td>
|
||||
{{if and .Document.UserFirstName.Valid .Document.UserLastName.Valid}}
|
||||
<a href="/users/{{.Document.UserID}}">
|
||||
{{.Document.UserFirstName.String}} {{.Document.UserLastName.String}}
|
||||
</a>
|
||||
{{else if .Document.UserUsername.Valid}}
|
||||
<a href="/users/{{.Document.UserID}}">
|
||||
{{.Document.UserUsername.String}}
|
||||
</a>
|
||||
{{else}}
|
||||
Unknown User
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Page Count</th>
|
||||
<td>{{.Document.DocPageCount}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-half">
|
||||
{{if .Document.PdfFilename}}
|
||||
<div class="box">
|
||||
<h3 class="title is-5">PDF Information</h3>
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<td>
|
||||
<a href="/pdf/{{.Document.PdfFilename}}" target="_blank">
|
||||
{{.Document.PdfFilename}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>PDF Created</th>
|
||||
<td>{{.Document.PdfCreatedAt.Format "2 January 2006 at 15:04"}}</td>
|
||||
</tr>
|
||||
{{if and .Document.PdfCreatorFirstName.Valid .Document.PdfCreatorLastName.Valid}}
|
||||
<tr>
|
||||
<th>PDF Created By</th>
|
||||
<td>
|
||||
<a href="/users/{{.Document.PdfCreatedByUserID}}">
|
||||
{{.Document.PdfCreatorFirstName.String}} {{.Document.PdfCreatorLastName.String}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if or .Document.CustomerName .Document.EnquiryTitle.Valid}}
|
||||
<div class="box">
|
||||
<h3 class="title is-5">Related Information</h3>
|
||||
<table class="table is-fullwidth">
|
||||
<tbody>
|
||||
{{if .Document.CustomerName}}
|
||||
<tr>
|
||||
<th>Customer</th>
|
||||
<td>{{.Document.CustomerName}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .Document.EnquiryTitle.Valid}}
|
||||
<tr>
|
||||
<th>Enquiry</th>
|
||||
<td>{{.Document.EnquiryTitle.String}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr>
|
||||
<th>CMC Reference</th>
|
||||
<td>{{.Document.CmcReference}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Revision</th>
|
||||
<td>{{.Document.Revision}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
123
go-app/templates/documents/table.html
Normal file
123
go-app/templates/documents/table.html
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
{{define "document-table"}}
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<th>User</th>
|
||||
<th>Type</th>
|
||||
<th>#Pages</th>
|
||||
<th>PDF Filename</th>
|
||||
<th>PDF Created</th>
|
||||
<th>PDF Created By</th>
|
||||
<th>Reference</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Documents}}
|
||||
<tr>
|
||||
<td>
|
||||
<time class="has-text-grey-dark" datetime="{{.Created.Format "2006-01-02T15:04:05Z"}}">
|
||||
{{.Created.Format "2 Jan 2006 15:04"}}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
{{if and .UserFirstName.Valid .UserLastName.Valid}}
|
||||
<a href="/users/{{.UserID}}" class="has-text-link">
|
||||
{{.UserFirstName.String}} {{.UserLastName.String}}
|
||||
</a>
|
||||
{{else if .UserUsername.Valid}}
|
||||
<a href="/users/{{.UserID}}" class="has-text-link">
|
||||
{{.UserUsername.String}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="has-text-grey">Unknown User</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<span class="tag {{if eq .Type "quote"}}is-info{{else if eq .Type "invoice"}}is-success{{else if eq .Type "purchaseOrder"}}is-warning{{else if eq .Type "orderAck"}}is-link{{else}}is-light{{end}}">
|
||||
{{if eq .Type "quote"}}Quote
|
||||
{{else if eq .Type "invoice"}}Invoice
|
||||
{{else if eq .Type "purchaseOrder"}}Purchase Order
|
||||
{{else if eq .Type "orderAck"}}Order Acknowledgement
|
||||
{{else if eq .Type "packingList"}}Packing List
|
||||
{{else}}{{.Type}}{{end}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{if .DocPageCount}}
|
||||
{{.DocPageCount}}
|
||||
{{else}}
|
||||
<span class="has-text-grey">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .PdfFilename}}
|
||||
<a href="/pdf/{{.PdfFilename}}" target="_blank" class="has-text-link">
|
||||
{{.PdfFilename}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="has-text-grey">No PDF</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<time class="has-text-grey-dark" datetime="{{.PdfCreatedAt.Format "2006-01-02T15:04:05Z"}}">
|
||||
{{.PdfCreatedAt.Format "2 Jan 2006 15:04"}}
|
||||
</time>
|
||||
</td>
|
||||
<td>
|
||||
{{if and .PdfCreatorFirstName.Valid .PdfCreatorLastName.Valid}}
|
||||
<a href="/users/{{.PdfCreatedByUserID}}" class="has-text-link">
|
||||
{{.PdfCreatorFirstName.String}} {{.PdfCreatorLastName.String}}
|
||||
</a>
|
||||
{{else if .PdfCreatorUsername.Valid}}
|
||||
<a href="/users/{{.PdfCreatedByUserID}}" class="has-text-link">
|
||||
{{.PdfCreatorUsername.String}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="has-text-grey">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .CmcReference}}
|
||||
<span class="has-text-link">{{.CmcReference}}</span>
|
||||
{{else}}
|
||||
<span class="has-text-grey">-</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons are-small">
|
||||
<a href="/documents/view/{{.ID}}" class="button is-small is-info">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-eye"></i>
|
||||
</span>
|
||||
<span>View</span>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="10" class="has-text-centered has-text-grey">
|
||||
<div class="py-6">
|
||||
<span class="icon is-large">
|
||||
<i class="fas fa-file-alt fa-2x"></i>
|
||||
</span>
|
||||
<p class="mt-2">No documents found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{if .Documents}}
|
||||
<div class="has-text-centered mt-4">
|
||||
<p class="has-text-grey is-size-7">
|
||||
Showing most recent 1000 documents
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
502
go-app/templates/documents/view.html
Normal file
502
go-app/templates/documents/view.html
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
{{define "content"}}
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li><a href="/documents">Documents</a></li>
|
||||
<li class="is-active"><a href="#" aria-current="page">{{.Document.Type}} #{{.Document.ID}}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Include document type specific view -->
|
||||
{{if eq .DocType "quote"}}
|
||||
{{template "document-quote-view" .}}
|
||||
{{else if eq .DocType "invoice"}}
|
||||
{{template "document-invoice-view" .}}
|
||||
{{else if eq .DocType "purchaseOrder"}}
|
||||
{{template "document-purchase-order-view" .}}
|
||||
{{else if eq .DocType "orderAck"}}
|
||||
{{template "document-orderack-view" .}}
|
||||
{{else if eq .DocType "packingList"}}
|
||||
{{template "document-packinglist-view" .}}
|
||||
{{else}}
|
||||
<div class="notification is-warning">
|
||||
Unknown document type: {{.DocType}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Line Items Section -->
|
||||
<div class="box mt-5">
|
||||
<h2 class="title is-4">Line Items</h2>
|
||||
<div id="line-items-table">
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Item #</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Quantity</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Total Price</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="line-items-tbody">
|
||||
{{if .LineItems}}
|
||||
{{range .LineItems}}
|
||||
<tr id="line-item-{{.ID}}">
|
||||
<td>{{.ItemNumber}}</td>
|
||||
<td>{{.Title}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td>{{.Quantity}}</td>
|
||||
<td>
|
||||
{{if .GrossUnitPrice.Valid}}
|
||||
${{.GrossUnitPrice.String}}
|
||||
{{else if .UnitPriceString.Valid}}
|
||||
{{.UnitPriceString.String}}
|
||||
{{else}}
|
||||
-
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .GrossPrice.Valid}}
|
||||
${{.GrossPrice.String}}
|
||||
{{else if .GrossPriceString.Valid}}
|
||||
{{.GrossPriceString.String}}
|
||||
{{else}}
|
||||
-
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<button class="button is-small is-primary" onclick="editLineItem({{.ID}})">
|
||||
<span class="icon">
|
||||
<i class="fas fa-edit"></i>
|
||||
</span>
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
<button class="button is-small is-danger" onclick="deleteLineItem({{.ID}})">
|
||||
<span class="icon">
|
||||
<i class="fas fa-trash"></i>
|
||||
</span>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<tr id="no-line-items">
|
||||
<td colspan="7" class="has-text-centered has-text-grey">
|
||||
No line items yet
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button class="button is-primary" onclick="showAddLineItemForm()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-plus"></i>
|
||||
</span>
|
||||
<span>Add Line Item</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Line Item Modal -->
|
||||
<div class="modal" id="add-line-item-modal">
|
||||
<div class="modal-background" onclick="hideAddLineItemForm()"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Add Line Item</p>
|
||||
<button class="delete" aria-label="close" onclick="hideAddLineItemForm()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form id="add-line-item-form">
|
||||
<input type="hidden" name="document_id" value="{{.Document.ID}}">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Title</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="title" placeholder="Line item title" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="description" placeholder="Line item description" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Quantity</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="quantity" placeholder="1.00" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Unit Price</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="gross_unit_price" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Total Price</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="gross_price" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="option" value="1">
|
||||
Optional item
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-primary" onclick="submitLineItem()">Add Line Item</button>
|
||||
<button class="button" onclick="hideAddLineItemForm()">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Line Item Modal -->
|
||||
<div class="modal" id="edit-line-item-modal">
|
||||
<div class="modal-background" onclick="hideEditLineItemForm()"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Edit Line Item</p>
|
||||
<button class="delete" aria-label="close" onclick="hideEditLineItemForm()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form id="edit-line-item-form">
|
||||
<input type="hidden" name="id" id="edit-line-item-id">
|
||||
<input type="hidden" name="document_id" value="{{.Document.ID}}">
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Item Number</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="item_number" id="edit-item-number" placeholder="1.00" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Title</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="title" id="edit-title" placeholder="Line item title" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" name="description" id="edit-description" placeholder="Line item description" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Quantity</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="quantity" id="edit-quantity" placeholder="1.00" required oninput="calculateEditTotal()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Unit Price</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="gross_unit_price" id="edit-unit-price" placeholder="0.00" oninput="calculateEditTotal()">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="field">
|
||||
<label class="label">Total Price</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="gross_price" id="edit-total-price" placeholder="0.00">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="option" id="edit-option" value="1">
|
||||
Optional item
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-primary" onclick="submitEditLineItem()">Update Line Item</button>
|
||||
<button class="button" onclick="hideEditLineItemForm()">Cancel</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Attachments Section -->
|
||||
<div class="box mt-5">
|
||||
<h2 class="title is-4">Attachments</h2>
|
||||
<div id="attachments-table">
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Size</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="5" class="has-text-centered has-text-grey">
|
||||
No attachments yet
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button class="button is-info" onclick="addAttachment()">
|
||||
<span class="icon">
|
||||
<i class="fas fa-paperclip"></i>
|
||||
</span>
|
||||
<span>Add Attachment</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showAddLineItemForm() {
|
||||
document.getElementById('add-line-item-modal').classList.add('is-active');
|
||||
}
|
||||
|
||||
function hideAddLineItemForm() {
|
||||
document.getElementById('add-line-item-modal').classList.remove('is-active');
|
||||
document.getElementById('add-line-item-form').reset();
|
||||
}
|
||||
|
||||
function hideEditLineItemForm() {
|
||||
document.getElementById('edit-line-item-modal').classList.remove('is-active');
|
||||
document.getElementById('edit-line-item-form').reset();
|
||||
}
|
||||
|
||||
function submitEditLineItem() {
|
||||
const form = document.getElementById('edit-line-item-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch('/line_items/ajax_edit', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'SUCCESS') {
|
||||
hideEditLineItemForm();
|
||||
refreshLineItemsTable();
|
||||
showNotification('Line item updated successfully', 'is-success');
|
||||
} else {
|
||||
showNotification('Failed to update line item', 'is-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error updating line item', 'is-danger');
|
||||
});
|
||||
}
|
||||
|
||||
function submitLineItem() {
|
||||
const form = document.getElementById('add-line-item-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch('/line_items/ajax_add', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'SUCCESS') {
|
||||
hideAddLineItemForm();
|
||||
refreshLineItemsTable();
|
||||
showNotification('Line item added successfully', 'is-success');
|
||||
} else {
|
||||
showNotification('Failed to add line item', 'is-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error adding line item', 'is-danger');
|
||||
});
|
||||
}
|
||||
|
||||
function editLineItem(id) {
|
||||
// Fetch line item data
|
||||
fetch(`/api/v1/line-items/${id}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Populate the edit form
|
||||
document.getElementById('edit-line-item-id').value = data.id;
|
||||
document.getElementById('edit-item-number').value = data.item_number;
|
||||
document.getElementById('edit-title').value = data.title;
|
||||
document.getElementById('edit-description').value = data.description;
|
||||
document.getElementById('edit-quantity').value = data.quantity;
|
||||
|
||||
// Handle price fields - check both numeric and string versions
|
||||
if (data.gross_unit_price && data.gross_unit_price.Valid) {
|
||||
document.getElementById('edit-unit-price').value = data.gross_unit_price.String;
|
||||
} else {
|
||||
document.getElementById('edit-unit-price').value = '';
|
||||
}
|
||||
|
||||
if (data.gross_price && data.gross_price.Valid) {
|
||||
document.getElementById('edit-total-price').value = data.gross_price.String;
|
||||
} else {
|
||||
document.getElementById('edit-total-price').value = '';
|
||||
}
|
||||
|
||||
// Set checkbox
|
||||
document.getElementById('edit-option').checked = data.option;
|
||||
|
||||
// Show the modal
|
||||
document.getElementById('edit-line-item-modal').classList.add('is-active');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching line item:', error);
|
||||
showNotification('Error loading line item data', 'is-danger');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteLineItem(id) {
|
||||
if (confirm('Are you sure you want to delete this line item?')) {
|
||||
fetch(`/line_items/ajax_delete/${id}`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
if (data === 'SUCCESS') {
|
||||
document.getElementById(`line-item-${id}`).remove();
|
||||
|
||||
// Check if there are no more line items
|
||||
const tbody = document.getElementById('line-items-tbody');
|
||||
if (tbody.children.length === 0) {
|
||||
tbody.innerHTML = '<tr id="no-line-items"><td colspan="7" class="has-text-centered has-text-grey">No line items yet</td></tr>';
|
||||
}
|
||||
|
||||
showNotification('Line item deleted successfully', 'is-success');
|
||||
} else {
|
||||
showNotification('Failed to delete line item', 'is-danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Error deleting line item', 'is-danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLineItemsTable() {
|
||||
const documentId = {{.Document.ID}};
|
||||
fetch(`/line_items/get_table/${documentId}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// The response is already the tbody content
|
||||
document.getElementById('line-items-tbody').innerHTML = html;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error refreshing line items:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
// Create notification element
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type} is-light`;
|
||||
notification.innerHTML = `
|
||||
<button class="delete" onclick="this.parentElement.remove()"></button>
|
||||
${message}
|
||||
`;
|
||||
|
||||
// Add to page
|
||||
const container = document.querySelector('.container');
|
||||
container.insertBefore(notification, container.firstChild);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (notification.parentElement) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function addAttachment() {
|
||||
// TODO: Implement attachment modal
|
||||
alert('Add attachment functionality coming soon');
|
||||
}
|
||||
|
||||
function generatePDF() {
|
||||
const documentId = {{.Document.ID}};
|
||||
const currentUrl = window.location.href;
|
||||
|
||||
// Show loading state
|
||||
const buttons = document.querySelectorAll('button');
|
||||
const originalTexts = new Map();
|
||||
buttons.forEach(btn => {
|
||||
if (btn.onclick && btn.onclick.toString().includes('generatePDF')) {
|
||||
originalTexts.set(btn, btn.innerHTML);
|
||||
btn.innerHTML = '<span class="icon"><i class="fas fa-spinner fa-spin"></i></span><span>Generating PDF...</span>';
|
||||
btn.disabled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Call PDF generation endpoint
|
||||
window.location.href = `/documents/pdf/${documentId}`;
|
||||
|
||||
// Reset button state after a delay (in case user stays on page)
|
||||
setTimeout(() => {
|
||||
buttons.forEach(btn => {
|
||||
if (originalTexts.has(btn)) {
|
||||
btn.innerHTML = originalTexts.get(btn);
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function emailDocument() {
|
||||
// TODO: Implement email functionality
|
||||
alert('Email functionality coming soon');
|
||||
}
|
||||
|
||||
function calculateEditTotal() {
|
||||
const quantity = parseFloat(document.getElementById('edit-quantity').value) || 0;
|
||||
const unitPrice = parseFloat(document.getElementById('edit-unit-price').value) || 0;
|
||||
const total = quantity * unitPrice;
|
||||
|
||||
if (total > 0) {
|
||||
document.getElementById('edit-total-price').value = total.toFixed(2);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
|
@ -106,6 +106,51 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="columns">
|
||||
<!-- Documents Card -->
|
||||
<div class="column is-3">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<span class="icon is-large has-text-danger">
|
||||
<i class="fas fa-file-alt fa-2x"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4">Documents</p>
|
||||
<p class="subtitle is-6">Quotes, invoices, and more</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<a href="/documents" class="button is-danger is-fullwidth">
|
||||
View Documents
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder for future modules -->
|
||||
<div class="column is-3">
|
||||
<div class="card has-background-light">
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<span class="icon is-large has-text-grey-light">
|
||||
<i class="fas fa-plus fa-2x"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title is-4 has-text-grey">More Coming</p>
|
||||
<p class="subtitle is-6 has-text-grey">Additional features planned</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity Section -->
|
||||
<div class="box mt-5">
|
||||
<h2 class="title is-4">Recent Activity</h2>
|
||||
|
|
|
|||
125
start-development.sh
Executable file
125
start-development.sh
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
#!/bin/bash
|
||||
|
||||
# CMC Sales Development Environment Setup Script
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}🚀 Starting CMC Sales Development Environment${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
echo -e "${YELLOW}📋 Checking prerequisites...${NC}"
|
||||
|
||||
if ! command_exists docker; then
|
||||
echo -e "${RED}❌ Docker is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command_exists docker-compose; then
|
||||
echo -e "${RED}❌ Docker Compose is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Prerequisites satisfied${NC}"
|
||||
echo ""
|
||||
|
||||
# Check Docker Compose version for watch support
|
||||
DOCKER_COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "unknown")
|
||||
SUPPORTS_WATCH=false
|
||||
|
||||
if [[ "$DOCKER_COMPOSE_VERSION" != "unknown" ]]; then
|
||||
# Check if version is 2.22.0 or higher (when watch was introduced)
|
||||
if docker compose version 2>/dev/null | grep -q "Docker Compose version v2\.\([2-9][2-9]\|[3-9][0-9]\)"; then
|
||||
SUPPORTS_WATCH=true
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}🏗️ Building and starting services...${NC}"
|
||||
|
||||
# Ask user about watch mode
|
||||
if [[ "$SUPPORTS_WATCH" == "true" ]]; then
|
||||
echo -e "${BLUE}💡 Docker Compose watch mode is available for automatic rebuilds on file changes.${NC}"
|
||||
read -p "Would you like to enable watch mode? (Y/n): " enable_watch
|
||||
if [[ ! $enable_watch =~ ^[Nn]$ ]]; then
|
||||
USE_WATCH=true
|
||||
else
|
||||
USE_WATCH=false
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Docker Compose watch mode not available (requires v2.22.0+)${NC}"
|
||||
USE_WATCH=false
|
||||
fi
|
||||
|
||||
if [[ "$USE_WATCH" == "true" ]]; then
|
||||
echo -e "${GREEN}🔄 Starting services with watch mode enabled...${NC}"
|
||||
echo -e "${BLUE} 📁 Watching for changes in: go-app/, app/webroot/css/, app/webroot/js/${NC}"
|
||||
echo -e "${BLUE} 🔄 Services will automatically rebuild on file changes${NC}"
|
||||
echo -e "${BLUE} ⏹️ Press Ctrl+C to stop all services${NC}"
|
||||
echo ""
|
||||
|
||||
# Start with watch mode (this will run in foreground)
|
||||
docker compose up --build --watch
|
||||
else
|
||||
# Traditional mode
|
||||
docker compose up --build -d
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}⏳ Waiting for services to be ready...${NC}"
|
||||
sleep 10
|
||||
|
||||
# Check service health
|
||||
echo -e "${YELLOW}🏥 Checking service health...${NC}"
|
||||
|
||||
# Check database
|
||||
if docker compose exec -T db mysqladmin ping -h localhost -u cmc -p'xVRQI&cA?7AU=hqJ!%au' --silent; then
|
||||
echo -e "${GREEN}✅ Database is ready${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Database is not ready${NC}"
|
||||
fi
|
||||
|
||||
# Check Go app
|
||||
if curl -s http://localhost:8080/api/v1/health > /dev/null; then
|
||||
echo -e "${GREEN}✅ Go application is ready${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Go application might still be starting...${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}🎉 Development environment is running!${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}📱 Access your applications:${NC}"
|
||||
echo -e " 🏛️ CakePHP (Legacy): ${GREEN}http://cmclocal${NC} (add to /etc/hosts: 127.0.0.1 cmclocal)"
|
||||
echo -e " 🚀 Go (Modern): ${GREEN}http://localhost:8080${NC}"
|
||||
echo -e " 🗄️ Database: ${GREEN}localhost:3306${NC} (user: cmc, pass: xVRQI&cA?7AU=hqJ!%au)"
|
||||
echo ""
|
||||
echo -e "${BLUE}🔧 Useful commands:${NC}"
|
||||
echo -e " View logs: ${YELLOW}docker compose logs -f${NC}"
|
||||
echo -e " Stop services: ${YELLOW}docker compose down${NC}"
|
||||
echo -e " Restart Go app: ${YELLOW}docker compose restart cmc-go${NC}"
|
||||
echo -e " Restart PHP app: ${YELLOW}docker compose restart cmc-php${NC}"
|
||||
echo -e " Enable watch mode: ${YELLOW}docker compose up --watch${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}📚 Documentation:${NC}"
|
||||
echo -e " Go app README: ${YELLOW}go-app/README.md${NC}"
|
||||
echo -e " Project guide: ${YELLOW}CLAUDE.md${NC}"
|
||||
echo ""
|
||||
|
||||
# Optional: Follow logs
|
||||
read -p "Would you like to follow the logs? (y/N): " follow_logs
|
||||
if [[ $follow_logs =~ ^[Yy]$ ]]; then
|
||||
echo -e "${YELLOW}📝 Following logs (Ctrl+C to exit)...${NC}"
|
||||
docker compose logs -f
|
||||
fi
|
||||
fi
|
||||
Loading…
Reference in a new issue