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(`
Error creating customer
`)) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + id, err := result.LastInsertId() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // If HTMX request, redirect to customer page + if r.Header.Get("HX-Request") == "true" { + w.Header().Set("HX-Redirect", "/customers/"+strconv.FormatInt(id, 10)) + w.WriteHeader(http.StatusCreated) + return + } + + // JSON response for API + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": id, + }) +} + +func (h *CustomerHandler) Update(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 + } + + var params db.UpdateCustomerParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + params.ID = int32(id) + + if err := h.queries.UpdateCustomer(r.Context(), params); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *CustomerHandler) Delete(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 + } + + if err := h.queries.DeleteCustomer(r.Context(), int32(id)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *CustomerHandler) Search(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Search query required", http.StatusBadRequest) + return + } + + 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.SearchCustomersByName(r.Context(), db.SearchCustomersByNameParams{ + CONCAT: query, + CONCAT_2: query, + 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) +} \ No newline at end of file diff --git a/go-app/internal/cmc/handlers/enquiry.go b/go-app/internal/cmc/handlers/enquiry.go new file mode 100644 index 00000000..d6f7e260 --- /dev/null +++ b/go-app/internal/cmc/handlers/enquiry.go @@ -0,0 +1,344 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + "time" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "github.com/gorilla/mux" +) + +type EnquiryHandler struct { + queries *db.Queries +} + +func NewEnquiryHandler(queries *db.Queries) *EnquiryHandler { + return &EnquiryHandler{queries: queries} +} + +type CreateEnquiryRequest 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"` +} + +type UpdateEnquiryRequest 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"` +} + +func (h *EnquiryHandler) List(w http.ResponseWriter, r *http.Request) { + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 150 // Same as CakePHP controller + offset := (page - 1) * limit + + var enquiries interface{} + var err error + + // Check if we want archived enquiries + if r.URL.Query().Get("archived") == "true" { + enquiries, err = h.queries.ListArchivedEnquiries(r.Context(), db.ListArchivedEnquiriesParams{ + Limit: int32(limit), + Offset: int32(offset), + }) + } else { + enquiries, err = h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ + 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(enquiries) +} + +func (h *EnquiryHandler) Get(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Enquiry not found", http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(enquiry) +} + +func (h *EnquiryHandler) Create(w http.ResponseWriter, r *http.Request) { + var req CreateEnquiryRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + now := time.Now() + + result, err := h.queries.CreateEnquiry(r.Context(), db.CreateEnquiryParams{ + Created: now, + Title: req.Title, + UserID: req.UserID, + CustomerID: req.CustomerID, + ContactID: req.ContactID, + ContactUserID: req.ContactUserID, + StateID: req.StateID, + CountryID: req.CountryID, + PrincipleID: req.PrincipleID, + StatusID: req.StatusID, + Comments: req.Comments, + PrincipleCode: req.PrincipleCode, + Gst: req.Gst, + BillingAddressID: req.BillingAddressID, + ShippingAddressID: req.ShippingAddressID, + Posted: req.Posted, + Archived: int8(0), + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + id, err := result.LastInsertId() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Return the created enquiry + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(enquiry) +} + +func (h *EnquiryHandler) Update(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + var req UpdateEnquiryRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + err = h.queries.UpdateEnquiry(r.Context(), db.UpdateEnquiryParams{ + Title: req.Title, + UserID: req.UserID, + CustomerID: req.CustomerID, + ContactID: req.ContactID, + ContactUserID: req.ContactUserID, + StateID: req.StateID, + CountryID: req.CountryID, + PrincipleID: req.PrincipleID, + StatusID: req.StatusID, + Comments: req.Comments, + PrincipleCode: req.PrincipleCode, + Gst: req.Gst, + BillingAddressID: req.BillingAddressID, + ShippingAddressID: req.ShippingAddressID, + Posted: req.Posted, + Submitted: req.Submitted, + ID: int32(id), + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Return the updated enquiry + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(enquiry) +} + +func (h *EnquiryHandler) Delete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + err = h.queries.ArchiveEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *EnquiryHandler) Undelete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + err = h.queries.UnarchiveEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *EnquiryHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + var req struct { + StatusID int32 `json:"status_id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + err = h.queries.UpdateEnquiryStatus(r.Context(), db.UpdateEnquiryStatusParams{ + StatusID: req.StatusID, + ID: int32(id), + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Return the updated enquiry + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(enquiry) +} + +func (h *EnquiryHandler) MarkSubmitted(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + today := time.Now() + err = h.queries.MarkEnquirySubmitted(r.Context(), db.MarkEnquirySubmittedParams{ + Submitted: sql.NullTime{Time: today, Valid: true}, + ID: int32(id), + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *EnquiryHandler) Search(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + h.List(w, r) + return + } + + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 150 + offset := (page - 1) * limit + + enquiries, err := h.queries.SearchEnquiries(r.Context(), db.SearchEnquiriesParams{ + CONCAT: query, + CONCAT_2: query, + CONCAT_3: query, + 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(enquiries) +} \ No newline at end of file diff --git a/go-app/internal/cmc/handlers/pages.go b/go-app/internal/cmc/handlers/pages.go new file mode 100644 index 00000000..c0273d02 --- /dev/null +++ b/go-app/internal/cmc/handlers/pages.go @@ -0,0 +1,603 @@ +package handlers + +import ( + "net/http" + "strconv" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" + "github.com/gorilla/mux" +) + +type PageHandler struct { + queries *db.Queries + tmpl *templates.TemplateManager +} + +func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager) *PageHandler { + return &PageHandler{ + queries: queries, + tmpl: tmpl, + } +} + +// Home page +func (h *PageHandler) Home(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "Title": "Dashboard", + } + + if err := h.tmpl.Render(w, "index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Customer pages +func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) { + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 20 + offset := (page - 1) * limit + + customers, err := h.queries.ListCustomers(r.Context(), db.ListCustomersParams{ + Limit: int32(limit + 1), // Get one extra to check if there are more + Offset: int32(offset), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + hasMore := len(customers) > limit + if hasMore { + customers = customers[:limit] + } + + data := map[string]interface{}{ + "Customers": customers, + "Page": page, + "PrevPage": page - 1, + "NextPage": page + 1, + "HasMore": hasMore, + } + + // Check if this is an HTMX request + if r.Header.Get("HX-Request") == "true" { + if err := h.tmpl.Render(w, "customers/table.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + if err := h.tmpl.Render(w, "customers/index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) CustomersNew(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "Customer": db.Customer{}, + } + + if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) CustomersEdit(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 { + http.Error(w, "Customer not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "Customer": customer, + } + + if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) CustomersShow(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 { + http.Error(w, "Customer not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "Customer": customer, + } + + if err := h.tmpl.Render(w, "customers/show.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("search") + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 20 + offset := (page - 1) * limit + + var customers []db.Customer + var err error + + if query == "" { + customers, err = h.queries.ListCustomers(r.Context(), db.ListCustomersParams{ + Limit: int32(limit + 1), + Offset: int32(offset), + }) + } else { + customers, err = h.queries.SearchCustomersByName(r.Context(), db.SearchCustomersByNameParams{ + CONCAT: query, + CONCAT_2: query, + Limit: int32(limit + 1), + Offset: int32(offset), + }) + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + hasMore := len(customers) > limit + if hasMore { + customers = customers[:limit] + } + + data := map[string]interface{}{ + "Customers": customers, + "Page": page, + "PrevPage": page - 1, + "NextPage": page + 1, + "HasMore": hasMore, + } + + w.Header().Set("Content-Type", "text/html") + if err := h.tmpl.Render(w, "customers/table.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Product page handlers +func (h *PageHandler) ProductsIndex(w http.ResponseWriter, r *http.Request) { + // Similar implementation to CustomersIndex but for products + data := map[string]interface{}{ + "Products": []db.Product{}, // Placeholder + } + + if err := h.tmpl.Render(w, "products/index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) ProductsNew(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "Product": db.Product{}, + } + + if err := h.tmpl.Render(w, "products/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) ProductsShow(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + product, err := h.queries.GetProduct(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "Product": product, + } + + if err := h.tmpl.Render(w, "products/show.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) ProductsEdit(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + product, err := h.queries.GetProduct(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "Product": product, + } + + if err := h.tmpl.Render(w, "products/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) ProductsSearch(w http.ResponseWriter, r *http.Request) { + // Similar to CustomersSearch but for products + data := map[string]interface{}{ + "Products": []db.Product{}, + } + + w.Header().Set("Content-Type", "text/html") + if err := h.tmpl.Render(w, "products/table.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Purchase Order page handlers +func (h *PageHandler) PurchaseOrdersIndex(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "PurchaseOrders": []db.PurchaseOrder{}, + } + + if err := h.tmpl.Render(w, "purchase-orders/index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) PurchaseOrdersNew(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "PurchaseOrder": db.PurchaseOrder{}, + } + + if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) PurchaseOrdersShow(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) + return + } + + purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Purchase order not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "PurchaseOrder": purchaseOrder, + } + + if err := h.tmpl.Render(w, "purchase-orders/show.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) PurchaseOrdersEdit(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) + return + } + + purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Purchase order not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "PurchaseOrder": purchaseOrder, + } + + if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) PurchaseOrdersSearch(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "PurchaseOrders": []db.PurchaseOrder{}, + } + + w.Header().Set("Content-Type", "text/html") + if err := h.tmpl.Render(w, "purchase-orders/table.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Enquiry page handlers +func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 150 + offset := (page - 1) * limit + + var enquiries interface{} + var err error + var hasMore bool + + // Check if we want archived enquiries + if r.URL.Query().Get("archived") == "true" { + archivedEnquiries, err := h.queries.ListArchivedEnquiries(r.Context(), db.ListArchivedEnquiriesParams{ + Limit: int32(limit + 1), + Offset: int32(offset), + }) + if err == nil { + hasMore = len(archivedEnquiries) > limit + if hasMore { + archivedEnquiries = archivedEnquiries[:limit] + } + enquiries = archivedEnquiries + } + } else { + activeEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ + Limit: int32(limit + 1), + Offset: int32(offset), + }) + if err == nil { + hasMore = len(activeEnquiries) > limit + if hasMore { + activeEnquiries = activeEnquiries[:limit] + } + enquiries = activeEnquiries + } + } + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Get status list for dropdown and CSS classes + statuses, err := h.queries.GetAllStatuses(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "Enquiries": enquiries, + "Statuses": statuses, + "Page": page, + "PrevPage": page - 1, + "NextPage": page + 1, + "HasMore": hasMore, + } + + // Check if this is an HTMX request + if r.Header.Get("HX-Request") == "true" { + if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } + + if err := h.tmpl.Render(w, "enquiries/index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) EnquiriesNew(w http.ResponseWriter, r *http.Request) { + // Get required form data + statuses, err := h.queries.GetAllStatuses(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + principles, err := h.queries.GetAllPrinciples(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + states, err := h.queries.GetAllStates(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + countries, err := h.queries.GetAllCountries(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "Enquiry": db.Enquiry{}, + "Statuses": statuses, + "Principles": principles, + "States": states, + "Countries": countries, + } + + if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) EnquiriesShow(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Enquiry not found", http.StatusNotFound) + return + } + + data := map[string]interface{}{ + "Enquiry": enquiry, + } + + if err := h.tmpl.Render(w, "enquiries/show.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) EnquiriesEdit(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid enquiry ID", http.StatusBadRequest) + return + } + + enquiry, err := h.queries.GetEnquiry(r.Context(), int32(id)) + if err != nil { + http.Error(w, "Enquiry not found", http.StatusNotFound) + return + } + + // Get required form data + statuses, err := h.queries.GetAllStatuses(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + principles, err := h.queries.GetAllPrinciples(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + states, err := h.queries.GetAllStates(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + countries, err := h.queries.GetAllCountries(r.Context()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + data := map[string]interface{}{ + "Enquiry": enquiry, + "Statuses": statuses, + "Principles": principles, + "States": states, + "Countries": countries, + } + + if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("search") + page := 1 + if p := r.URL.Query().Get("page"); p != "" { + if val, err := strconv.Atoi(p); err == nil && val > 0 { + page = val + } + } + + limit := 150 + offset := (page - 1) * limit + + var enquiries interface{} + var hasMore bool + + if query == "" { + // If no search query, return regular list + regularEnquiries, err := h.queries.ListEnquiries(r.Context(), db.ListEnquiriesParams{ + Limit: int32(limit + 1), + Offset: int32(offset), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + hasMore = len(regularEnquiries) > limit + if hasMore { + regularEnquiries = regularEnquiries[:limit] + } + enquiries = regularEnquiries + } else { + searchResults, err := h.queries.SearchEnquiries(r.Context(), db.SearchEnquiriesParams{ + CONCAT: query, + CONCAT_2: query, + CONCAT_3: query, + Limit: int32(limit + 1), + Offset: int32(offset), + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + hasMore = len(searchResults) > limit + if hasMore { + searchResults = searchResults[:limit] + } + enquiries = searchResults + } + + data := map[string]interface{}{ + "Enquiries": enquiries, + "Page": page, + "PrevPage": page - 1, + "NextPage": page + 1, + "HasMore": hasMore, + } + + w.Header().Set("Content-Type", "text/html") + if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} \ No newline at end of file diff --git a/go-app/internal/cmc/handlers/products.go b/go-app/internal/cmc/handlers/products.go new file mode 100644 index 00000000..891168f3 --- /dev/null +++ b/go-app/internal/cmc/handlers/products.go @@ -0,0 +1,171 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "github.com/gorilla/mux" +) + +type ProductHandler struct { + queries *db.Queries +} + +func NewProductHandler(queries *db.Queries) *ProductHandler { + return &ProductHandler{queries: queries} +} + +func (h *ProductHandler) 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 + } + } + + products, err := h.queries.ListProducts(r.Context(), db.ListProductsParams{ + 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(products) +} + +func (h *ProductHandler) Get(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + product, err := h.queries.GetProduct(r.Context(), int32(id)) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Product not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(product) +} + +func (h *ProductHandler) Create(w http.ResponseWriter, r *http.Request) { + var params db.CreateProductParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + result, err := h.queries.CreateProduct(r.Context(), params) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + id, err := result.LastInsertId() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": id, + }) +} + +func (h *ProductHandler) Update(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + var params db.UpdateProductParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + params.ID = int32(id) + + if err := h.queries.UpdateProduct(r.Context(), params); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *ProductHandler) Delete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid product ID", http.StatusBadRequest) + return + } + + if err := h.queries.DeleteProduct(r.Context(), int32(id)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *ProductHandler) Search(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Search query required", http.StatusBadRequest) + return + } + + 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 + } + } + + products, err := h.queries.SearchProductsByTitle(r.Context(), db.SearchProductsByTitleParams{ + CONCAT: query, + 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(products) +} \ No newline at end of file diff --git a/go-app/internal/cmc/handlers/purchase_orders.go b/go-app/internal/cmc/handlers/purchase_orders.go new file mode 100644 index 00000000..bd9f30f2 --- /dev/null +++ b/go-app/internal/cmc/handlers/purchase_orders.go @@ -0,0 +1,171 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + "strconv" + + "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" + "github.com/gorilla/mux" +) + +type PurchaseOrderHandler struct { + queries *db.Queries +} + +func NewPurchaseOrderHandler(queries *db.Queries) *PurchaseOrderHandler { + return &PurchaseOrderHandler{queries: queries} +} + +func (h *PurchaseOrderHandler) 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 + } + } + + purchaseOrders, err := h.queries.ListPurchaseOrders(r.Context(), db.ListPurchaseOrdersParams{ + 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(purchaseOrders) +} + +func (h *PurchaseOrderHandler) Get(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) + return + } + + purchaseOrder, err := h.queries.GetPurchaseOrder(r.Context(), int32(id)) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Purchase order not found", http.StatusNotFound) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(purchaseOrder) +} + +func (h *PurchaseOrderHandler) Create(w http.ResponseWriter, r *http.Request) { + var params db.CreatePurchaseOrderParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + result, err := h.queries.CreatePurchaseOrder(r.Context(), params) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + id, err := result.LastInsertId() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": id, + }) +} + +func (h *PurchaseOrderHandler) Update(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) + return + } + + var params db.UpdatePurchaseOrderParams + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + params.ID = int32(id) + + if err := h.queries.UpdatePurchaseOrder(r.Context(), params); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *PurchaseOrderHandler) Delete(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := strconv.Atoi(vars["id"]) + if err != nil { + http.Error(w, "Invalid purchase order ID", http.StatusBadRequest) + return + } + + if err := h.queries.DeletePurchaseOrder(r.Context(), int32(id)); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +func (h *PurchaseOrderHandler) Search(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Search query required", http.StatusBadRequest) + return + } + + 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 + } + } + + purchaseOrders, err := h.queries.SearchPurchaseOrdersByTitle(r.Context(), db.SearchPurchaseOrdersByTitleParams{ + CONCAT: query, + 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(purchaseOrders) +} \ No newline at end of file diff --git a/go-app/internal/cmc/templates/templates.go b/go-app/internal/cmc/templates/templates.go new file mode 100644 index 00000000..57fabbce --- /dev/null +++ b/go-app/internal/cmc/templates/templates.go @@ -0,0 +1,128 @@ +package templates + +import ( + "fmt" + "html/template" + "io" + "os" + "path/filepath" + "time" +) + +type TemplateManager struct { + templates map[string]*template.Template +} + +func NewTemplateManager(templatesDir string) (*TemplateManager, error) { + tm := &TemplateManager{ + templates: make(map[string]*template.Template), + } + + // Define template functions + funcMap := template.FuncMap{ + "formatDate": formatDate, + "truncate": truncate, + "currency": formatCurrency, + } + + // Load all templates + layouts, err := filepath.Glob(filepath.Join(templatesDir, "layouts/*.html")) + if err != nil { + return nil, err + } + + partials, err := filepath.Glob(filepath.Join(templatesDir, "partials/*.html")) + if err != nil { + return nil, err + } + + // Load page templates + pages := []string{ + "customers/index.html", + "customers/show.html", + "customers/form.html", + "customers/table.html", + "products/index.html", + "products/show.html", + "products/form.html", + "products/table.html", + "purchase-orders/index.html", + "purchase-orders/show.html", + "purchase-orders/form.html", + "purchase-orders/table.html", + "enquiries/index.html", + "enquiries/show.html", + "enquiries/form.html", + "enquiries/table.html", + "index.html", + } + + for _, page := range pages { + pagePath := filepath.Join(templatesDir, page) + files := append(layouts, partials...) + files = append(files, pagePath) + + // For index pages, also include the corresponding table template + if filepath.Base(page) == "index.html" { + dir := filepath.Dir(page) + tablePath := filepath.Join(templatesDir, dir, "table.html") + // Check if table file exists before adding it + if _, err := os.Stat(tablePath); err == nil { + files = append(files, tablePath) + } + } + + tmpl, err := template.New(filepath.Base(page)).Funcs(funcMap).ParseFiles(files...) + if err != nil { + return nil, err + } + + tm.templates[page] = tmpl + } + + return tm, nil +} + +func (tm *TemplateManager) Render(w io.Writer, name string, data interface{}) error { + tmpl, ok := tm.templates[name] + if !ok { + return template.New("error").Execute(w, "Template not found") + } + + return tmpl.ExecuteTemplate(w, "base", data) +} + +func (tm *TemplateManager) RenderPartial(w io.Writer, templateFile, templateName string, data interface{}) error { + tmpl, ok := tm.templates[templateFile] + if !ok { + return template.New("error").Execute(w, "Template not found") + } + + return tmpl.ExecuteTemplate(w, templateName, data) +} + +// Template helper functions +func formatDate(t interface{}) string { + switch v := t.(type) { + case time.Time: + return v.Format("2006-01-02") + case string: + if tm, err := time.Parse("2006-01-02 15:04:05", v); err == nil { + return tm.Format("2006-01-02") + } + return v + default: + return fmt.Sprintf("%v", v) + } +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} + +func formatCurrency(amount float64) string { + return fmt.Sprintf("$%.2f", amount) +} \ No newline at end of file diff --git a/go-app/server b/go-app/server new file mode 100755 index 00000000..b4baa764 Binary files /dev/null and b/go-app/server differ diff --git a/go-app/sql/queries/customers.sql b/go-app/sql/queries/customers.sql new file mode 100644 index 00000000..c60678ff --- /dev/null +++ b/go-app/sql/queries/customers.sql @@ -0,0 +1,46 @@ +-- name: GetCustomer :one +SELECT * FROM customers +WHERE id = ? LIMIT 1; + +-- name: ListCustomers :many +SELECT * FROM customers +ORDER BY name +LIMIT ? OFFSET ?; + +-- 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(), ?, ?, ?, ?, ?, ? +); + +-- name: UpdateCustomer :exec +UPDATE customers +SET name = ?, + trading_name = ?, + abn = ?, + notes = ?, + discount_pricing_policies = ?, + payment_terms = ?, + customer_category_id = ?, + url = ?, + country_id = ? +WHERE id = ?; + +-- name: DeleteCustomer :exec +DELETE FROM customers +WHERE id = ?; + +-- name: SearchCustomersByName :many +SELECT * FROM customers +WHERE name LIKE CONCAT('%', ?, '%') + OR trading_name LIKE CONCAT('%', ?, '%') +ORDER BY name +LIMIT ? OFFSET ?; + +-- name: GetCustomerByABN :one +SELECT * FROM customers +WHERE abn = ? +LIMIT 1; \ No newline at end of file diff --git a/go-app/sql/queries/enquiries.sql b/go-app/sql/queries/enquiries.sql new file mode 100644 index 00000000..03348395 --- /dev/null +++ b/go-app/sql/queries/enquiries.sql @@ -0,0 +1,191 @@ +-- name: GetEnquiry :one +SELECT e.*, + 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 = ?; + +-- name: ListEnquiries :many +SELECT e.*, + 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 ?; + +-- name: ListArchivedEnquiries :many +SELECT e.*, + 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 ?; + +-- 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 ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +); + +-- 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 = ?; + +-- name: UpdateEnquiryStatus :exec +UPDATE enquiries SET status_id = ? WHERE id = ?; + +-- name: ArchiveEnquiry :exec +UPDATE enquiries SET archived = 1 WHERE id = ?; + +-- name: UnarchiveEnquiry :exec +UPDATE enquiries SET archived = 0 WHERE id = ?; + +-- name: MarkEnquirySubmitted :exec +UPDATE enquiries SET submitted = ? WHERE id = ?; + +-- name: SearchEnquiries :many +SELECT e.*, + 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 ?; + +-- name: GetEnquiriesByUser :many +SELECT e.*, + 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 ?; + +-- name: GetEnquiriesByCustomer :many +SELECT e.*, + 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 ?; + +-- name: CountEnquiries :one +SELECT COUNT(*) FROM enquiries WHERE archived = 0; + +-- name: CountEnquiriesByStatus :one +SELECT COUNT(*) FROM enquiries WHERE status_id = ? AND archived = 0; + +-- name: CountEnquiriesByPrinciple :one +SELECT COUNT(*) FROM enquiries WHERE principle_code = ?; + +-- name: CountEnquiriesByPrincipleAndState :one +SELECT COUNT(*) FROM enquiries WHERE principle_code = ? AND state_id = ?; + +-- name: GetAllStatuses :many +SELECT id, name FROM statuses ORDER BY name; + +-- name: GetAllPrinciples :many +SELECT id, name, short_name, code FROM principles ORDER BY name; + +-- name: GetAllStates :many +SELECT id, name, shortform, enqform FROM states ORDER BY name; + +-- name: GetAllCountries :many +SELECT id, name FROM countries ORDER BY name; \ No newline at end of file diff --git a/go-app/sql/queries/products.sql b/go-app/sql/queries/products.sql new file mode 100644 index 00000000..09062b00 --- /dev/null +++ b/go-app/sql/queries/products.sql @@ -0,0 +1,52 @@ +-- name: GetProduct :one +SELECT * FROM products +WHERE id = ? LIMIT 1; + +-- name: ListProducts :many +SELECT * FROM products +ORDER BY title +LIMIT ? OFFSET ?; + +-- 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 ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +); + +-- 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 = ?; + +-- name: DeleteProduct :exec +DELETE FROM products +WHERE id = ?; + +-- name: SearchProductsByTitle :many +SELECT * FROM products +WHERE title LIKE CONCAT('%', ?, '%') +ORDER BY title +LIMIT ? OFFSET ?; + +-- name: GetProductsByCategory :many +SELECT * FROM products +WHERE product_category_id = ? +ORDER BY title +LIMIT ? OFFSET ?; + +-- name: GetProductByItemCode :one +SELECT * FROM products +WHERE item_code = ? +LIMIT 1; \ No newline at end of file diff --git a/go-app/sql/queries/purchase_orders.sql b/go-app/sql/queries/purchase_orders.sql new file mode 100644 index 00000000..bb307793 --- /dev/null +++ b/go-app/sql/queries/purchase_orders.sql @@ -0,0 +1,60 @@ +-- name: GetPurchaseOrder :one +SELECT * FROM purchase_orders +WHERE id = ? LIMIT 1; + +-- name: ListPurchaseOrders :many +SELECT * FROM purchase_orders +ORDER BY issue_date DESC +LIMIT ? OFFSET ?; + +-- 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 ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? +); + +-- 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 = ?; + +-- name: DeletePurchaseOrder :exec +DELETE FROM purchase_orders +WHERE id = ?; + +-- name: GetPurchaseOrdersByPrinciple :many +SELECT * FROM purchase_orders +WHERE principle_id = ? +ORDER BY issue_date DESC +LIMIT ? OFFSET ?; + +-- name: GetPurchaseOrderRevisions :many +SELECT * FROM purchase_orders +WHERE parent_purchase_order_id = ? +ORDER BY id DESC; + +-- name: SearchPurchaseOrdersByTitle :many +SELECT * FROM purchase_orders +WHERE title LIKE CONCAT('%', ?, '%') +ORDER BY issue_date DESC +LIMIT ? OFFSET ?; \ No newline at end of file diff --git a/go-app/sql/schema/001_customers.sql b/go-app/sql/schema/001_customers.sql new file mode 100644 index 00000000..aad28d11 --- /dev/null +++ b/go-app/sql/schema/001_customers.sql @@ -0,0 +1,15 @@ +-- Customers table schema (extracted from existing database) +CREATE TABLE IF NOT EXISTS `customers` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(400) NOT NULL COMMENT 'Company Name', + `trading_name` varchar(400) NOT NULL DEFAULT '', + `abn` varchar(255) DEFAULT NULL, + `created` datetime NOT NULL, + `notes` text NOT NULL, + `discount_pricing_policies` text NOT NULL, + `payment_terms` varchar(255) NOT NULL, + `customer_category_id` int(11) NOT NULL, + `url` varchar(300) NOT NULL, + `country_id` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/go-app/sql/schema/002_products.sql b/go-app/sql/schema/002_products.sql new file mode 100644 index 00000000..74a88054 --- /dev/null +++ b/go-app/sql/schema/002_products.sql @@ -0,0 +1,15 @@ +-- Products table schema (extracted from existing database) +CREATE TABLE IF NOT EXISTS `products` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `principle_id` int(11) NOT NULL COMMENT 'Principle FK', + `product_category_id` int(11) NOT NULL, + `title` varchar(255) NOT NULL COMMENT 'This must match the Title in the Excel Costing File', + `description` text NOT NULL, + `model_number` varchar(255) DEFAULT NULL COMMENT 'Part or model number principle uses to identify this product', + `model_number_format` varchar(255) DEFAULT NULL COMMENT '%1% - first item, %2% , second item etc ', + `notes` text DEFAULT NULL COMMENT 'Any notes about this product. Note displayed on quotes', + `stock` tinyint(1) NOT NULL COMMENT 'Stock or Ident', + `item_code` varchar(255) NOT NULL, + `item_description` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/go-app/sql/schema/003_purchase_orders.sql b/go-app/sql/schema/003_purchase_orders.sql new file mode 100644 index 00000000..2c3d5fed --- /dev/null +++ b/go-app/sql/schema/003_purchase_orders.sql @@ -0,0 +1,21 @@ +-- Purchase Orders table schema (extracted from existing database) +CREATE TABLE IF NOT EXISTS `purchase_orders` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `issue_date` date NOT NULL, + `dispatch_date` date NOT NULL, + `date_arrived` date NOT NULL, + `title` varchar(255) NOT NULL COMMENT 'CMC PONumber', + `principle_id` int(11) NOT NULL, + `principle_reference` varchar(255) NOT NULL, + `document_id` int(11) NOT NULL, + `currency_id` int(11) DEFAULT NULL, + `ordered_from` text NOT NULL, + `description` text NOT NULL, + `dispatch_by` varchar(255) NOT NULL, + `deliver_to` text NOT NULL, + `shipping_instructions` text NOT NULL, + `jobs_text` varchar(512) NOT NULL, + `freight_forwarder_text` text NOT NULL DEFAULT '', + `parent_purchase_order_id` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/go-app/sql/schema/004_users.sql b/go-app/sql/schema/004_users.sql new file mode 100644 index 00000000..5eab0266 --- /dev/null +++ b/go-app/sql/schema/004_users.sql @@ -0,0 +1,26 @@ +-- Users table schema +CREATE TABLE `users` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `principle_id` int(11) NOT NULL, + `customer_id` int(11) NOT NULL, + `type` enum('principle','contact','user') NOT NULL, + `access_level` enum('admin','manager','user','none') NOT NULL DEFAULT 'none', + `username` char(50) NOT NULL, + `password` char(60) NOT NULL, + `first_name` varchar(255) NOT NULL, + `last_name` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `job_title` varchar(255) NOT NULL, + `phone` varchar(255) NOT NULL, + `mobile` varchar(255) NOT NULL, + `fax` varchar(255) NOT NULL, + `phone_extension` varchar(255) NOT NULL, + `direct_phone` varchar(255) NOT NULL, + `notes` text NOT NULL, + `by_vault` tinyint(1) NOT NULL COMMENT 'Added by Vault. May or may not be a real person.', + `blacklisted` tinyint(1) NOT NULL COMMENT 'Disregard emails from this address in future.', + `enabled` tinyint(1) NOT NULL DEFAULT 0, + `archived` tinyint(1) DEFAULT 0, + `primary_contact` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=48276 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; \ No newline at end of file diff --git a/go-app/sql/schema/enquiries.sql b/go-app/sql/schema/enquiries.sql new file mode 100644 index 00000000..2df83c31 --- /dev/null +++ b/go-app/sql/schema/enquiries.sql @@ -0,0 +1,57 @@ +-- Enquiries table schema +CREATE TABLE `enquiries` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `submitted` date DEFAULT NULL, + `title` varchar(255) NOT NULL COMMENT 'enquirynumber', + `user_id` int(11) NOT NULL, + `customer_id` int(11) NOT NULL, + `contact_id` int(11) NOT NULL, + `contact_user_id` int(11) NOT NULL, + `state_id` int(11) NOT NULL, + `country_id` int(11) NOT NULL, + `principle_id` int(11) NOT NULL, + `status_id` int(11) NOT NULL, + `comments` text NOT NULL, + `principle_code` int(3) NOT NULL COMMENT 'Numeric Principle Code', + `gst` tinyint(1) NOT NULL COMMENT 'GST applicable on this enquiry', + `billing_address_id` int(11) DEFAULT NULL, + `shipping_address_id` int(11) DEFAULT NULL, + `posted` tinyint(1) NOT NULL COMMENT 'has the enquired been posted', + `email_count` int(11) NOT NULL DEFAULT 0, + `invoice_count` int(11) NOT NULL DEFAULT 0, + `job_count` int(11) NOT NULL DEFAULT 0, + `quote_count` int(11) NOT NULL DEFAULT 0, + `archived` tinyint(4) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=MyISAM AUTO_INCREMENT=17101 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +-- Related tables for foreign key references +CREATE TABLE `statuses` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `color` varchar(7) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE `principles` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `short_name` varchar(50) DEFAULT NULL, + `code` int(3) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE `states` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `shortform` varchar(20) DEFAULT NULL, + `enqform` varchar(10) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; + +CREATE TABLE `countries` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci; \ No newline at end of file diff --git a/go-app/sqlc.yaml b/go-app/sqlc.yaml new file mode 100644 index 00000000..a5744cfc --- /dev/null +++ b/go-app/sqlc.yaml @@ -0,0 +1,14 @@ +version: "2" +sql: + - engine: "mysql" + queries: "sql/queries/" + schema: "sql/schema/" + gen: + go: + package: "db" + out: "internal/cmc/db" + emit_json_tags: true + emit_prepared_queries: false + emit_interface: true + emit_exact_table_names: false + emit_empty_slices: true \ No newline at end of file diff --git a/go-app/static/css/style.css b/go-app/static/css/style.css new file mode 100644 index 00000000..d5d7e6d7 --- /dev/null +++ b/go-app/static/css/style.css @@ -0,0 +1,109 @@ +/* Custom styles for CMC Sales */ + +/* Loading spinner */ +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: inline; +} + +.htmx-request.htmx-indicator { + display: inline; +} + +/* Smooth transitions */ +.htmx-swapping { + opacity: 0; + transition: opacity 200ms ease-out; +} + +.htmx-settling { + opacity: 1; + transition: opacity 200ms ease-in; +} + +/* Table hover effects */ +.table.is-hoverable tbody tr:hover { + background-color: #f5f5f5; + cursor: pointer; +} + +/* Form improvements */ +.field:not(:last-child) { + margin-bottom: 1.5rem; +} + +/* Notification animations */ +.notification { + animation: slideIn 300ms ease-out; +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Breadcrumb improvements */ +.breadcrumb { + margin-bottom: 2rem; +} + +/* Card hover effects */ +.card:hover { + box-shadow: 0 8px 16px rgba(10,10,10,.1); + transition: box-shadow 200ms ease; +} + +/* Button loading state */ +.button.is-loading::after { + border-color: transparent transparent #fff #fff !important; +} + +/* Responsive improvements */ +@media screen and (max-width: 768px) { + .level-left, + .level-right { + display: flex; + flex-direction: column; + align-items: flex-start; + } + + .level-right { + margin-top: 1rem; + } +} + +/* Footer stick to bottom */ +body { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.section { + flex: 1; +} + +/* Search box improvements */ +.control.has-icons-left .input:focus ~ .icon { + color: #3273dc; +} + +/* Table action buttons */ +.table td .buttons { + margin-bottom: 0; +} + +/* Error states */ +.input.is-danger:focus { + border-color: #ff3860; + box-shadow: 0 0 0 0.125em rgba(255,56,96,.25); +} \ No newline at end of file diff --git a/go-app/static/js/app.js b/go-app/static/js/app.js new file mode 100644 index 00000000..517194da --- /dev/null +++ b/go-app/static/js/app.js @@ -0,0 +1,112 @@ +// CMC Sales Application JavaScript + +// Initialize HTMX events +document.body.addEventListener('htmx:configRequest', (event) => { + // Add CSRF token if needed in the future +}); + +// Handle HTMX errors +document.body.addEventListener('htmx:responseError', (event) => { + console.error('HTMX request failed:', event.detail); + showNotification('An error occurred. Please try again.', 'danger'); +}); + +// Handle form validation +document.body.addEventListener('htmx:validation:validate', (event) => { + const element = event.detail.elt; + if (element.tagName === 'FORM') { + // Clear previous errors + element.querySelectorAll('.is-danger').forEach(el => { + el.classList.remove('is-danger'); + }); + element.querySelectorAll('.help.is-danger').forEach(el => { + el.remove(); + }); + + // Validate required fields + let isValid = true; + element.querySelectorAll('[required]').forEach(input => { + if (!input.value.trim()) { + input.classList.add('is-danger'); + const help = document.createElement('p'); + help.className = 'help is-danger'; + help.textContent = 'This field is required'; + input.parentElement.appendChild(help); + isValid = false; + } + }); + + if (!isValid) { + event.preventDefault(); + } + } +}); + +// Show notification +function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification is-${type}`; + notification.innerHTML = ` + + ${message} + `; + + // Insert at the top of the main content + const container = document.querySelector('.section .container'); + container.insertBefore(notification, container.firstChild); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + notification.remove(); + }, 5000); + + // Handle delete button + notification.querySelector('.delete').addEventListener('click', () => { + notification.remove(); + }); +} + +// Handle delete confirmations +document.body.addEventListener('htmx:confirm', (event) => { + if (event.detail.question === 'Are you sure you want to delete this customer?') { + event.preventDefault(); + + // Use Bulma modal or native confirm + if (confirm(event.detail.question)) { + event.detail.issueRequest(); + } + } +}); + +// Handle successful deletes +document.body.addEventListener('htmx:afterRequest', (event) => { + if (event.detail.xhr.status === 204 && event.detail.verb === 'delete') { + showNotification('Item deleted successfully', 'success'); + } +}); + +// Format dates +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleDateString('en-AU', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} + +// Initialize tooltips if using Bulma extensions +document.addEventListener('DOMContentLoaded', () => { + // Add any initialization code here +}); + +// Handle search clear button +document.addEventListener('click', (event) => { + if (event.target.matches('[data-clear-search]')) { + const searchInput = document.querySelector('#customer-search'); + if (searchInput) { + searchInput.value = ''; + searchInput.dispatchEvent(new Event('keyup')); + } + } +}); \ No newline at end of file diff --git a/go-app/templates/customers/form.html b/go-app/templates/customers/form.html new file mode 100644 index 00000000..acdd70ef --- /dev/null +++ b/go-app/templates/customers/form.html @@ -0,0 +1,100 @@ +{{define "title"}}{{if .Customer.ID}}Edit{{else}}New{{end}} Customer - CMC Sales{{end}} + +{{define "content"}} +
+
+

