{{.Customer.Notes}}
+diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..618cb48e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,122 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Application Overview + +CMC Sales is a B2B sales management system for CMC Technologies. The codebase consists of: + +- **Legacy CakePHP 1.2.5 application** (2008-era) - Primary business logic +- **Modern Go API** (in `/go-app/`) - New development using sqlc and Gorilla Mux + +**Note**: Documentation also references a Django application that is not present in the current codebase. + +## Development Commands + +### Docker Development +```bash +# Start all services (both CakePHP and Go applications) +docker compose up + +# Start specific services +docker compose up cmc-php # CakePHP app only +docker compose up cmc-go # Go app only +docker compose up db # Database only + +# Restore database from backup (get backups via rsync first) +rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups . +gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc + +# Access development sites (add to /etc/hosts: 127.0.0.1 cmclocal) +# http://cmclocal (CakePHP legacy app via nginx) +# http://localhost:8080 (Go modern app direct access) +``` + +### Testing +```bash +# CakePHP has minimal test coverage +# Test files located in /app/tests/ +# Uses SimpleTest framework +``` + +### Go Application Development +```bash +# Navigate to Go app directory +cd go-app + +# Configure private module access (first time only) +go env -w GOPRIVATE=code.springupsoftware.com + +# Install dependencies and sqlc +make install + +# Generate sqlc code from SQL queries +make sqlc + +# Run the Go server locally +make run + +# Run tests +make test + +# Build binary +make build +``` + +## Architecture + +### CakePHP Application (Legacy) +- **Framework**: CakePHP 1.2.5 (MVC pattern) +- **PHP Version**: PHP 5 (Ubuntu Lucid 10.04 container) +- **Location**: `/app/` +- **Key Directories**: + - `app/models/` - ActiveRecord models + - `app/controllers/` - Request handlers + - `app/views/` - Templates (.ctp files) + - `app/config/` - Configuration including database.php + - `app/vendors/` - Third-party libraries (TCPDF for PDFs, PHPExcel) + - `app/webroot/` - Public assets and file uploads + +### Go Application (Modern) +- **Framework**: Gorilla Mux (HTTP router) +- **Database**: sqlc for type-safe SQL queries +- **Location**: `/go-app/` +- **Structure**: + - `cmd/server/` - Main application entry point + - `internal/cmc/handlers/` - HTTP request handlers + - `internal/cmc/db/` - Generated sqlc code + - `sql/queries/` - SQL query definitions + - `sql/schema/` - Database schema +- **API Base Path**: `/api/v1` + +### Database +- **Engine**: MariaDB +- **Host**: `db` (Docker service) or `127.0.0.1` locally +- **Name**: `cmc` +- **User**: `cmc` +- **Password**: `xVRQI&cA?7AU=hqJ!%au` (hardcoded in `app/config/database.php`) + +## Key Business Entities + +- **Customers** - Client companies with ABN validation +- **Purchase Orders** - Orders with suppliers (revision tracking enabled) +- **Quotes** - Customer price quotations +- **Invoices** - Billing documents +- **Products** - Catalog with categories and options +- **Shipments** - Logistics and freight management +- **Jobs** - Project tracking +- **Documents** - PDF generation via TCPDF +- **Emails** - Correspondence tracking + +## File Storage + +- **PDF files**: `/app/webroot/pdf/` (mounted volume) +- **Attachments**: `/app/webroot/attachments_files/` (mounted volume) + +## Critical Considerations + +- **Security**: Running on extremely outdated Ubuntu Lucid (10.04) with PHP 5 +- **Database Changes**: Exercise caution - the database may be shared with other applications +- **CakePHP Conventions**: Uses 2008-era patterns and practices +- **Authentication**: Basic auth configured via `userpasswd` file +- **PDF Generation**: Uses TCPDF library in `/app/vendors/tcpdf/` \ No newline at end of file diff --git a/Dockerfile.go b/Dockerfile.go new file mode 100644 index 00000000..2e59e2ec --- /dev/null +++ b/Dockerfile.go @@ -0,0 +1,50 @@ +# Build stage +FROM golang:1.23-alpine AS builder + +# Install build dependencies +RUN apk add --no-cache git + +# Set working directory +WORKDIR /app + +# Copy go mod files +COPY go-app/go.mod go-app/go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY go-app/ . + +# Install sqlc (compatible with Go 1.23+) +RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + +# Generate sqlc code +RUN sqlc generate + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go + +# Runtime stage +FROM alpine:latest + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Copy the binary from builder +COPY --from=builder /app/server . + +# Copy templates and static files +COPY go-app/templates ./templates +COPY go-app/static ./static + +# Copy .env file if needed +COPY go-app/.env.example .env + +# Expose port +EXPOSE 8080 + +# Run the application +CMD ["./server"] \ No newline at end of file diff --git a/README.md b/README.md index 463b5ce9..b9c72315 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,50 @@ # cmc-sales -## Development Docker compose instructions +## Development Setup + +CMC Sales now runs both legacy CakePHP and modern Go applications side by side. + +### Quick Start ``` shell git clone git@code.springupsoftware.com:cmc/cmc-sales.git cd cmc-sales + +# Easy way - use the setup script +./start-development.sh + +# Manual way rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups . -docker compose up -# DB password is in config/database.php because..reasons. -# TODO move it to an environment var and rotate it.. +docker compose up --build gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc - -# edit your dev machine /etc/hosts and add -127.0.0.1 cmclocal - -# open up cmclocal in your browser.. hopefully it'll work. ``` +### Access Applications + +**Add to /etc/hosts:** +``` +127.0.0.1 cmclocal +``` + +**Application URLs:** +- **CakePHP (Legacy)**: http://cmclocal - Original CakePHP 1.2.5 application +- **Go (Modern)**: http://localhost:8080 - New Go application with HTMX frontend +- **Database**: localhost:3306 (user: `cmc`, password: see `app/config/database.php`) + +### Architecture + +- **cmc-php**: Legacy CakePHP application (nginx proxied) +- **cmc-go**: Modern Go application (direct access on port 8080) +- **db**: Shared MariaDB database +- **nginx**: Reverse proxy for CakePHP app + +Both applications share the same database, allowing for gradual migration. + +### Requirements + +- **Go Application**: Requires Go 1.23+ (for latest sqlc) + - Alternative: Use `Dockerfile.go.legacy` with Go 1.21 and sqlc v1.26.0 + ## Install a new server diff --git a/conf/nginx-site.conf b/conf/nginx-site.conf index 92dce82e..4804ec30 100644 --- a/conf/nginx-site.conf +++ b/conf/nginx-site.conf @@ -3,8 +3,12 @@ server { auth_basic_user_file /etc/nginx/userpasswd; auth_basic "Restricted"; location / { - proxy_pass http://cmc:80; + proxy_pass http://cmc-php:80; proxy_read_timeout 300s; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } listen 0.0.0.0:80; diff --git a/docker-compose.yml b/docker-compose.yml index 45e6115c..e32f4f0d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,21 +3,22 @@ services: image: nginx:latest hostname: nginx ports: - - "80:80" # Expose HTTP traffic + - "80:80" # CakePHP app (cmclocal) volumes: - ./conf/nginx-site.conf:/etc/nginx/conf.d/cmc.conf - # todo setup site config. - ./userpasswd:/etc/nginx/userpasswd:ro + depends_on: + - cmc-php restart: unless-stopped networks: - cmc-network - cmc: + cmc-php: build: context: . dockerfile: Dockerfile platform: linux/amd64 - + container_name: cmc-php depends_on: - db volumes: @@ -25,6 +26,7 @@ services: - ./app/webroot/attachments_files:/var/www/cmc-sales/app/webroot/attachments_files networks: - cmc-network + restart: unless-stopped develop: watch: - action: rebuild @@ -32,32 +34,6 @@ services: ignore: - ./app/webroot - cmc-django: # Renamed service for clarity (optional, but good practice) - build: # Added build section - context: ./cmc-django # Path to the directory containing the Dockerfile - dockerfile: Dockerfile # Name of the Dockerfile - platform: linux/amd64 # Keep platform if needed for compatibility - container_name: cmc-django-web # Optional: Keep or change container name - command: uv run python cmcsales/manage.py runserver 0.0.0.0:8888 # Add command if needed - volumes: - - ./cmc-django:/app # Mount the Django project directory - # Keep other necessary volumes if they exist outside ./cmc-django - # Example: - ./app/webroot/pdf:/app/webroot/pdf # Adjust path if needed - # Example: - ./app/webroot/attachments_files:/app/webroot/attachments_files # Adjust path if needed - ports: # Add ports if the Django app needs to be accessed directly - - "8888:8888" - environment: # Add environment variables needed by Django - DJANGO_SETTINGS_MODULE: cmcsales.settings - DATABASE_HOST: db - DATABASE_PORT: 3306 - DATABASE_NAME: cmc - DATABASE_USER: cmc - DATABASE_PASSWORD: xVRQI&cA?7AU=hqJ!%au - depends_on: - - db - networks: - - cmc-network - db: image: mariadb:latest container_name: cmc-db @@ -73,6 +49,34 @@ services: networks: - cmc-network + cmc-go: + build: + context: . + dockerfile: Dockerfile.go + container_name: cmc-go + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: cmc + DB_PASSWORD: xVRQI&cA?7AU=hqJ!%au + DB_NAME: cmc + PORT: 8080 + depends_on: + db: + condition: service_started + ports: + - "8080:8080" + networks: + - cmc-network + restart: unless-stopped + develop: + watch: + - action: rebuild + path: ./go-app + ignore: + - ./go-app/bin + - ./go-app/.env + volumes: db_data: diff --git a/go-app/.env.example b/go-app/.env.example new file mode 100644 index 00000000..c827b7df --- /dev/null +++ b/go-app/.env.example @@ -0,0 +1,9 @@ +# Database configuration +DB_HOST=localhost +DB_PORT=3306 +DB_USER=cmc +DB_PASSWORD=xVRQI&cA?7AU=hqJ!%au +DB_NAME=cmc + +# Server configuration +PORT=8080 \ No newline at end of file diff --git a/go-app/.gitignore b/go-app/.gitignore new file mode 100644 index 00000000..56c712b8 --- /dev/null +++ b/go-app/.gitignore @@ -0,0 +1,33 @@ +# Binaries +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Go vendor directory +vendor/ + +# Environment files +.env + +# Generated sqlc files (optional - you may want to commit these) +# internal/cmc/db/*.go + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/go-app/Makefile b/go-app/Makefile new file mode 100644 index 00000000..5af701b2 --- /dev/null +++ b/go-app/Makefile @@ -0,0 +1,46 @@ +.PHONY: help +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +.PHONY: install +install: ## Install dependencies + @echo "Setting up private module configuration..." + go env -w GOPRIVATE=code.springupsoftware.com + go mod download + go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest + +.PHONY: sqlc +sqlc: ## Generate Go code from SQL queries + sqlc generate + +.PHONY: build +build: sqlc ## Build the application + go build -o bin/server cmd/server/main.go + +.PHONY: run +run: ## Run the application + go run cmd/server/main.go + +.PHONY: dev +dev: sqlc ## Run the application with hot reload (requires air) + air + +.PHONY: test +test: ## Run tests + go test -v ./... + +.PHONY: clean +clean: ## Clean build artifacts + rm -rf bin/ + rm -rf internal/cmc/db/*.go + +.PHONY: docker-build +docker-build: ## Build Docker image + docker build -t cmc-go:latest -f Dockerfile.go . + +.PHONY: docker-run +docker-run: ## Run application in Docker + docker run --rm -p 8080:8080 --network=host cmc-go:latest \ No newline at end of file diff --git a/go-app/README.md b/go-app/README.md new file mode 100644 index 00000000..14344324 --- /dev/null +++ b/go-app/README.md @@ -0,0 +1,123 @@ +# CMC Sales Go Application + +Modern Go web application for CMC Sales system, designed to work alongside the legacy CakePHP application. + +## Architecture + +- **Backend**: Go with Gorilla Mux for HTTP routing +- **Database**: sqlc for type-safe SQL queries +- **Frontend**: Go templates with HTMX for dynamic interactions +- **Styling**: Bulma CSS framework +- **Database Driver**: MySQL/MariaDB + +## Quick Start + +### Local Development + +1. Configure private module access (required for code.springupsoftware.com): +```bash +go env -w GOPRIVATE=code.springupsoftware.com +# If using SSH keys with git: +git config --global url."git@code.springupsoftware.com:".insteadOf "https://code.springupsoftware.com/" +``` + +2. Install dependencies: +```bash +make install +``` + +3. Copy environment variables: +```bash +cp .env.example .env +``` + +4. Generate sqlc code: +```bash +make sqlc +``` + +5. Run the server: +```bash +make run +``` + +### Docker Development + +From the project root: +```bash +# Standard build (requires Go 1.23+) +docker compose up cmc-go + +# If you encounter Go version issues, use the legacy build: +# Edit docker-compose.yml to use: dockerfile: Dockerfile.go.legacy +``` + +## Web Interface + +The application provides a modern web interface accessible at: +- **Home**: `/` - Dashboard with quick links +- **Customers**: `/customers` - Customer management interface +- **Products**: `/products` - Product catalog management +- **Purchase Orders**: `/purchase-orders` - Order tracking + +### Features + +- **Real-time search** with HTMX +- **Responsive design** with Bulma CSS +- **Form validation** and error handling +- **Pagination** for large datasets +- **CRUD operations** with dynamic updates + +## API Endpoints + +REST API endpoints are available under `/api/v1`: + +### Customers +- `GET /api/v1/customers` - List customers (JSON) +- `GET /api/v1/customers/{id}` - Get customer by ID +- `POST /api/v1/customers` - Create new customer +- `PUT /api/v1/customers/{id}` - Update customer +- `DELETE /api/v1/customers/{id}` - Delete customer +- `GET /api/v1/customers/search?q={query}` - Search customers + +### Products +- `GET /api/v1/products` - List products (JSON) +- `GET /api/v1/products/{id}` - Get product by ID +- `POST /api/v1/products` - Create new product +- `PUT /api/v1/products/{id}` - Update product +- `DELETE /api/v1/products/{id}` - Delete product +- `GET /api/v1/products/search?q={query}` - Search products + +### Purchase Orders +- `GET /api/v1/purchase-orders` - List purchase orders (JSON) +- `GET /api/v1/purchase-orders/{id}` - Get purchase order by ID +- `POST /api/v1/purchase-orders` - Create new purchase order +- `PUT /api/v1/purchase-orders/{id}` - Update purchase order +- `DELETE /api/v1/purchase-orders/{id}` - Delete purchase order +- `GET /api/v1/purchase-orders/search?q={query}` - Search purchase orders + +### Health Check +- `GET /api/v1/health` - Service health check + +## Development + +### Adding New Queries + +1. Add SQL queries to `sql/queries/` +2. Run `make sqlc` to generate Go code +3. Create handler in `internal/cmc/handlers/` +4. Register routes in `cmd/server/main.go` + +### Testing + +```bash +make test +``` + +### Building + +```bash +make build +``` + +The binary will be output to `bin/server` \ No newline at end of file diff --git a/go-app/cmd/server/main.go b/go-app/cmd/server/main.go new file mode 100644 index 00000000..aae03bf7 --- /dev/null +++ b/go-app/cmd/server/main.go @@ -0,0 +1,176 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "net/http" + "os" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/handlers" + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" + _ "github.com/go-sql-driver/mysql" + "github.com/gorilla/mux" + "github.com/joho/godotenv" +) + +func main() { + // Load environment variables + if err := godotenv.Load(); err != nil { + log.Println("No .env file found") + } + + // Database configuration + dbHost := getEnv("DB_HOST", "localhost") + dbPort := getEnv("DB_PORT", "3306") + dbUser := getEnv("DB_USER", "cmc") + dbPass := getEnv("DB_PASSWORD", "xVRQI&cA?7AU=hqJ!%au") + dbName := getEnv("DB_NAME", "cmc") + + // Connect to database + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", dbUser, dbPass, dbHost, dbPort, dbName) + database, err := sql.Open("mysql", dsn) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + defer database.Close() + + // Test database connection + if err := database.Ping(); err != nil { + log.Fatal("Failed to ping database:", err) + } + + log.Println("Connected to database successfully") + + // Create queries instance + queries := db.New(database) + + // Initialize template manager + tmpl, err := templates.NewTemplateManager("templates") + if err != nil { + log.Fatal("Failed to initialize templates:", err) + } + + // Create handlers + customerHandler := handlers.NewCustomerHandler(queries) + productHandler := handlers.NewProductHandler(queries) + purchaseOrderHandler := handlers.NewPurchaseOrderHandler(queries) + enquiryHandler := handlers.NewEnquiryHandler(queries) + pageHandler := handlers.NewPageHandler(queries, tmpl) + + // Setup routes + r := mux.NewRouter() + + // Static files + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + // API routes + api := r.PathPrefix("/api/v1").Subrouter() + + // Customer routes + api.HandleFunc("/customers", customerHandler.List).Methods("GET") + api.HandleFunc("/customers", customerHandler.Create).Methods("POST") + api.HandleFunc("/customers/{id}", customerHandler.Get).Methods("GET") + api.HandleFunc("/customers/{id}", customerHandler.Update).Methods("PUT") + api.HandleFunc("/customers/{id}", customerHandler.Delete).Methods("DELETE") + api.HandleFunc("/customers/search", customerHandler.Search).Methods("GET") + + // Product routes + api.HandleFunc("/products", productHandler.List).Methods("GET") + api.HandleFunc("/products", productHandler.Create).Methods("POST") + api.HandleFunc("/products/{id}", productHandler.Get).Methods("GET") + api.HandleFunc("/products/{id}", productHandler.Update).Methods("PUT") + api.HandleFunc("/products/{id}", productHandler.Delete).Methods("DELETE") + api.HandleFunc("/products/search", productHandler.Search).Methods("GET") + + // Purchase Order routes + api.HandleFunc("/purchase-orders", purchaseOrderHandler.List).Methods("GET") + api.HandleFunc("/purchase-orders", purchaseOrderHandler.Create).Methods("POST") + api.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Get).Methods("GET") + api.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Update).Methods("PUT") + api.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Delete).Methods("DELETE") + api.HandleFunc("/purchase-orders/search", purchaseOrderHandler.Search).Methods("GET") + + // Enquiry routes + api.HandleFunc("/enquiries", enquiryHandler.List).Methods("GET") + api.HandleFunc("/enquiries", enquiryHandler.Create).Methods("POST") + api.HandleFunc("/enquiries/{id}", enquiryHandler.Get).Methods("GET") + api.HandleFunc("/enquiries/{id}", enquiryHandler.Update).Methods("PUT") + api.HandleFunc("/enquiries/{id}", enquiryHandler.Delete).Methods("DELETE") + api.HandleFunc("/enquiries/{id}/undelete", enquiryHandler.Undelete).Methods("PUT") + api.HandleFunc("/enquiries/{id}/status", enquiryHandler.UpdateStatus).Methods("PUT") + api.HandleFunc("/enquiries/{id}/mark-submitted", enquiryHandler.MarkSubmitted).Methods("PUT") + api.HandleFunc("/enquiries/search", enquiryHandler.Search).Methods("GET") + + // Health check + api.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + }).Methods("GET") + + // 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") + r.HandleFunc("/enquiries/search", pageHandler.EnquiriesSearch).Methods("GET") + r.HandleFunc("/enquiries/{id}", pageHandler.EnquiriesShow).Methods("GET") + r.HandleFunc("/enquiries/{id}/edit", pageHandler.EnquiriesEdit).Methods("GET") + + // 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") + r.HandleFunc("/enquiries/{id}/undelete", enquiryHandler.Undelete).Methods("PUT") + r.HandleFunc("/enquiries/{id}/status", enquiryHandler.UpdateStatus).Methods("PUT") + r.HandleFunc("/enquiries/{id}/mark-submitted", enquiryHandler.MarkSubmitted).Methods("PUT") + + // Start server + port := getEnv("PORT", "8080") + log.Printf("Starting server on port %s", port) + if err := http.ListenAndServe(":"+port, r); err != nil { + log.Fatal("Failed to start server:", err) + } +} + +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} \ No newline at end of file diff --git a/go-app/go.mod b/go-app/go.mod new file mode 100644 index 00000000..61cb205d --- /dev/null +++ b/go-app/go.mod @@ -0,0 +1,9 @@ +module code.springupsoftware.com/cmc/cmc-sales + +go 1.23 + +require ( + github.com/go-sql-driver/mysql v1.7.1 + github.com/gorilla/mux v1.8.1 + github.com/joho/godotenv v1.5.1 +) diff --git a/go-app/go.sum b/go-app/go.sum new file mode 100644 index 00000000..67ed1c14 --- /dev/null +++ b/go-app/go.sum @@ -0,0 +1,6 @@ +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= diff --git a/go-app/internal/cmc/db/customers.sql.go b/go-app/internal/cmc/db/customers.sql.go new file mode 100644 index 00000000..75157547 --- /dev/null +++ b/go-app/internal/cmc/db/customers.sql.go @@ -0,0 +1,250 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: customers.sql + +package db + +import ( + "context" + "database/sql" +) + +const createCustomer = `-- name: CreateCustomer :execresult +INSERT INTO customers ( + name, trading_name, abn, created, notes, + discount_pricing_policies, payment_terms, + customer_category_id, url, country_id +) VALUES ( + ?, ?, ?, NOW(), ?, ?, ?, ?, ?, ? +) +` + +type CreateCustomerParams struct { + Name string `json:"name"` + TradingName string `json:"trading_name"` + Abn sql.NullString `json:"abn"` + Notes string `json:"notes"` + DiscountPricingPolicies string `json:"discount_pricing_policies"` + PaymentTerms string `json:"payment_terms"` + CustomerCategoryID int32 `json:"customer_category_id"` + Url string `json:"url"` + CountryID int32 `json:"country_id"` +} + +func (q *Queries) CreateCustomer(ctx context.Context, arg CreateCustomerParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createCustomer, + arg.Name, + arg.TradingName, + arg.Abn, + arg.Notes, + arg.DiscountPricingPolicies, + arg.PaymentTerms, + arg.CustomerCategoryID, + arg.Url, + arg.CountryID, + ) +} + +const deleteCustomer = `-- name: DeleteCustomer :exec +DELETE FROM customers +WHERE id = ? +` + +func (q *Queries) DeleteCustomer(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deleteCustomer, id) + return err +} + +const getCustomer = `-- name: GetCustomer :one +SELECT id, name, trading_name, abn, created, notes, discount_pricing_policies, payment_terms, customer_category_id, url, country_id FROM customers +WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetCustomer(ctx context.Context, id int32) (Customer, error) { + row := q.db.QueryRowContext(ctx, getCustomer, id) + var i Customer + err := row.Scan( + &i.ID, + &i.Name, + &i.TradingName, + &i.Abn, + &i.Created, + &i.Notes, + &i.DiscountPricingPolicies, + &i.PaymentTerms, + &i.CustomerCategoryID, + &i.Url, + &i.CountryID, + ) + return i, err +} + +const getCustomerByABN = `-- name: GetCustomerByABN :one +SELECT id, name, trading_name, abn, created, notes, discount_pricing_policies, payment_terms, customer_category_id, url, country_id FROM customers +WHERE abn = ? +LIMIT 1 +` + +func (q *Queries) GetCustomerByABN(ctx context.Context, abn sql.NullString) (Customer, error) { + row := q.db.QueryRowContext(ctx, getCustomerByABN, abn) + var i Customer + err := row.Scan( + &i.ID, + &i.Name, + &i.TradingName, + &i.Abn, + &i.Created, + &i.Notes, + &i.DiscountPricingPolicies, + &i.PaymentTerms, + &i.CustomerCategoryID, + &i.Url, + &i.CountryID, + ) + return i, err +} + +const listCustomers = `-- name: ListCustomers :many +SELECT id, name, trading_name, abn, created, notes, discount_pricing_policies, payment_terms, customer_category_id, url, country_id FROM customers +ORDER BY name +LIMIT ? OFFSET ? +` + +type ListCustomersParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListCustomers(ctx context.Context, arg ListCustomersParams) ([]Customer, error) { + rows, err := q.db.QueryContext(ctx, listCustomers, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Customer{} + for rows.Next() { + var i Customer + if err := rows.Scan( + &i.ID, + &i.Name, + &i.TradingName, + &i.Abn, + &i.Created, + &i.Notes, + &i.DiscountPricingPolicies, + &i.PaymentTerms, + &i.CustomerCategoryID, + &i.Url, + &i.CountryID, + ); 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 searchCustomersByName = `-- name: SearchCustomersByName :many +SELECT id, name, trading_name, abn, created, notes, discount_pricing_policies, payment_terms, customer_category_id, url, country_id FROM customers +WHERE name LIKE CONCAT('%', ?, '%') + OR trading_name LIKE CONCAT('%', ?, '%') +ORDER BY name +LIMIT ? OFFSET ? +` + +type SearchCustomersByNameParams struct { + CONCAT interface{} `json:"CONCAT"` + CONCAT_2 interface{} `json:"CONCAT_2"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) SearchCustomersByName(ctx context.Context, arg SearchCustomersByNameParams) ([]Customer, error) { + rows, err := q.db.QueryContext(ctx, searchCustomersByName, + arg.CONCAT, + arg.CONCAT_2, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Customer{} + for rows.Next() { + var i Customer + if err := rows.Scan( + &i.ID, + &i.Name, + &i.TradingName, + &i.Abn, + &i.Created, + &i.Notes, + &i.DiscountPricingPolicies, + &i.PaymentTerms, + &i.CustomerCategoryID, + &i.Url, + &i.CountryID, + ); 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 updateCustomer = `-- name: UpdateCustomer :exec +UPDATE customers +SET name = ?, + trading_name = ?, + abn = ?, + notes = ?, + discount_pricing_policies = ?, + payment_terms = ?, + customer_category_id = ?, + url = ?, + country_id = ? +WHERE id = ? +` + +type UpdateCustomerParams struct { + Name string `json:"name"` + TradingName string `json:"trading_name"` + Abn sql.NullString `json:"abn"` + Notes string `json:"notes"` + DiscountPricingPolicies string `json:"discount_pricing_policies"` + PaymentTerms string `json:"payment_terms"` + CustomerCategoryID int32 `json:"customer_category_id"` + Url string `json:"url"` + CountryID int32 `json:"country_id"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateCustomer(ctx context.Context, arg UpdateCustomerParams) error { + _, err := q.db.ExecContext(ctx, updateCustomer, + arg.Name, + arg.TradingName, + arg.Abn, + arg.Notes, + arg.DiscountPricingPolicies, + arg.PaymentTerms, + arg.CustomerCategoryID, + arg.Url, + arg.CountryID, + arg.ID, + ) + return err +} diff --git a/go-app/internal/cmc/db/db.go b/go-app/internal/cmc/db/db.go new file mode 100644 index 00000000..0c56c2b4 --- /dev/null +++ b/go-app/internal/cmc/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/go-app/internal/cmc/db/enquiries.sql.go b/go-app/internal/cmc/db/enquiries.sql.go new file mode 100644 index 00000000..8d94d696 --- /dev/null +++ b/go-app/internal/cmc/db/enquiries.sql.go @@ -0,0 +1,1059 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: enquiries.sql + +package db + +import ( + "context" + "database/sql" + "time" +) + +const archiveEnquiry = `-- name: ArchiveEnquiry :exec +UPDATE enquiries SET archived = 1 WHERE id = ? +` + +func (q *Queries) ArchiveEnquiry(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, archiveEnquiry, id) + return err +} + +const countEnquiries = `-- name: CountEnquiries :one +SELECT COUNT(*) FROM enquiries WHERE archived = 0 +` + +func (q *Queries) CountEnquiries(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, countEnquiries) + var count int64 + err := row.Scan(&count) + return count, err +} + +const countEnquiriesByPrinciple = `-- name: CountEnquiriesByPrinciple :one +SELECT COUNT(*) FROM enquiries WHERE principle_code = ? +` + +func (q *Queries) CountEnquiriesByPrinciple(ctx context.Context, principleCode int32) (int64, error) { + row := q.db.QueryRowContext(ctx, countEnquiriesByPrinciple, principleCode) + var count int64 + err := row.Scan(&count) + return count, err +} + +const countEnquiriesByPrincipleAndState = `-- name: CountEnquiriesByPrincipleAndState :one +SELECT COUNT(*) FROM enquiries WHERE principle_code = ? AND state_id = ? +` + +type CountEnquiriesByPrincipleAndStateParams struct { + PrincipleCode int32 `json:"principle_code"` + StateID int32 `json:"state_id"` +} + +func (q *Queries) CountEnquiriesByPrincipleAndState(ctx context.Context, arg CountEnquiriesByPrincipleAndStateParams) (int64, error) { + row := q.db.QueryRowContext(ctx, countEnquiriesByPrincipleAndState, arg.PrincipleCode, arg.StateID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const countEnquiriesByStatus = `-- name: CountEnquiriesByStatus :one +SELECT COUNT(*) FROM enquiries WHERE status_id = ? AND archived = 0 +` + +func (q *Queries) CountEnquiriesByStatus(ctx context.Context, statusID int32) (int64, error) { + row := q.db.QueryRowContext(ctx, countEnquiriesByStatus, statusID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const createEnquiry = `-- name: CreateEnquiry :execresult +INSERT INTO enquiries ( + created, title, user_id, customer_id, contact_id, contact_user_id, + state_id, country_id, principle_id, status_id, comments, + principle_code, gst, billing_address_id, shipping_address_id, + posted, archived +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +) +` + +type CreateEnquiryParams struct { + Created time.Time `json:"created"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + Archived int8 `json:"archived"` +} + +func (q *Queries) CreateEnquiry(ctx context.Context, arg CreateEnquiryParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createEnquiry, + arg.Created, + arg.Title, + arg.UserID, + arg.CustomerID, + arg.ContactID, + arg.ContactUserID, + arg.StateID, + arg.CountryID, + arg.PrincipleID, + arg.StatusID, + arg.Comments, + arg.PrincipleCode, + arg.Gst, + arg.BillingAddressID, + arg.ShippingAddressID, + arg.Posted, + arg.Archived, + ) +} + +const getAllCountries = `-- name: GetAllCountries :many +SELECT id, name FROM countries ORDER BY name +` + +func (q *Queries) GetAllCountries(ctx context.Context) ([]Country, error) { + rows, err := q.db.QueryContext(ctx, getAllCountries) + 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 getAllPrinciples = `-- name: GetAllPrinciples :many +SELECT id, name, short_name, code FROM principles ORDER BY name +` + +func (q *Queries) GetAllPrinciples(ctx context.Context) ([]Principle, error) { + rows, err := q.db.QueryContext(ctx, getAllPrinciples) + 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 getAllStates = `-- name: GetAllStates :many +SELECT id, name, shortform, enqform FROM states ORDER BY name +` + +func (q *Queries) GetAllStates(ctx context.Context) ([]State, error) { + rows, err := q.db.QueryContext(ctx, getAllStates) + 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 getAllStatuses = `-- name: GetAllStatuses :many +SELECT id, name FROM statuses ORDER BY name +` + +type GetAllStatusesRow struct { + ID int32 `json:"id"` + Name string `json:"name"` +} + +func (q *Queries) GetAllStatuses(ctx context.Context) ([]GetAllStatusesRow, error) { + rows, err := q.db.QueryContext(ctx, getAllStatuses) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetAllStatusesRow{} + for rows.Next() { + var i GetAllStatusesRow + 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 getEnquiriesByCustomer = `-- name: GetEnquiriesByCustomer :many +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +WHERE e.customer_id = ? AND e.archived = 0 +ORDER BY e.id DESC +LIMIT ? OFFSET ? +` + +type GetEnquiriesByCustomerParams struct { + CustomerID int32 `json:"customer_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetEnquiriesByCustomerRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` +} + +func (q *Queries) GetEnquiriesByCustomer(ctx context.Context, arg GetEnquiriesByCustomerParams) ([]GetEnquiriesByCustomerRow, error) { + rows, err := q.db.QueryContext(ctx, getEnquiriesByCustomer, arg.CustomerID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetEnquiriesByCustomerRow{} + for rows.Next() { + var i GetEnquiriesByCustomerRow + if err := rows.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + ); 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 getEnquiriesByUser = `-- name: GetEnquiriesByUser :many +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +WHERE e.user_id = ? AND e.archived = 0 +ORDER BY e.id DESC +LIMIT ? OFFSET ? +` + +type GetEnquiriesByUserParams struct { + UserID int32 `json:"user_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetEnquiriesByUserRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` +} + +func (q *Queries) GetEnquiriesByUser(ctx context.Context, arg GetEnquiriesByUserParams) ([]GetEnquiriesByUserRow, error) { + rows, err := q.db.QueryContext(ctx, getEnquiriesByUser, arg.UserID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetEnquiriesByUserRow{} + for rows.Next() { + var i GetEnquiriesByUserRow + if err := rows.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + ); 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 getEnquiry = `-- name: GetEnquiry :one +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + contact.email as contact_email, + contact.mobile as contact_mobile, + contact.direct_phone as contact_direct_phone, + contact.phone as contact_phone, + contact.phone_extension as contact_phone_extension, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name, + st.name as state_name, + st.shortform as state_shortform, + co.name as country_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +LEFT JOIN states st ON e.state_id = st.id +LEFT JOIN countries co ON e.country_id = co.id +WHERE e.id = ? +` + +type GetEnquiryRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + ContactEmail sql.NullString `json:"contact_email"` + ContactMobile sql.NullString `json:"contact_mobile"` + ContactDirectPhone sql.NullString `json:"contact_direct_phone"` + ContactPhone sql.NullString `json:"contact_phone"` + ContactPhoneExtension sql.NullString `json:"contact_phone_extension"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` + StateName sql.NullString `json:"state_name"` + StateShortform sql.NullString `json:"state_shortform"` + CountryName sql.NullString `json:"country_name"` +} + +func (q *Queries) GetEnquiry(ctx context.Context, id int32) (GetEnquiryRow, error) { + row := q.db.QueryRowContext(ctx, getEnquiry, id) + var i GetEnquiryRow + err := row.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.ContactEmail, + &i.ContactMobile, + &i.ContactDirectPhone, + &i.ContactPhone, + &i.ContactPhoneExtension, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + &i.StateName, + &i.StateShortform, + &i.CountryName, + ) + return i, err +} + +const listArchivedEnquiries = `-- name: ListArchivedEnquiries :many +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +WHERE e.archived = 1 +ORDER BY e.id DESC +LIMIT ? OFFSET ? +` + +type ListArchivedEnquiriesParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type ListArchivedEnquiriesRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` +} + +func (q *Queries) ListArchivedEnquiries(ctx context.Context, arg ListArchivedEnquiriesParams) ([]ListArchivedEnquiriesRow, error) { + rows, err := q.db.QueryContext(ctx, listArchivedEnquiries, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListArchivedEnquiriesRow{} + for rows.Next() { + var i ListArchivedEnquiriesRow + if err := rows.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + ); 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 listEnquiries = `-- name: ListEnquiries :many +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + contact.email as contact_email, + contact.mobile as contact_mobile, + contact.direct_phone as contact_direct_phone, + contact.phone as contact_phone, + contact.phone_extension as contact_phone_extension, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +WHERE e.archived = 0 +ORDER BY e.id DESC +LIMIT ? OFFSET ? +` + +type ListEnquiriesParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type ListEnquiriesRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + ContactEmail sql.NullString `json:"contact_email"` + ContactMobile sql.NullString `json:"contact_mobile"` + ContactDirectPhone sql.NullString `json:"contact_direct_phone"` + ContactPhone sql.NullString `json:"contact_phone"` + ContactPhoneExtension sql.NullString `json:"contact_phone_extension"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` +} + +func (q *Queries) ListEnquiries(ctx context.Context, arg ListEnquiriesParams) ([]ListEnquiriesRow, error) { + rows, err := q.db.QueryContext(ctx, listEnquiries, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListEnquiriesRow{} + for rows.Next() { + var i ListEnquiriesRow + if err := rows.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.ContactEmail, + &i.ContactMobile, + &i.ContactDirectPhone, + &i.ContactPhone, + &i.ContactPhoneExtension, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + ); 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 markEnquirySubmitted = `-- name: MarkEnquirySubmitted :exec +UPDATE enquiries SET submitted = ? WHERE id = ? +` + +type MarkEnquirySubmittedParams struct { + Submitted sql.NullTime `json:"submitted"` + ID int32 `json:"id"` +} + +func (q *Queries) MarkEnquirySubmitted(ctx context.Context, arg MarkEnquirySubmittedParams) error { + _, err := q.db.ExecContext(ctx, markEnquirySubmitted, arg.Submitted, arg.ID) + return err +} + +const searchEnquiries = `-- name: SearchEnquiries :many +SELECT e.id, e.created, e.submitted, e.title, e.user_id, e.customer_id, e.contact_id, e.contact_user_id, e.state_id, e.country_id, e.principle_id, e.status_id, e.comments, e.principle_code, e.gst, e.billing_address_id, e.shipping_address_id, e.posted, e.email_count, e.invoice_count, e.job_count, e.quote_count, e.archived, + u.first_name as user_first_name, + u.last_name as user_last_name, + c.name as customer_name, + contact.first_name as contact_first_name, + contact.last_name as contact_last_name, + s.name as status_name, + p.name as principle_name, + p.short_name as principle_short_name +FROM enquiries e +LEFT JOIN users u ON e.user_id = u.id +LEFT JOIN customers c ON e.customer_id = c.id +LEFT JOIN users contact ON e.contact_user_id = contact.id +LEFT JOIN statuses s ON e.status_id = s.id +LEFT JOIN principles p ON e.principle_id = p.id +WHERE e.archived = 0 +AND ( + e.title LIKE CONCAT('%', ?, '%') OR + c.name LIKE CONCAT('%', ?, '%') OR + CONCAT(contact.first_name, ' ', contact.last_name) LIKE CONCAT('%', ?, '%') +) +ORDER BY e.id DESC +LIMIT ? OFFSET ? +` + +type SearchEnquiriesParams struct { + CONCAT interface{} `json:"CONCAT"` + CONCAT_2 interface{} `json:"CONCAT_2"` + CONCAT_3 interface{} `json:"CONCAT_3"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type SearchEnquiriesRow struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` + UserFirstName sql.NullString `json:"user_first_name"` + UserLastName sql.NullString `json:"user_last_name"` + CustomerName sql.NullString `json:"customer_name"` + ContactFirstName sql.NullString `json:"contact_first_name"` + ContactLastName sql.NullString `json:"contact_last_name"` + StatusName sql.NullString `json:"status_name"` + PrincipleName sql.NullString `json:"principle_name"` + PrincipleShortName sql.NullString `json:"principle_short_name"` +} + +func (q *Queries) SearchEnquiries(ctx context.Context, arg SearchEnquiriesParams) ([]SearchEnquiriesRow, error) { + rows, err := q.db.QueryContext(ctx, searchEnquiries, + arg.CONCAT, + arg.CONCAT_2, + arg.CONCAT_3, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchEnquiriesRow{} + for rows.Next() { + var i SearchEnquiriesRow + if err := rows.Scan( + &i.ID, + &i.Created, + &i.Submitted, + &i.Title, + &i.UserID, + &i.CustomerID, + &i.ContactID, + &i.ContactUserID, + &i.StateID, + &i.CountryID, + &i.PrincipleID, + &i.StatusID, + &i.Comments, + &i.PrincipleCode, + &i.Gst, + &i.BillingAddressID, + &i.ShippingAddressID, + &i.Posted, + &i.EmailCount, + &i.InvoiceCount, + &i.JobCount, + &i.QuoteCount, + &i.Archived, + &i.UserFirstName, + &i.UserLastName, + &i.CustomerName, + &i.ContactFirstName, + &i.ContactLastName, + &i.StatusName, + &i.PrincipleName, + &i.PrincipleShortName, + ); 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 unarchiveEnquiry = `-- name: UnarchiveEnquiry :exec +UPDATE enquiries SET archived = 0 WHERE id = ? +` + +func (q *Queries) UnarchiveEnquiry(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, unarchiveEnquiry, id) + return err +} + +const updateEnquiry = `-- name: UpdateEnquiry :exec +UPDATE enquiries +SET title = ?, user_id = ?, customer_id = ?, contact_id = ?, contact_user_id = ?, + state_id = ?, country_id = ?, principle_id = ?, status_id = ?, comments = ?, + principle_code = ?, gst = ?, billing_address_id = ?, shipping_address_id = ?, + posted = ?, submitted = ? +WHERE id = ? +` + +type UpdateEnquiryParams struct { + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + PrincipleCode int32 `json:"principle_code"` + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + Posted bool `json:"posted"` + Submitted sql.NullTime `json:"submitted"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateEnquiry(ctx context.Context, arg UpdateEnquiryParams) error { + _, err := q.db.ExecContext(ctx, updateEnquiry, + arg.Title, + arg.UserID, + arg.CustomerID, + arg.ContactID, + arg.ContactUserID, + arg.StateID, + arg.CountryID, + arg.PrincipleID, + arg.StatusID, + arg.Comments, + arg.PrincipleCode, + arg.Gst, + arg.BillingAddressID, + arg.ShippingAddressID, + arg.Posted, + arg.Submitted, + arg.ID, + ) + return err +} + +const updateEnquiryStatus = `-- name: UpdateEnquiryStatus :exec +UPDATE enquiries SET status_id = ? WHERE id = ? +` + +type UpdateEnquiryStatusParams struct { + StatusID int32 `json:"status_id"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateEnquiryStatus(ctx context.Context, arg UpdateEnquiryStatusParams) error { + _, err := q.db.ExecContext(ctx, updateEnquiryStatus, arg.StatusID, arg.ID) + return err +} diff --git a/go-app/internal/cmc/db/models.go b/go-app/internal/cmc/db/models.go new file mode 100644 index 00000000..e16a4b6f --- /dev/null +++ b/go-app/internal/cmc/db/models.go @@ -0,0 +1,237 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +import ( + "database/sql" + "database/sql/driver" + "fmt" + "time" +) + +type UsersAccessLevel string + +const ( + UsersAccessLevelAdmin UsersAccessLevel = "admin" + UsersAccessLevelManager UsersAccessLevel = "manager" + UsersAccessLevelUser UsersAccessLevel = "user" + UsersAccessLevelNone UsersAccessLevel = "none" +) + +func (e *UsersAccessLevel) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UsersAccessLevel(s) + case string: + *e = UsersAccessLevel(s) + default: + return fmt.Errorf("unsupported scan type for UsersAccessLevel: %T", src) + } + return nil +} + +type NullUsersAccessLevel struct { + UsersAccessLevel UsersAccessLevel `json:"users_access_level"` + Valid bool `json:"valid"` // Valid is true if UsersAccessLevel is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullUsersAccessLevel) Scan(value interface{}) error { + if value == nil { + ns.UsersAccessLevel, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.UsersAccessLevel.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullUsersAccessLevel) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.UsersAccessLevel), nil +} + +type UsersType string + +const ( + UsersTypePrinciple UsersType = "principle" + UsersTypeContact UsersType = "contact" + UsersTypeUser UsersType = "user" +) + +func (e *UsersType) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = UsersType(s) + case string: + *e = UsersType(s) + default: + return fmt.Errorf("unsupported scan type for UsersType: %T", src) + } + return nil +} + +type NullUsersType struct { + UsersType UsersType `json:"users_type"` + Valid bool `json:"valid"` // Valid is true if UsersType is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullUsersType) Scan(value interface{}) error { + if value == nil { + ns.UsersType, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.UsersType.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullUsersType) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.UsersType), nil +} + +type Country struct { + ID int32 `json:"id"` + Name string `json:"name"` +} + +type Customer struct { + ID int32 `json:"id"` + // Company Name + Name string `json:"name"` + TradingName string `json:"trading_name"` + Abn sql.NullString `json:"abn"` + Created time.Time `json:"created"` + Notes string `json:"notes"` + DiscountPricingPolicies string `json:"discount_pricing_policies"` + PaymentTerms string `json:"payment_terms"` + CustomerCategoryID int32 `json:"customer_category_id"` + Url string `json:"url"` + CountryID int32 `json:"country_id"` +} + +type Enquiry struct { + ID int32 `json:"id"` + Created time.Time `json:"created"` + Submitted sql.NullTime `json:"submitted"` + // enquirynumber + Title string `json:"title"` + UserID int32 `json:"user_id"` + CustomerID int32 `json:"customer_id"` + ContactID int32 `json:"contact_id"` + ContactUserID int32 `json:"contact_user_id"` + StateID int32 `json:"state_id"` + CountryID int32 `json:"country_id"` + PrincipleID int32 `json:"principle_id"` + StatusID int32 `json:"status_id"` + Comments string `json:"comments"` + // Numeric Principle Code + PrincipleCode int32 `json:"principle_code"` + // GST applicable on this enquiry + Gst bool `json:"gst"` + BillingAddressID sql.NullInt32 `json:"billing_address_id"` + ShippingAddressID sql.NullInt32 `json:"shipping_address_id"` + // has the enquired been posted + Posted bool `json:"posted"` + EmailCount int32 `json:"email_count"` + InvoiceCount int32 `json:"invoice_count"` + JobCount int32 `json:"job_count"` + QuoteCount int32 `json:"quote_count"` + Archived int8 `json:"archived"` +} + +type Principle struct { + ID int32 `json:"id"` + Name string `json:"name"` + ShortName sql.NullString `json:"short_name"` + Code int32 `json:"code"` +} + +type Product struct { + ID int32 `json:"id"` + // Principle FK + PrincipleID int32 `json:"principle_id"` + ProductCategoryID int32 `json:"product_category_id"` + // This must match the Title in the Excel Costing File + Title string `json:"title"` + Description string `json:"description"` + // Part or model number principle uses to identify this product + ModelNumber sql.NullString `json:"model_number"` + // %1% - first item, %2% , second item etc + ModelNumberFormat sql.NullString `json:"model_number_format"` + // Any notes about this product. Note displayed on quotes + Notes sql.NullString `json:"notes"` + // Stock or Ident + Stock bool `json:"stock"` + ItemCode string `json:"item_code"` + ItemDescription string `json:"item_description"` +} + +type PurchaseOrder struct { + ID int32 `json:"id"` + IssueDate time.Time `json:"issue_date"` + DispatchDate time.Time `json:"dispatch_date"` + DateArrived time.Time `json:"date_arrived"` + // CMC PONumber + Title string `json:"title"` + PrincipleID int32 `json:"principle_id"` + PrincipleReference string `json:"principle_reference"` + DocumentID int32 `json:"document_id"` + CurrencyID sql.NullInt32 `json:"currency_id"` + OrderedFrom string `json:"ordered_from"` + Description string `json:"description"` + DispatchBy string `json:"dispatch_by"` + DeliverTo string `json:"deliver_to"` + ShippingInstructions string `json:"shipping_instructions"` + JobsText string `json:"jobs_text"` + FreightForwarderText string `json:"freight_forwarder_text"` + ParentPurchaseOrderID int32 `json:"parent_purchase_order_id"` +} + +type State struct { + ID int32 `json:"id"` + Name string `json:"name"` + Shortform sql.NullString `json:"shortform"` + Enqform sql.NullString `json:"enqform"` +} + +type Status struct { + ID int32 `json:"id"` + Name string `json:"name"` + Color sql.NullString `json:"color"` +} + +type User struct { + ID int32 `json:"id"` + PrincipleID int32 `json:"principle_id"` + CustomerID int32 `json:"customer_id"` + Type UsersType `json:"type"` + AccessLevel UsersAccessLevel `json:"access_level"` + Username string `json:"username"` + Password string `json:"password"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + JobTitle string `json:"job_title"` + Phone string `json:"phone"` + Mobile string `json:"mobile"` + Fax string `json:"fax"` + PhoneExtension string `json:"phone_extension"` + DirectPhone string `json:"direct_phone"` + Notes string `json:"notes"` + // Added by Vault. May or may not be a real person. + ByVault bool `json:"by_vault"` + // Disregard emails from this address in future. + Blacklisted bool `json:"blacklisted"` + Enabled bool `json:"enabled"` + Archived sql.NullBool `json:"archived"` + PrimaryContact bool `json:"primary_contact"` +} diff --git a/go-app/internal/cmc/db/products.sql.go b/go-app/internal/cmc/db/products.sql.go new file mode 100644 index 00000000..e16c36a5 --- /dev/null +++ b/go-app/internal/cmc/db/products.sql.go @@ -0,0 +1,296 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: products.sql + +package db + +import ( + "context" + "database/sql" +) + +const createProduct = `-- name: CreateProduct :execresult +INSERT INTO products ( + principle_id, product_category_id, title, description, + model_number, model_number_format, notes, stock, + item_code, item_description +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +) +` + +type CreateProductParams struct { + PrincipleID int32 `json:"principle_id"` + ProductCategoryID int32 `json:"product_category_id"` + Title string `json:"title"` + Description string `json:"description"` + ModelNumber sql.NullString `json:"model_number"` + ModelNumberFormat sql.NullString `json:"model_number_format"` + Notes sql.NullString `json:"notes"` + Stock bool `json:"stock"` + ItemCode string `json:"item_code"` + ItemDescription string `json:"item_description"` +} + +func (q *Queries) CreateProduct(ctx context.Context, arg CreateProductParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createProduct, + arg.PrincipleID, + arg.ProductCategoryID, + arg.Title, + arg.Description, + arg.ModelNumber, + arg.ModelNumberFormat, + arg.Notes, + arg.Stock, + arg.ItemCode, + arg.ItemDescription, + ) +} + +const deleteProduct = `-- name: DeleteProduct :exec +DELETE FROM products +WHERE id = ? +` + +func (q *Queries) DeleteProduct(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deleteProduct, id) + return err +} + +const getProduct = `-- name: GetProduct :one +SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products +WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetProduct(ctx context.Context, id int32) (Product, error) { + row := q.db.QueryRowContext(ctx, getProduct, id) + var i Product + err := row.Scan( + &i.ID, + &i.PrincipleID, + &i.ProductCategoryID, + &i.Title, + &i.Description, + &i.ModelNumber, + &i.ModelNumberFormat, + &i.Notes, + &i.Stock, + &i.ItemCode, + &i.ItemDescription, + ) + return i, err +} + +const getProductByItemCode = `-- name: GetProductByItemCode :one +SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products +WHERE item_code = ? +LIMIT 1 +` + +func (q *Queries) GetProductByItemCode(ctx context.Context, itemCode string) (Product, error) { + row := q.db.QueryRowContext(ctx, getProductByItemCode, itemCode) + var i Product + err := row.Scan( + &i.ID, + &i.PrincipleID, + &i.ProductCategoryID, + &i.Title, + &i.Description, + &i.ModelNumber, + &i.ModelNumberFormat, + &i.Notes, + &i.Stock, + &i.ItemCode, + &i.ItemDescription, + ) + return i, err +} + +const getProductsByCategory = `-- name: GetProductsByCategory :many +SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products +WHERE product_category_id = ? +ORDER BY title +LIMIT ? OFFSET ? +` + +type GetProductsByCategoryParams struct { + ProductCategoryID int32 `json:"product_category_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetProductsByCategory(ctx context.Context, arg GetProductsByCategoryParams) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, getProductsByCategory, arg.ProductCategoryID, arg.Limit, arg.Offset) + 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 listProducts = `-- name: ListProducts :many +SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products +ORDER BY title +LIMIT ? OFFSET ? +` + +type ListProductsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListProducts(ctx context.Context, arg ListProductsParams) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, listProducts, arg.Limit, arg.Offset) + 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 searchProductsByTitle = `-- name: SearchProductsByTitle :many +SELECT id, principle_id, product_category_id, title, description, model_number, model_number_format, notes, stock, item_code, item_description FROM products +WHERE title LIKE CONCAT('%', ?, '%') +ORDER BY title +LIMIT ? OFFSET ? +` + +type SearchProductsByTitleParams struct { + CONCAT interface{} `json:"CONCAT"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) SearchProductsByTitle(ctx context.Context, arg SearchProductsByTitleParams) ([]Product, error) { + rows, err := q.db.QueryContext(ctx, searchProductsByTitle, arg.CONCAT, arg.Limit, arg.Offset) + 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 updateProduct = `-- name: UpdateProduct :exec +UPDATE products +SET principle_id = ?, + product_category_id = ?, + title = ?, + description = ?, + model_number = ?, + model_number_format = ?, + notes = ?, + stock = ?, + item_code = ?, + item_description = ? +WHERE id = ? +` + +type UpdateProductParams struct { + PrincipleID int32 `json:"principle_id"` + ProductCategoryID int32 `json:"product_category_id"` + Title string `json:"title"` + Description string `json:"description"` + ModelNumber sql.NullString `json:"model_number"` + ModelNumberFormat sql.NullString `json:"model_number_format"` + Notes sql.NullString `json:"notes"` + Stock bool `json:"stock"` + ItemCode string `json:"item_code"` + ItemDescription string `json:"item_description"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdateProduct(ctx context.Context, arg UpdateProductParams) error { + _, err := q.db.ExecContext(ctx, updateProduct, + arg.PrincipleID, + arg.ProductCategoryID, + arg.Title, + arg.Description, + arg.ModelNumber, + arg.ModelNumberFormat, + arg.Notes, + arg.Stock, + arg.ItemCode, + arg.ItemDescription, + arg.ID, + ) + return err +} diff --git a/go-app/internal/cmc/db/purchase_orders.sql.go b/go-app/internal/cmc/db/purchase_orders.sql.go new file mode 100644 index 00000000..2b782599 --- /dev/null +++ b/go-app/internal/cmc/db/purchase_orders.sql.go @@ -0,0 +1,375 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: purchase_orders.sql + +package db + +import ( + "context" + "database/sql" + "time" +) + +const createPurchaseOrder = `-- name: CreatePurchaseOrder :execresult +INSERT INTO purchase_orders ( + issue_date, dispatch_date, date_arrived, title, + principle_id, principle_reference, document_id, + currency_id, ordered_from, description, dispatch_by, + deliver_to, shipping_instructions, jobs_text, + freight_forwarder_text, parent_purchase_order_id +) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +) +` + +type CreatePurchaseOrderParams struct { + IssueDate time.Time `json:"issue_date"` + DispatchDate time.Time `json:"dispatch_date"` + DateArrived time.Time `json:"date_arrived"` + Title string `json:"title"` + PrincipleID int32 `json:"principle_id"` + PrincipleReference string `json:"principle_reference"` + DocumentID int32 `json:"document_id"` + CurrencyID sql.NullInt32 `json:"currency_id"` + OrderedFrom string `json:"ordered_from"` + Description string `json:"description"` + DispatchBy string `json:"dispatch_by"` + DeliverTo string `json:"deliver_to"` + ShippingInstructions string `json:"shipping_instructions"` + JobsText string `json:"jobs_text"` + FreightForwarderText string `json:"freight_forwarder_text"` + ParentPurchaseOrderID int32 `json:"parent_purchase_order_id"` +} + +func (q *Queries) CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (sql.Result, error) { + return q.db.ExecContext(ctx, createPurchaseOrder, + arg.IssueDate, + arg.DispatchDate, + arg.DateArrived, + arg.Title, + arg.PrincipleID, + arg.PrincipleReference, + arg.DocumentID, + arg.CurrencyID, + arg.OrderedFrom, + arg.Description, + arg.DispatchBy, + arg.DeliverTo, + arg.ShippingInstructions, + arg.JobsText, + arg.FreightForwarderText, + arg.ParentPurchaseOrderID, + ) +} + +const deletePurchaseOrder = `-- name: DeletePurchaseOrder :exec +DELETE FROM purchase_orders +WHERE id = ? +` + +func (q *Queries) DeletePurchaseOrder(ctx context.Context, id int32) error { + _, err := q.db.ExecContext(ctx, deletePurchaseOrder, id) + return err +} + +const getPurchaseOrder = `-- name: GetPurchaseOrder :one +SELECT id, issue_date, dispatch_date, date_arrived, title, principle_id, principle_reference, document_id, currency_id, ordered_from, description, dispatch_by, deliver_to, shipping_instructions, jobs_text, freight_forwarder_text, parent_purchase_order_id FROM purchase_orders +WHERE id = ? LIMIT 1 +` + +func (q *Queries) GetPurchaseOrder(ctx context.Context, id int32) (PurchaseOrder, error) { + row := q.db.QueryRowContext(ctx, getPurchaseOrder, id) + var i PurchaseOrder + err := row.Scan( + &i.ID, + &i.IssueDate, + &i.DispatchDate, + &i.DateArrived, + &i.Title, + &i.PrincipleID, + &i.PrincipleReference, + &i.DocumentID, + &i.CurrencyID, + &i.OrderedFrom, + &i.Description, + &i.DispatchBy, + &i.DeliverTo, + &i.ShippingInstructions, + &i.JobsText, + &i.FreightForwarderText, + &i.ParentPurchaseOrderID, + ) + return i, err +} + +const getPurchaseOrderRevisions = `-- name: GetPurchaseOrderRevisions :many +SELECT id, issue_date, dispatch_date, date_arrived, title, principle_id, principle_reference, document_id, currency_id, ordered_from, description, dispatch_by, deliver_to, shipping_instructions, jobs_text, freight_forwarder_text, parent_purchase_order_id FROM purchase_orders +WHERE parent_purchase_order_id = ? +ORDER BY id DESC +` + +func (q *Queries) GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error) { + rows, err := q.db.QueryContext(ctx, getPurchaseOrderRevisions, parentPurchaseOrderID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurchaseOrder{} + for rows.Next() { + var i PurchaseOrder + if err := rows.Scan( + &i.ID, + &i.IssueDate, + &i.DispatchDate, + &i.DateArrived, + &i.Title, + &i.PrincipleID, + &i.PrincipleReference, + &i.DocumentID, + &i.CurrencyID, + &i.OrderedFrom, + &i.Description, + &i.DispatchBy, + &i.DeliverTo, + &i.ShippingInstructions, + &i.JobsText, + &i.FreightForwarderText, + &i.ParentPurchaseOrderID, + ); 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 getPurchaseOrdersByPrinciple = `-- name: GetPurchaseOrdersByPrinciple :many +SELECT id, issue_date, dispatch_date, date_arrived, title, principle_id, principle_reference, document_id, currency_id, ordered_from, description, dispatch_by, deliver_to, shipping_instructions, jobs_text, freight_forwarder_text, parent_purchase_order_id FROM purchase_orders +WHERE principle_id = ? +ORDER BY issue_date DESC +LIMIT ? OFFSET ? +` + +type GetPurchaseOrdersByPrincipleParams struct { + PrincipleID int32 `json:"principle_id"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error) { + rows, err := q.db.QueryContext(ctx, getPurchaseOrdersByPrinciple, arg.PrincipleID, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurchaseOrder{} + for rows.Next() { + var i PurchaseOrder + if err := rows.Scan( + &i.ID, + &i.IssueDate, + &i.DispatchDate, + &i.DateArrived, + &i.Title, + &i.PrincipleID, + &i.PrincipleReference, + &i.DocumentID, + &i.CurrencyID, + &i.OrderedFrom, + &i.Description, + &i.DispatchBy, + &i.DeliverTo, + &i.ShippingInstructions, + &i.JobsText, + &i.FreightForwarderText, + &i.ParentPurchaseOrderID, + ); 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 listPurchaseOrders = `-- name: ListPurchaseOrders :many +SELECT id, issue_date, dispatch_date, date_arrived, title, principle_id, principle_reference, document_id, currency_id, ordered_from, description, dispatch_by, deliver_to, shipping_instructions, jobs_text, freight_forwarder_text, parent_purchase_order_id FROM purchase_orders +ORDER BY issue_date DESC +LIMIT ? OFFSET ? +` + +type ListPurchaseOrdersParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) ListPurchaseOrders(ctx context.Context, arg ListPurchaseOrdersParams) ([]PurchaseOrder, error) { + rows, err := q.db.QueryContext(ctx, listPurchaseOrders, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurchaseOrder{} + for rows.Next() { + var i PurchaseOrder + if err := rows.Scan( + &i.ID, + &i.IssueDate, + &i.DispatchDate, + &i.DateArrived, + &i.Title, + &i.PrincipleID, + &i.PrincipleReference, + &i.DocumentID, + &i.CurrencyID, + &i.OrderedFrom, + &i.Description, + &i.DispatchBy, + &i.DeliverTo, + &i.ShippingInstructions, + &i.JobsText, + &i.FreightForwarderText, + &i.ParentPurchaseOrderID, + ); 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 searchPurchaseOrdersByTitle = `-- name: SearchPurchaseOrdersByTitle :many +SELECT id, issue_date, dispatch_date, date_arrived, title, principle_id, principle_reference, document_id, currency_id, ordered_from, description, dispatch_by, deliver_to, shipping_instructions, jobs_text, freight_forwarder_text, parent_purchase_order_id FROM purchase_orders +WHERE title LIKE CONCAT('%', ?, '%') +ORDER BY issue_date DESC +LIMIT ? OFFSET ? +` + +type SearchPurchaseOrdersByTitleParams struct { + CONCAT interface{} `json:"CONCAT"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) SearchPurchaseOrdersByTitle(ctx context.Context, arg SearchPurchaseOrdersByTitleParams) ([]PurchaseOrder, error) { + rows, err := q.db.QueryContext(ctx, searchPurchaseOrdersByTitle, arg.CONCAT, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurchaseOrder{} + for rows.Next() { + var i PurchaseOrder + if err := rows.Scan( + &i.ID, + &i.IssueDate, + &i.DispatchDate, + &i.DateArrived, + &i.Title, + &i.PrincipleID, + &i.PrincipleReference, + &i.DocumentID, + &i.CurrencyID, + &i.OrderedFrom, + &i.Description, + &i.DispatchBy, + &i.DeliverTo, + &i.ShippingInstructions, + &i.JobsText, + &i.FreightForwarderText, + &i.ParentPurchaseOrderID, + ); 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 updatePurchaseOrder = `-- name: UpdatePurchaseOrder :exec +UPDATE purchase_orders +SET issue_date = ?, + dispatch_date = ?, + date_arrived = ?, + title = ?, + principle_id = ?, + principle_reference = ?, + document_id = ?, + currency_id = ?, + ordered_from = ?, + description = ?, + dispatch_by = ?, + deliver_to = ?, + shipping_instructions = ?, + jobs_text = ?, + freight_forwarder_text = ?, + parent_purchase_order_id = ? +WHERE id = ? +` + +type UpdatePurchaseOrderParams struct { + IssueDate time.Time `json:"issue_date"` + DispatchDate time.Time `json:"dispatch_date"` + DateArrived time.Time `json:"date_arrived"` + Title string `json:"title"` + PrincipleID int32 `json:"principle_id"` + PrincipleReference string `json:"principle_reference"` + DocumentID int32 `json:"document_id"` + CurrencyID sql.NullInt32 `json:"currency_id"` + OrderedFrom string `json:"ordered_from"` + Description string `json:"description"` + DispatchBy string `json:"dispatch_by"` + DeliverTo string `json:"deliver_to"` + ShippingInstructions string `json:"shipping_instructions"` + JobsText string `json:"jobs_text"` + FreightForwarderText string `json:"freight_forwarder_text"` + ParentPurchaseOrderID int32 `json:"parent_purchase_order_id"` + ID int32 `json:"id"` +} + +func (q *Queries) UpdatePurchaseOrder(ctx context.Context, arg UpdatePurchaseOrderParams) error { + _, err := q.db.ExecContext(ctx, updatePurchaseOrder, + arg.IssueDate, + arg.DispatchDate, + arg.DateArrived, + arg.Title, + arg.PrincipleID, + arg.PrincipleReference, + arg.DocumentID, + arg.CurrencyID, + arg.OrderedFrom, + arg.Description, + arg.DispatchBy, + arg.DeliverTo, + arg.ShippingInstructions, + arg.JobsText, + arg.FreightForwarderText, + arg.ParentPurchaseOrderID, + arg.ID, + ) + return err +} diff --git a/go-app/internal/cmc/db/querier.go b/go-app/internal/cmc/db/querier.go new file mode 100644 index 00000000..7b450c35 --- /dev/null +++ b/go-app/internal/cmc/db/querier.go @@ -0,0 +1,58 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 + +package db + +import ( + "context" + "database/sql" +) + +type Querier interface { + ArchiveEnquiry(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) + CreateCustomer(ctx context.Context, arg CreateCustomerParams) (sql.Result, error) + CreateEnquiry(ctx context.Context, arg CreateEnquiryParams) (sql.Result, error) + CreateProduct(ctx context.Context, arg CreateProductParams) (sql.Result, error) + CreatePurchaseOrder(ctx context.Context, arg CreatePurchaseOrderParams) (sql.Result, error) + DeleteCustomer(ctx context.Context, id int32) error + DeleteProduct(ctx context.Context, id int32) error + DeletePurchaseOrder(ctx context.Context, id int32) 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) + GetCustomer(ctx context.Context, id int32) (Customer, error) + GetCustomerByABN(ctx context.Context, abn sql.NullString) (Customer, 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) + 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) + GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error) + GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error) + ListArchivedEnquiries(ctx context.Context, arg ListArchivedEnquiriesParams) ([]ListArchivedEnquiriesRow, error) + ListCustomers(ctx context.Context, arg ListCustomersParams) ([]Customer, error) + ListEnquiries(ctx context.Context, arg ListEnquiriesParams) ([]ListEnquiriesRow, error) + ListProducts(ctx context.Context, arg ListProductsParams) ([]Product, error) + ListPurchaseOrders(ctx context.Context, arg ListPurchaseOrdersParams) ([]PurchaseOrder, error) + MarkEnquirySubmitted(ctx context.Context, arg MarkEnquirySubmittedParams) error + SearchCustomersByName(ctx context.Context, arg SearchCustomersByNameParams) ([]Customer, 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) + UnarchiveEnquiry(ctx context.Context, id int32) error + UpdateCustomer(ctx context.Context, arg UpdateCustomerParams) error + UpdateEnquiry(ctx context.Context, arg UpdateEnquiryParams) error + UpdateEnquiryStatus(ctx context.Context, arg UpdateEnquiryStatusParams) error + UpdateProduct(ctx context.Context, arg UpdateProductParams) error + UpdatePurchaseOrder(ctx context.Context, arg UpdatePurchaseOrderParams) error +} + +var _ Querier = (*Queries)(nil) diff --git a/go-app/internal/cmc/handlers/customers.go b/go-app/internal/cmc/handlers/customers.go new file mode 100644 index 00000000..c730be68 --- /dev/null +++ b/go-app/internal/cmc/handlers/customers.go @@ -0,0 +1,205 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "github.com/gorilla/mux" +) + +type CustomerHandler struct { + queries *db.Queries +} + +func NewCustomerHandler(queries *db.Queries) *CustomerHandler { + return &CustomerHandler{queries: queries} +} + +func (h *CustomerHandler) 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 + } + } + + customers, err := h.queries.ListCustomers(r.Context(), db.ListCustomersParams{ + 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(customers) +} + +func (h *CustomerHandler) Get(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid customer ID", http.StatusBadRequest) + return + } + + customer, err := h.queries.GetCustomer(r.Context(), int32(id)) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Customer not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(customer) +} + +func (h *CustomerHandler) 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.CreateCustomerParams{ + Name: r.FormValue("name"), + TradingName: r.FormValue("trading_name"), + Abn: sql.NullString{String: r.FormValue("abn"), Valid: r.FormValue("abn") != ""}, + Notes: r.FormValue("notes"), + DiscountPricingPolicies: r.FormValue("discount_pricing_policies"), + PaymentTerms: r.FormValue("payment_terms"), + CustomerCategoryID: 1, // Default category + Url: r.FormValue("url"), + CountryID: 1, // Default country + } + + // Check if this is a JSON request + if r.Header.Get("Content-Type") == "application/json" { + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + + result, err := h.queries.CreateCustomer(r.Context(), params) + if err != nil { + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(`
| Company Name: | +{{.Customer.Name}} | +
|---|---|
| Trading Name: | +{{if .Customer.TradingName}}{{.Customer.TradingName}}{{else}}Not specified{{end}} | +
| ABN: | +{{if .Customer.Abn.Valid}}{{.Customer.Abn.String}}{{else}}Not specified{{end}} | +
| Payment Terms: | +{{.Customer.PaymentTerms}} | +
| Website: | ++ {{if .Customer.Url}} + {{.Customer.Url}} + {{else}} + Not specified + {{end}} + | +
| Created: | +{{.Customer.Created}} | +
{{.Customer.Notes}}
+{{.Customer.DiscountPricingPolicies}}
+| ID | +Name | +Trading Name | +ABN | +Payment Terms | +Actions | +
|---|---|---|---|---|---|
| {{.ID}} | ++ {{.Name}} + | +{{.TradingName}} | +{{.Abn}} | +{{.PaymentTerms}} | ++ + | +
|
+ No customers found + |
+ |||||
+ {{if .Enquiry.ID}} + Edit Enquiry: {{.Enquiry.Title}} + {{else}} + New Enquiry + {{end}} +
++ Page {{.Page}} of pages, showing {{len .Enquiries}} Enquiries +
+Enquiry Details
+| Enquiry Number: | +{{.Enquiry.Title}} | +
| Created: | +{{.Enquiry.Created.Format "2 Jan 2006 15:04"}} | +
| Submitted: | +{{.Enquiry.Submitted.Time.Format "2 Jan 2006"}} | +
| Sales Rep: | ++ {{if and .Enquiry.UserFirstName.Valid .Enquiry.UserLastName.Valid}} + + {{.Enquiry.UserFirstName.String}} {{.Enquiry.UserLastName.String}} + + {{else}} + - + {{end}} + | +
| Status: | ++ + {{if .Enquiry.StatusName.Valid}} + {{.Enquiry.StatusName.String}} + {{else}} + Unknown + {{end}} + + | +
| Posted: | ++ {{if .Enquiry.Posted}} + Yes + {{else}} + No + {{end}} + | +
| GST: | ++ {{if .Enquiry.Gst}} + Applicable + {{else}} + Not Applicable + {{end}} + | +
{{.Enquiry.Comments}}
+Related Documents
+Note: Related quotes, invoices, jobs, and email correspondence would be displayed here. These require additional database queries and relationships to be implemented.
+Customer
+| Contact: | ++ {{if and .Enquiry.ContactFirstName.Valid .Enquiry.ContactLastName.Valid}} + + {{.Enquiry.ContactFirstName.String}} {{.Enquiry.ContactLastName.String}} + + {{else}} + - + {{end}} + | +
| Email: | ++ + {{.Enquiry.ContactEmail.String}} + + | +
| Phone: | ++ {{if .Enquiry.ContactMobile.Valid}} + {{.Enquiry.ContactMobile.String}} + {{else if .Enquiry.ContactDirectPhone.Valid}} + {{.Enquiry.ContactDirectPhone.String}} + {{else if .Enquiry.ContactPhone.Valid}} + {{.Enquiry.ContactPhone.String}} + {{if .Enquiry.ContactPhoneExtension.Valid}} + ext:{{.Enquiry.ContactPhoneExtension.String}} + {{end}} + {{else}} + - + {{end}} + | +
Principle (Supplier)
+{{.Enquiry.PrincipleName.String}}
+ {{end}} +Code: {{.Enquiry.PrincipleCode}}
+ {{end}} +Location
+| State: | ++ {{if .Enquiry.StateName.Valid}} + {{.Enquiry.StateName.String}} + {{else}} + - + {{end}} + | +
| Country: | ++ {{if .Enquiry.CountryName.Valid}} + {{.Enquiry.CountryName.String}} + {{else}} + - + {{end}} + | +
Actions
+| User | +Date | +Principle | +Enquiry Number | +Customer | +Contact | +Phone No | +Status | +Comments | +Actions | +|
|---|---|---|---|---|---|---|---|---|---|---|
| + {{if and .UserFirstName.Valid .UserLastName.Valid}} + + {{slice .UserFirstName.String 0 1}}{{slice .UserLastName.String 0 1}} + + {{else}} + - + {{end}} + | + + ++ {{.Created.Format "2 Jan 2006"}} + | + + ++ {{if .PrincipleShortName.Valid}} + {{.PrincipleShortName.String}} + {{else if .PrincipleName.Valid}} + {{.PrincipleName.String}} + {{else}} + - + {{end}} + | + + ++ {{.Title}} + | + + ++ {{if .CustomerName.Valid}} + {{.CustomerName.String}} + {{else}} + - + {{end}} + | + + ++ {{if and .ContactFirstName.Valid .ContactLastName.Valid}} + {{.ContactFirstName.String}} {{.ContactLastName.String}} + {{else}} + - + {{end}} + | + + ++ {{if .ContactEmail.Valid}} + + {{.ContactEmail.String}} + + {{else}} + - + {{end}} + | + + ++ {{if .ContactMobile.Valid}} + {{.ContactMobile.String}} + {{else if .ContactDirectPhone.Valid}} + {{.ContactDirectPhone.String}} + {{else if .ContactPhone.Valid}} + {{.ContactPhone.String}} + {{if .ContactPhoneExtension.Valid}} + ext:{{.ContactPhoneExtension.String}} + {{end}} + {{else}} + - + {{end}} + | + + +
+
+ {{if .StatusName.Valid}}
+ {{.StatusName.String}}
+ {{else}}
+ -
+ {{end}}
+
+ |
+
+
+ + {{if gt (len .Comments) 150}} + {{slice .Comments 0 150}} + .....more + {{else}} + {{.Comments}} + {{end}} + | + + ++ + | +
| + No enquiries found. + | +||||||||||
Welcome to the modern CMC Sales management system
+Customers
+Manage customer accounts
+Products
+Product catalog management
+Purchase Orders
+Track and manage orders
+Enquiries
+Sales enquiry management
+| Title: | +{{.Product.Title}} | +
|---|---|
| Item Code: | +{{.Product.ItemCode}} | +
| Item Description: | +{{.Product.ItemDescription}} | +
| Model Number: | +{{if .Product.ModelNumber.Valid}}{{.Product.ModelNumber.String}}{{else}}Not specified{{end}} | +
| Stock Type: | ++ {{if .Product.Stock}} + Stock + {{else}} + Indent + {{end}} + | +
| Category: | +Category {{.Product.ProductCategoryID}} | +
| Principle: | +Principle {{.Product.PrincipleID}} | +
{{.Product.Description}}
+{{.Product.Notes.String}}
+| ID | +Title | +Item Code | +Description | +Model Number | +Stock Type | +Actions | +
|---|---|---|---|---|---|---|
| {{.ID}} | ++ {{.Title}} + | ++ {{.ItemCode}} + | +{{truncate .Description 50}} | +{{if .ModelNumber.Valid}}{{.ModelNumber.String}}{{else}}-{{end}} | ++ {{if .Stock}} + Stock + {{else}} + Indent + {{end}} + | ++ + | +
|
+ No products found + |
+ ||||||
| PO Number: | +{{.PurchaseOrder.Title}} | +
|---|---|
| Issue Date: | +{{formatDate .PurchaseOrder.IssueDate}} | +
| Dispatch Date: | +{{formatDate .PurchaseOrder.DispatchDate}} | +
| Date Arrived: | ++ {{if .PurchaseOrder.DateArrived}} + {{formatDate .PurchaseOrder.DateArrived}} + {{else}} + Not arrived + {{end}} + | +
| Principle Reference: | +{{.PurchaseOrder.PrincipleReference}} | +
| Currency: | +{{if .PurchaseOrder.CurrencyID.Valid}}Currency {{.PurchaseOrder.CurrencyID.Int32}}{{else}}Not specified{{end}} | +
| Dispatch By: | +{{.PurchaseOrder.DispatchBy}} | +
{{.PurchaseOrder.OrderedFrom}}
+ {{.PurchaseOrder.DeliverTo}}
+ {{.PurchaseOrder.Description}}
+{{.PurchaseOrder.ShippingInstructions}}
+{{.PurchaseOrder.JobsText}}
+{{.PurchaseOrder.FreightForwarderText}}
+| PO Number | +Issue Date | +Dispatch Date | +Ordered From | +Description | +Status | +Actions | +
|---|---|---|---|---|---|---|
| + {{.Title}} + | +{{formatDate .IssueDate}} | +{{formatDate .DispatchDate}} | +{{truncate .OrderedFrom 30}} | +{{truncate .Description 50}} | ++ {{if .DateArrived}} + Arrived + {{else if .DispatchDate}} + Dispatched + {{else}} + Pending + {{end}} + | ++ + | +
|
+ No purchase orders found + |
+ ||||||