Add Go app

Add start-development.sh
This commit is contained in:
Karl Cordes 2025-07-02 22:04:36 +10:00
parent e3442c29cc
commit 4f54a93c62
62 changed files with 8204 additions and 124 deletions

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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
)

View file

@ -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=

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View file

@ -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"`

View 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
}

View file

@ -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)

View 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
}

View 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
}

View 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
}

View 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(&params); 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(&params); 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)
}

View 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(&params); 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)
}

View 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(&params); 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(&params); 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)
}

View 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)
}

View 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(&params); 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(&params); 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)
}

View file

@ -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)
}
}

View 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(&params); 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(&params); 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)
}

View 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
}

View 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
}

View file

@ -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

Binary file not shown.

View 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;

View 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;

View 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 = ?;

View 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;

View 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';

View 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 = ?;

View 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;

View 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 = ?;

View 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 = ?;

View 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 = ?;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View 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}}

View file

@ -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
View 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