{{if .Customer.ID}}Edit{{else}}New{{end}} Customer

+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ Cancel +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/customers/index.html b/go-app/templates/customers/index.html new file mode 100644 index 00000000..34aef47a --- /dev/null +++ b/go-app/templates/customers/index.html @@ -0,0 +1,47 @@ +{{define "title"}}Customers - CMC Sales{{end}} + +{{define "content"}} +
+
+
+

Customers

+
+
+
+ +
+
+ + +
+
+
+ + + + +
+
+ +
+
+
+ + +
+ {{template "customer-table" .}} +
+{{end}} \ No newline at end of file diff --git a/go-app/templates/customers/show.html b/go-app/templates/customers/show.html new file mode 100644 index 00000000..336a281b --- /dev/null +++ b/go-app/templates/customers/show.html @@ -0,0 +1,121 @@ +{{define "title"}}{{.Customer.Name}} - CMC Sales{{end}} + +{{define "content"}} + + +
+
+
+

{{.Customer.Name}}

+
+
+
+ +
+
+ +
+
+
+

Customer Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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}}
+
+ + {{if .Customer.Notes}} +
+

Notes

+
+

{{.Customer.Notes}}

+
+
+ {{end}} + + {{if .Customer.DiscountPricingPolicies}} +
+

Discount Pricing Policies

+
+

{{.Customer.DiscountPricingPolicies}}

+
+
+ {{end}} +
+ +
+ + +
+

Recent Activity

+
+
+ + + +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/customers/table.html b/go-app/templates/customers/table.html new file mode 100644 index 00000000..84c1b0db --- /dev/null +++ b/go-app/templates/customers/table.html @@ -0,0 +1,68 @@ +{{define "customer-table"}} +
+ + + + + + + + + + + + + {{range .Customers}} + + + + + + + + + {{else}} + + + + {{end}} + +
IDNameTrading NameABNPayment TermsActions
{{.ID}} + {{.Name}} + {{.TradingName}}{{.Abn}}{{.PaymentTerms}} +
+ + + + + + +
+
+

No customers found

+
+
+ + +{{if .Customers}} + +{{end}} +{{end}} \ No newline at end of file diff --git a/go-app/templates/enquiries/form.html b/go-app/templates/enquiries/form.html new file mode 100644 index 00000000..7773560e --- /dev/null +++ b/go-app/templates/enquiries/form.html @@ -0,0 +1,270 @@ +{{define "title"}}{{if .Enquiry.ID}}Edit{{else}}New{{end}} Enquiry - CMC Sales{{end}} + +{{define "content"}} +
+
+
+
+

+ {{if .Enquiry.ID}} + Edit Enquiry: {{.Enquiry.Title}} + {{else}} + New Enquiry + {{end}} +

+
+
+
+ + +
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+ + +
+ +
+
+ +
+
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + {{if not .Enquiry.ID}} +
+
+ +
+
+ {{end}} + + +
+
+ +
+
+ Cancel +
+
+ + +
+
+
+
+
+
+{{end}} + +{{define "scripts"}} + +{{end}} \ No newline at end of file diff --git a/go-app/templates/enquiries/index.html b/go-app/templates/enquiries/index.html new file mode 100644 index 00000000..596572a8 --- /dev/null +++ b/go-app/templates/enquiries/index.html @@ -0,0 +1,159 @@ +{{define "title"}}Enquiries - CMC Sales{{end}} + +{{define "head"}} + +{{end}} + +{{define "content"}} +
+
+
+

Enquiries

+
+
+
+ +
+
+ + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+

+ Page {{.Page}} of pages, showing {{len .Enquiries}} Enquiries +

+
+ + + + + +
+ {{template "enquiry-table" .}} +
+ + + +{{end}} + +{{define "scripts"}} + +{{end}} \ No newline at end of file diff --git a/go-app/templates/enquiries/show.html b/go-app/templates/enquiries/show.html new file mode 100644 index 00000000..0e85741f --- /dev/null +++ b/go-app/templates/enquiries/show.html @@ -0,0 +1,326 @@ +{{define "title"}}Enquiry: {{.Enquiry.Title}} - CMC Sales{{end}} + +{{define "content"}} + +
+
+
+

Enquiry: {{.Enquiry.Title}}

+
+
+
+ +
+
+ +
+ +
+
+
+

Enquiry Details

+
+
+
+
+
+ + + + + + + + + + + {{if .Enquiry.Submitted.Valid}} + + + + + {{end}} + + + + + + + + + + + + + + + + + +
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}} +
+
+
+ + {{if .Enquiry.Comments}} +
+ +
+

{{.Enquiry.Comments}}

+
+
+ {{end}} +
+
+
+ + +
+
+

Related Documents

+
+
+
+
+
+
Quick Stats
+
+ {{.Enquiry.EmailCount}} Emails + {{.Enquiry.QuoteCount}} Quotes + {{.Enquiry.InvoiceCount}} Invoices + {{.Enquiry.JobCount}} Jobs +
+
+
+ + +
+

Note: Related quotes, invoices, jobs, and email correspondence would be displayed here. These require additional database queries and relationships to be implemented.

+
+
+
+
+
+ + +
+ +
+
+

Customer

+
+
+
+ {{if .Enquiry.CustomerName.Valid}} +
+ {{.Enquiry.CustomerName.String}} +
+ {{end}} + + + + + + + + {{if .Enquiry.ContactEmail.Valid}} + + + + + {{end}} + + + + + +
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)

+
+
+
+ {{if .Enquiry.PrincipleName.Valid}} +
+ + {{if .Enquiry.PrincipleShortName.Valid}} + {{.Enquiry.PrincipleShortName.String}} + {{else}} + {{.Enquiry.PrincipleName.String}} + {{end}} + +
+ {{if and .Enquiry.PrincipleShortName.Valid .Enquiry.PrincipleName.Valid}} +

{{.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

+
+
+
+
+ {{if not .Enquiry.Submitted.Valid}} + + {{end}} + + {{if .Enquiry.Archived}} + + {{else}} + + {{end}} +
+
+
+
+
+
+{{end}} + +{{define "scripts"}} + +{{end}} \ No newline at end of file diff --git a/go-app/templates/enquiries/table.html b/go-app/templates/enquiries/table.html new file mode 100644 index 00000000..ce3d8f0d --- /dev/null +++ b/go-app/templates/enquiries/table.html @@ -0,0 +1,192 @@ +{{define "enquiry-table"}} +
+ + + + + + + + + + + + + + + + + + {{range .Enquiries}} + {{$rowClass := ""}} + {{$nameClass := ""}} + + {{/* Set row color based on status - matching CakePHP logic */}} + {{if eq .StatusID 3}} + {{$rowClass = "jobwon"}} + {{else if eq .StatusID 4}} + {{$rowClass = "joblost"}} + {{else if eq .StatusID 8}} + {{$rowClass = "joblost"}} + {{else if eq .StatusID 9}} + {{$rowClass = "joblost"}} + {{else if eq .StatusID 10}} + {{$rowClass = "joblost"}} + {{else if eq .StatusID 6}} + {{$rowClass = "information"}} + {{else if eq .StatusID 11}} + {{$rowClass = "informationsent"}} + {{else if eq .StatusID 5}} + {{$rowClass = "quoted"}} + {{else if eq .StatusID 1}} + {{$rowClass = "requestforquote"}} + {{end}} + + {{/* Set name class based on posted status */}} + {{if .Posted}} + {{$nameClass = "posted"}} + {{else}} + {{$nameClass = "notposted"}} + {{end}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{else}} + + + + {{end}} + +
UserDatePrincipleEnquiry NumberCustomerContactEmailPhone NoStatusCommentsActions
+ {{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}} + +
+ View + Edit + + {{if .Archived}} + + {{else}} + + {{end}} +
+
+ No enquiries found. +
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/index.html b/go-app/templates/index.html new file mode 100644 index 00000000..06408571 --- /dev/null +++ b/go-app/templates/index.html @@ -0,0 +1,120 @@ +{{define "title"}}Dashboard - CMC Sales{{end}} + +{{define "content"}} +
+
+

CMC Sales Dashboard

+

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

+
+
+ +
+
+
+
+ + +
+

Recent Activity

+
+
+ + + +
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/layouts/base.html b/go-app/templates/layouts/base.html new file mode 100644 index 00000000..a9e75312 --- /dev/null +++ b/go-app/templates/layouts/base.html @@ -0,0 +1,106 @@ +{{define "base"}} + + + + + + {{block "title" .}}CMC Sales{{end}} + + + + + + + + + + + + + + {{block "head" .}}{{end}} + + + + + + +
+
+ {{block "content" .}}{{end}} +
+
+ + + + + + + + + + + {{block "scripts" .}}{{end}} + + +{{end}} \ No newline at end of file diff --git a/go-app/templates/partials/notification.html b/go-app/templates/partials/notification.html new file mode 100644 index 00000000..5e8000ba --- /dev/null +++ b/go-app/templates/partials/notification.html @@ -0,0 +1,6 @@ +{{define "notification"}} +
+ + {{.Message}} +
+{{end}} \ No newline at end of file diff --git a/go-app/templates/products/form.html b/go-app/templates/products/form.html new file mode 100644 index 00000000..9dba3145 --- /dev/null +++ b/go-app/templates/products/form.html @@ -0,0 +1,134 @@ +{{define "title"}}{{if .Product.ID}}Edit{{else}}New{{end}} Product - CMC Sales{{end}} + +{{define "content"}} +
+
+

{{if .Product.ID}}Edit{{else}}New{{end}} Product

+ +
+ +
+ +
+ +
+

This must match the Title in the Excel Costing File

+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+

Notes displayed on quotes

+
+ +
+ +
+
+ +
+
+ Cancel +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/products/index.html b/go-app/templates/products/index.html new file mode 100644 index 00000000..8c9e23b3 --- /dev/null +++ b/go-app/templates/products/index.html @@ -0,0 +1,67 @@ +{{define "title"}}Products - CMC Sales{{end}} + +{{define "content"}} +
+
+
+

Products

+
+
+
+ +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+ {{template "product-table" .}} +
+{{end}} \ No newline at end of file diff --git a/go-app/templates/products/show.html b/go-app/templates/products/show.html new file mode 100644 index 00000000..e01b42d1 --- /dev/null +++ b/go-app/templates/products/show.html @@ -0,0 +1,125 @@ +{{define "title"}}{{.Product.Title}} - CMC Sales{{end}} + +{{define "content"}} + + +
+
+
+

{{.Product.Title}}

+
+
+
+ +
+
+ +
+
+
+

Product Information

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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}}
+
+ + {{if .Product.Description}} +
+

Description

+
+

{{.Product.Description}}

+
+
+ {{end}} + + {{if .Product.Notes.Valid}} +
+

Notes

+
+

{{.Product.Notes.String}}

+
+
+ {{end}} +
+ +
+ + +
+

Product Usage

+
+
+ + + +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/products/table.html b/go-app/templates/products/table.html new file mode 100644 index 00000000..7d58ac93 --- /dev/null +++ b/go-app/templates/products/table.html @@ -0,0 +1,78 @@ +{{define "product-table"}} +
+ + + + + + + + + + + + + + {{range .Products}} + + + + + + + + + + {{else}} + + + + {{end}} + +
IDTitleItem CodeDescriptionModel NumberStock TypeActions
{{.ID}} + {{.Title}} + + {{.ItemCode}} + {{truncate .Description 50}}{{if .ModelNumber.Valid}}{{.ModelNumber.String}}{{else}}-{{end}} + {{if .Stock}} + Stock + {{else}} + Indent + {{end}} + +
+ + + + + + +
+
+

No products found

+
+
+ + +{{if .Products}} + +{{end}} +{{end}} \ No newline at end of file diff --git a/go-app/templates/purchase-orders/form.html b/go-app/templates/purchase-orders/form.html new file mode 100644 index 00000000..a2ded9a6 --- /dev/null +++ b/go-app/templates/purchase-orders/form.html @@ -0,0 +1,177 @@ +{{define "title"}}{{if .PurchaseOrder.ID}}Edit{{else}}New{{end}} Purchase Order - CMC Sales{{end}} + +{{define "content"}} +
+
+

{{if .PurchaseOrder.ID}}Edit{{else}}New{{end}} Purchase Order

+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+ +
+ +
+
+ +
+
+ Cancel +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/purchase-orders/index.html b/go-app/templates/purchase-orders/index.html new file mode 100644 index 00000000..852385d9 --- /dev/null +++ b/go-app/templates/purchase-orders/index.html @@ -0,0 +1,68 @@ +{{define "title"}}Purchase Orders - CMC Sales{{end}} + +{{define "content"}} +
+
+
+

Purchase Orders

+
+
+
+ +
+
+ + +
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + +
+ {{template "purchase-order-table" .}} +
+{{end}} \ No newline at end of file diff --git a/go-app/templates/purchase-orders/show.html b/go-app/templates/purchase-orders/show.html new file mode 100644 index 00000000..e2b2bb1d --- /dev/null +++ b/go-app/templates/purchase-orders/show.html @@ -0,0 +1,159 @@ +{{define "title"}}PO {{.PurchaseOrder.Title}} - CMC Sales{{end}} + +{{define "content"}} + + +
+
+
+

Purchase Order {{.PurchaseOrder.Title}}

+
+
+
+ +
+
+ +
+
+
+

Order Information

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

Ordered From

+
+
{{.PurchaseOrder.OrderedFrom}}
+
+
+ +
+

Deliver To

+
+
{{.PurchaseOrder.DeliverTo}}
+
+
+ + {{if .PurchaseOrder.Description}} +
+

Description

+
+

{{.PurchaseOrder.Description}}

+
+
+ {{end}} + + {{if .PurchaseOrder.ShippingInstructions}} +
+

Shipping Instructions

+
+

{{.PurchaseOrder.ShippingInstructions}}

+
+
+ {{end}} +
+ +
+
+

Status

+
+ {{if .PurchaseOrder.DateArrived}} + Arrived + {{else if .PurchaseOrder.DispatchDate}} + Dispatched + {{else}} + Pending + {{end}} +
+
+ + + + {{if .PurchaseOrder.JobsText}} +
+

Related Jobs

+
+

{{.PurchaseOrder.JobsText}}

+
+
+ {{end}} + + {{if .PurchaseOrder.FreightForwarderText}} +
+

Freight Forwarder

+
+

{{.PurchaseOrder.FreightForwarderText}}

+
+
+ {{end}} +
+
+{{end}} \ No newline at end of file diff --git a/go-app/templates/purchase-orders/table.html b/go-app/templates/purchase-orders/table.html new file mode 100644 index 00000000..e5cc8e6c --- /dev/null +++ b/go-app/templates/purchase-orders/table.html @@ -0,0 +1,78 @@ +{{define "purchase-order-table"}} +
+ + + + + + + + + + + + + + {{range .PurchaseOrders}} + + + + + + + + + + {{else}} + + + + {{end}} + +
PO NumberIssue DateDispatch DateOrdered FromDescriptionStatusActions
+ {{.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

+
+
+ + +{{if .PurchaseOrders}} + +{{end}} +{{end}} \ No newline at end of file