diff --git a/Dockerfile.go b/Dockerfile.go index 2e59e2ec..9a787c48 100644 --- a/Dockerfile.go +++ b/Dockerfile.go @@ -1,50 +1,23 @@ -# Build stage -FROM golang:1.23-alpine AS builder +# Dev Dockerfile for Go hot reload with Air and sqlc +FROM golang:1.24.0 -# 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 +# Install Air for hot reload +RUN go install github.com/air-verse/air@latest +# Install sqlc for SQL code generation +RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest # 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 Air config +COPY go-app/.air.toml .air.toml COPY go-app/.env.example .env -# Expose port EXPOSE 8080 -# Run the application -CMD ["./server"] \ No newline at end of file +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/conf/nginx-site.conf b/conf/nginx-site.conf index 4804ec30..0940883b 100644 --- a/conf/nginx-site.conf +++ b/conf/nginx-site.conf @@ -2,6 +2,14 @@ server { server_name cmclocal; auth_basic_user_file /etc/nginx/userpasswd; auth_basic "Restricted"; + location /go/ { + proxy_pass http://cmc-go:8080; + 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; + } location / { proxy_pass http://cmc-php:80; proxy_read_timeout 300s; diff --git a/docker-compose.yml b/docker-compose.yml index 696b7a40..cee90e0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,25 +75,12 @@ services: ports: - "8080:8080" volumes: - - ./app/webroot/pdf:/root/webroot/pdf + - ./go-app:/app + - ./go-app/.air.toml:/root/.air.toml + - ./go-app/.env.example:/root/.env networks: - cmc-network restart: unless-stopped - develop: - watch: - - action: rebuild - path: ./go-app - ignore: - - ./go-app/bin - - ./go-app/.env - - ./go-app/tmp - - "**/.*" # Ignore hidden files - - action: sync - path: ./go-app/templates - target: /app/templates - - action: sync - path: ./go-app/static - target: /app/static volumes: db_data: diff --git a/go-app/.air.toml b/go-app/.air.toml new file mode 100644 index 00000000..b8689a6b --- /dev/null +++ b/go-app/.air.toml @@ -0,0 +1,25 @@ +# Air configuration for Go hot reload +root = "./" +cmd = ["air"] + +[build] + cmd = "go build -o server cmd/server/main.go" + bin = "server" + include = ["cmd", "internal", "go.mod", "go.sum"] + exclude = ["bin", "tmp", ".env"] + delay = 1000 + log = "stdout" + kill_on_error = true + +color = true + +[[watch]] + path = "templates" + reload = true +[[watch]] + path = "static" + reload = true +[[watch]] + path = "tmp" + reload = false + mkdir = true diff --git a/go-app/cmd/server/main.go b/go-app/cmd/server/main.go index 954dc444..184ad43c 100644 --- a/go-app/cmd/server/main.go +++ b/go-app/cmd/server/main.go @@ -52,227 +52,200 @@ func main() { 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) - documentHandler := handlers.NewDocumentHandler(queries) + // Only pageHandler is needed pageHandler := handlers.NewPageHandler(queries, tmpl) - addressHandler := handlers.NewAddressHandler(queries) - attachmentHandler := handlers.NewAttachmentHandler(queries) - countryHandler := handlers.NewCountryHandler(queries) - statusHandler := handlers.NewStatusHandler(queries) - lineItemHandler := handlers.NewLineItemHandler(queries) // Setup routes r := mux.NewRouter() + goRouter := r.PathPrefix("/go").Subrouter() // Static files - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + goRouter.PathPrefix("/static/").Handler(http.StripPrefix("/go/static/", http.FileServer(http.Dir("static")))) - // PDF files (matching CakePHP structure) - r.PathPrefix("/pdf/").Handler(http.StripPrefix("/pdf/", http.FileServer(http.Dir("webroot/pdf")))) + // PDF files + goRouter.PathPrefix("/pdf/").Handler(http.StripPrefix("/go/pdf/", http.FileServer(http.Dir("webroot/pdf")))) - // API routes - api := r.PathPrefix("/api/v1").Subrouter() + goRouter.HandleFunc("/quotes", pageHandler.QuotesView).Methods("GET") - // 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") + // The following routes are currently disabled: + /* + // API routes + api := r.PathPrefix("/api/v1").Subrouter() + 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") + // Document routes + api.HandleFunc("/documents", documentHandler.List).Methods("GET") + api.HandleFunc("/documents", documentHandler.Create).Methods("POST") + api.HandleFunc("/documents/{id}", documentHandler.Get).Methods("GET") + api.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT") + api.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT") + api.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT") + api.HandleFunc("/documents/search", documentHandler.Search).Methods("GET") + // Address routes + api.HandleFunc("/addresses", addressHandler.List).Methods("GET") + api.HandleFunc("/addresses", addressHandler.Create).Methods("POST") + api.HandleFunc("/addresses/{id}", addressHandler.Get).Methods("GET") + api.HandleFunc("/addresses/{id}", addressHandler.Update).Methods("PUT") + api.HandleFunc("/addresses/{id}", addressHandler.Delete).Methods("DELETE") + api.HandleFunc("/addresses/customer/{customerID}", addressHandler.CustomerAddresses).Methods("GET") + // Attachment routes + api.HandleFunc("/attachments", attachmentHandler.List).Methods("GET") + api.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET") + api.HandleFunc("/attachments", attachmentHandler.Create).Methods("POST") + api.HandleFunc("/attachments/{id}", attachmentHandler.Get).Methods("GET") + api.HandleFunc("/attachments/{id}", attachmentHandler.Update).Methods("PUT") + api.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE") + // Country routes + api.HandleFunc("/countries", countryHandler.List).Methods("GET") + api.HandleFunc("/countries", countryHandler.Create).Methods("POST") + api.HandleFunc("/countries/{id}", countryHandler.Get).Methods("GET") + api.HandleFunc("/countries/{id}", countryHandler.Update).Methods("PUT") + api.HandleFunc("/countries/{id}", countryHandler.Delete).Methods("DELETE") + api.HandleFunc("/countries/complete", countryHandler.CompleteCountry).Methods("GET") + // Status routes + api.HandleFunc("/statuses", statusHandler.List).Methods("GET") + api.HandleFunc("/statuses", statusHandler.Create).Methods("POST") + api.HandleFunc("/statuses/{id}", statusHandler.Get).Methods("GET") + api.HandleFunc("/statuses/{id}", statusHandler.Update).Methods("PUT") + api.HandleFunc("/statuses/{id}", statusHandler.Delete).Methods("DELETE") + api.HandleFunc("/statuses/json/{selectedId}", statusHandler.JsonList).Methods("GET") + // Line Item routes + api.HandleFunc("/line-items", lineItemHandler.List).Methods("GET") + api.HandleFunc("/line-items", lineItemHandler.Create).Methods("POST") + api.HandleFunc("/line-items/{id}", lineItemHandler.Get).Methods("GET") + api.HandleFunc("/line-items/{id}", lineItemHandler.Update).Methods("PUT") + api.HandleFunc("/line-items/{id}", lineItemHandler.Delete).Methods("DELETE") + api.HandleFunc("/line-items/document/{documentID}/table", lineItemHandler.GetTable).Methods("GET") + // Health check + api.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok"}`)) + }).Methods("GET") + // Recent activity endpoint + r.HandleFunc("/api/recent-activity", documentHandler.GetRecentActivity).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") + // Document pages + r.HandleFunc("/documents", pageHandler.DocumentsIndex).Methods("GET") + r.HandleFunc("/documents/search", pageHandler.DocumentsSearch).Methods("GET") + r.HandleFunc("/documents/view/{id}", pageHandler.DocumentsView).Methods("GET") + r.HandleFunc("/documents/{id}", pageHandler.DocumentsShow).Methods("GET") + r.HandleFunc("/documents/pdf/{id}", documentHandler.GeneratePDF).Methods("GET") + // Address routes (matching CakePHP) + r.HandleFunc("/addresses", addressHandler.List).Methods("GET") + r.HandleFunc("/addresses/view/{id}", addressHandler.Get).Methods("GET") + r.HandleFunc("/addresses/add/{customerid}", addressHandler.Create).Methods("GET", "POST") + r.HandleFunc("/addresses/add_another/{increment}", addressHandler.AddAnother).Methods("GET") + r.HandleFunc("/addresses/remove_another/{increment}", addressHandler.RemoveAnother).Methods("GET") + r.HandleFunc("/addresses/edit/{id}", addressHandler.Update).Methods("GET", "POST") + r.HandleFunc("/addresses/customer_addresses/{customerID}", addressHandler.CustomerAddresses).Methods("GET") + // Attachment routes (matching CakePHP) + r.HandleFunc("/attachments", attachmentHandler.List).Methods("GET") + r.HandleFunc("/attachments/view/{id}", attachmentHandler.Get).Methods("GET") + r.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET") + r.HandleFunc("/attachments/add", attachmentHandler.Create).Methods("GET", "POST") + r.HandleFunc("/attachments/edit/{id}", attachmentHandler.Update).Methods("GET", "POST") + r.HandleFunc("/attachments/delete/{id}", attachmentHandler.Delete).Methods("POST") + // Country routes (matching CakePHP) + r.HandleFunc("/countries", countryHandler.List).Methods("GET") + r.HandleFunc("/countries/view/{id}", countryHandler.Get).Methods("GET") + r.HandleFunc("/countries/add", countryHandler.Create).Methods("GET", "POST") + r.HandleFunc("/countries/edit/{id}", countryHandler.Update).Methods("GET", "POST") + r.HandleFunc("/countries/delete/{id}", countryHandler.Delete).Methods("POST") + r.HandleFunc("/countries/complete_country", countryHandler.CompleteCountry).Methods("GET") + // Status routes (matching CakePHP) + r.HandleFunc("/statuses", statusHandler.List).Methods("GET") + r.HandleFunc("/statuses/view/{id}", statusHandler.Get).Methods("GET") + r.HandleFunc("/statuses/add", statusHandler.Create).Methods("GET", "POST") + r.HandleFunc("/statuses/edit/{id}", statusHandler.Update).Methods("GET", "POST") + r.HandleFunc("/statuses/delete/{id}", statusHandler.Delete).Methods("POST") + r.HandleFunc("/statuses/json_list/{selectedId}", statusHandler.JsonList).Methods("GET") + // Line Item routes (matching CakePHP) + r.HandleFunc("/line_items/ajax_add", lineItemHandler.AjaxAdd).Methods("POST") + r.HandleFunc("/line_items/ajax_edit", lineItemHandler.AjaxEdit).Methods("POST") + r.HandleFunc("/line_items/ajax_delete/{id}", lineItemHandler.AjaxDelete).Methods("POST") + r.HandleFunc("/line_items/get_table/{documentID}", lineItemHandler.GetTable).Methods("GET") + r.HandleFunc("/line_items/edit/{id}", lineItemHandler.Update).Methods("GET", "POST") + r.HandleFunc("/line_items/add/{documentID}", lineItemHandler.Create).Methods("GET", "POST") + // HTMX endpoints + r.HandleFunc("/customers", customerHandler.Create).Methods("POST") + r.HandleFunc("/customers/{id}", customerHandler.Update).Methods("PUT") + r.HandleFunc("/customers/{id}", customerHandler.Delete).Methods("DELETE") + r.HandleFunc("/products", productHandler.Create).Methods("POST") + r.HandleFunc("/products/{id}", productHandler.Update).Methods("PUT") + r.HandleFunc("/products/{id}", productHandler.Delete).Methods("DELETE") + r.HandleFunc("/purchase-orders", purchaseOrderHandler.Create).Methods("POST") + r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Update).Methods("PUT") + r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Delete).Methods("DELETE") + r.HandleFunc("/enquiries", enquiryHandler.Create).Methods("POST") + r.HandleFunc("/enquiries/{id}", enquiryHandler.Update).Methods("PUT") + r.HandleFunc("/enquiries/{id}", enquiryHandler.Delete).Methods("DELETE") + 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") + r.HandleFunc("/documents", documentHandler.Create).Methods("POST") + r.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT") + r.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT") + r.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT") + */ - // 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") - - // Document routes - api.HandleFunc("/documents", documentHandler.List).Methods("GET") - api.HandleFunc("/documents", documentHandler.Create).Methods("POST") - api.HandleFunc("/documents/{id}", documentHandler.Get).Methods("GET") - api.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT") - api.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT") - api.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT") - api.HandleFunc("/documents/search", documentHandler.Search).Methods("GET") - - // Address routes - api.HandleFunc("/addresses", addressHandler.List).Methods("GET") - api.HandleFunc("/addresses", addressHandler.Create).Methods("POST") - api.HandleFunc("/addresses/{id}", addressHandler.Get).Methods("GET") - api.HandleFunc("/addresses/{id}", addressHandler.Update).Methods("PUT") - api.HandleFunc("/addresses/{id}", addressHandler.Delete).Methods("DELETE") - api.HandleFunc("/addresses/customer/{customerID}", addressHandler.CustomerAddresses).Methods("GET") - - // Attachment routes - api.HandleFunc("/attachments", attachmentHandler.List).Methods("GET") - api.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET") - api.HandleFunc("/attachments", attachmentHandler.Create).Methods("POST") - api.HandleFunc("/attachments/{id}", attachmentHandler.Get).Methods("GET") - api.HandleFunc("/attachments/{id}", attachmentHandler.Update).Methods("PUT") - api.HandleFunc("/attachments/{id}", attachmentHandler.Delete).Methods("DELETE") - - // Country routes - api.HandleFunc("/countries", countryHandler.List).Methods("GET") - api.HandleFunc("/countries", countryHandler.Create).Methods("POST") - api.HandleFunc("/countries/{id}", countryHandler.Get).Methods("GET") - api.HandleFunc("/countries/{id}", countryHandler.Update).Methods("PUT") - api.HandleFunc("/countries/{id}", countryHandler.Delete).Methods("DELETE") - api.HandleFunc("/countries/complete", countryHandler.CompleteCountry).Methods("GET") - - // Status routes - api.HandleFunc("/statuses", statusHandler.List).Methods("GET") - api.HandleFunc("/statuses", statusHandler.Create).Methods("POST") - api.HandleFunc("/statuses/{id}", statusHandler.Get).Methods("GET") - api.HandleFunc("/statuses/{id}", statusHandler.Update).Methods("PUT") - api.HandleFunc("/statuses/{id}", statusHandler.Delete).Methods("DELETE") - api.HandleFunc("/statuses/json/{selectedId}", statusHandler.JsonList).Methods("GET") - - // Line Item routes - api.HandleFunc("/line-items", lineItemHandler.List).Methods("GET") - api.HandleFunc("/line-items", lineItemHandler.Create).Methods("POST") - api.HandleFunc("/line-items/{id}", lineItemHandler.Get).Methods("GET") - api.HandleFunc("/line-items/{id}", lineItemHandler.Update).Methods("PUT") - api.HandleFunc("/line-items/{id}", lineItemHandler.Delete).Methods("DELETE") - api.HandleFunc("/line-items/document/{documentID}/table", lineItemHandler.GetTable).Methods("GET") - - // Health check - api.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"status":"ok"}`)) - }).Methods("GET") - - // Recent activity endpoint - r.HandleFunc("/api/recent-activity", documentHandler.GetRecentActivity).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") - - // Document pages - r.HandleFunc("/documents", pageHandler.DocumentsIndex).Methods("GET") - r.HandleFunc("/documents/search", pageHandler.DocumentsSearch).Methods("GET") - r.HandleFunc("/documents/view/{id}", pageHandler.DocumentsView).Methods("GET") - r.HandleFunc("/documents/{id}", pageHandler.DocumentsShow).Methods("GET") - r.HandleFunc("/documents/pdf/{id}", documentHandler.GeneratePDF).Methods("GET") - - // Address routes (matching CakePHP) - r.HandleFunc("/addresses", addressHandler.List).Methods("GET") - r.HandleFunc("/addresses/view/{id}", addressHandler.Get).Methods("GET") - r.HandleFunc("/addresses/add/{customerid}", addressHandler.Create).Methods("GET", "POST") - r.HandleFunc("/addresses/add_another/{increment}", addressHandler.AddAnother).Methods("GET") - r.HandleFunc("/addresses/remove_another/{increment}", addressHandler.RemoveAnother).Methods("GET") - r.HandleFunc("/addresses/edit/{id}", addressHandler.Update).Methods("GET", "POST") - r.HandleFunc("/addresses/customer_addresses/{customerID}", addressHandler.CustomerAddresses).Methods("GET") - - // Attachment routes (matching CakePHP) - r.HandleFunc("/attachments", attachmentHandler.List).Methods("GET") - r.HandleFunc("/attachments/view/{id}", attachmentHandler.Get).Methods("GET") - r.HandleFunc("/attachments/archived", attachmentHandler.Archived).Methods("GET") - r.HandleFunc("/attachments/add", attachmentHandler.Create).Methods("GET", "POST") - r.HandleFunc("/attachments/edit/{id}", attachmentHandler.Update).Methods("GET", "POST") - r.HandleFunc("/attachments/delete/{id}", attachmentHandler.Delete).Methods("POST") - - // Country routes (matching CakePHP) - r.HandleFunc("/countries", countryHandler.List).Methods("GET") - r.HandleFunc("/countries/view/{id}", countryHandler.Get).Methods("GET") - r.HandleFunc("/countries/add", countryHandler.Create).Methods("GET", "POST") - r.HandleFunc("/countries/edit/{id}", countryHandler.Update).Methods("GET", "POST") - r.HandleFunc("/countries/delete/{id}", countryHandler.Delete).Methods("POST") - r.HandleFunc("/countries/complete_country", countryHandler.CompleteCountry).Methods("GET") - - // Status routes (matching CakePHP) - r.HandleFunc("/statuses", statusHandler.List).Methods("GET") - r.HandleFunc("/statuses/view/{id}", statusHandler.Get).Methods("GET") - r.HandleFunc("/statuses/add", statusHandler.Create).Methods("GET", "POST") - r.HandleFunc("/statuses/edit/{id}", statusHandler.Update).Methods("GET", "POST") - r.HandleFunc("/statuses/delete/{id}", statusHandler.Delete).Methods("POST") - r.HandleFunc("/statuses/json_list/{selectedId}", statusHandler.JsonList).Methods("GET") - - // Line Item routes (matching CakePHP) - r.HandleFunc("/line_items/ajax_add", lineItemHandler.AjaxAdd).Methods("POST") - r.HandleFunc("/line_items/ajax_edit", lineItemHandler.AjaxEdit).Methods("POST") - r.HandleFunc("/line_items/ajax_delete/{id}", lineItemHandler.AjaxDelete).Methods("POST") - r.HandleFunc("/line_items/get_table/{documentID}", lineItemHandler.GetTable).Methods("GET") - r.HandleFunc("/line_items/edit/{id}", lineItemHandler.Update).Methods("GET", "POST") - r.HandleFunc("/line_items/add/{documentID}", lineItemHandler.Create).Methods("GET", "POST") - - // HTMX endpoints - r.HandleFunc("/customers", customerHandler.Create).Methods("POST") - r.HandleFunc("/customers/{id}", customerHandler.Update).Methods("PUT") - r.HandleFunc("/customers/{id}", customerHandler.Delete).Methods("DELETE") - - r.HandleFunc("/products", productHandler.Create).Methods("POST") - r.HandleFunc("/products/{id}", productHandler.Update).Methods("PUT") - r.HandleFunc("/products/{id}", productHandler.Delete).Methods("DELETE") - - r.HandleFunc("/purchase-orders", purchaseOrderHandler.Create).Methods("POST") - r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Update).Methods("PUT") - r.HandleFunc("/purchase-orders/{id}", purchaseOrderHandler.Delete).Methods("DELETE") - - r.HandleFunc("/enquiries", enquiryHandler.Create).Methods("POST") - r.HandleFunc("/enquiries/{id}", enquiryHandler.Update).Methods("PUT") - r.HandleFunc("/enquiries/{id}", enquiryHandler.Delete).Methods("DELETE") - 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") - - r.HandleFunc("/documents", documentHandler.Create).Methods("POST") - r.HandleFunc("/documents/{id}", documentHandler.Update).Methods("PUT") - r.HandleFunc("/documents/{id}/archive", documentHandler.Archive).Methods("PUT") - r.HandleFunc("/documents/{id}/unarchive", documentHandler.Unarchive).Methods("PUT") + // Catch-all for everything else + r.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 page not found")) + }) // Start server port := getEnv("PORT", "8080") diff --git a/go-app/go.mod b/go-app/go.mod index 3dbae654..50ab1a3d 100644 --- a/go-app/go.mod +++ b/go-app/go.mod @@ -1,10 +1,13 @@ module code.springupsoftware.com/cmc/cmc-sales -go 1.23 +go 1.23.0 + +toolchain go1.24.3 require ( github.com/go-sql-driver/mysql v1.7.1 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 github.com/jung-kurt/gofpdf v1.16.2 + golang.org/x/text v0.27.0 ) diff --git a/go-app/go.sum b/go-app/go.sum index 401bc738..e16d802c 100644 --- a/go-app/go.sum +++ b/go-app/go.sum @@ -16,3 +16,5 @@ github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfF github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= diff --git a/go-app/internal/cmc/handlers/pages.go b/go-app/internal/cmc/handlers/pages.go index 3b79fc4a..a9405bf4 100644 --- a/go-app/internal/cmc/handlers/pages.go +++ b/go-app/internal/cmc/handlers/pages.go @@ -8,6 +8,8 @@ import ( "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates" "github.com/gorilla/mux" + "golang.org/x/text/cases" + "golang.org/x/text/language" ) type PageHandler struct { @@ -22,10 +24,21 @@ func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager) *PageH } } +// Helper function to get the username from the request +func getUsername(r *http.Request) string { + username, _, ok := r.BasicAuth() + if ok && username != "" { + caser := cases.Title(language.English) + return caser.String(username) // Capitalise the username for display + } + return "Guest" +} + // Home page func (h *PageHandler) Home(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Title": "Dashboard", + "User": getUsername(r), } if err := h.tmpl.Render(w, "index.html", data); err != nil { @@ -65,6 +78,7 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) { "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, + "User": getUsername(r), } // Check if this is an HTMX request @@ -83,6 +97,7 @@ func (h *PageHandler) CustomersIndex(w http.ResponseWriter, r *http.Request) { func (h *PageHandler) CustomersNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Customer": db.Customer{}, + "User": getUsername(r), } if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { @@ -106,6 +121,7 @@ func (h *PageHandler) CustomersEdit(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Customer": customer, + "User": getUsername(r), } if err := h.tmpl.Render(w, "customers/form.html", data); err != nil { @@ -129,6 +145,7 @@ func (h *PageHandler) CustomersShow(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Customer": customer, + "User": getUsername(r), } if err := h.tmpl.Render(w, "customers/show.html", data); err != nil { @@ -181,6 +198,7 @@ func (h *PageHandler) CustomersSearch(w http.ResponseWriter, r *http.Request) { "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, + "User": getUsername(r), } w.Header().Set("Content-Type", "text/html") @@ -194,6 +212,7 @@ 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 + "User": getUsername(r), } if err := h.tmpl.Render(w, "products/index.html", data); err != nil { @@ -204,6 +223,7 @@ func (h *PageHandler) ProductsIndex(w http.ResponseWriter, r *http.Request) { func (h *PageHandler) ProductsNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Product": db.Product{}, + "User": getUsername(r), } if err := h.tmpl.Render(w, "products/form.html", data); err != nil { @@ -227,6 +247,7 @@ func (h *PageHandler) ProductsShow(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Product": product, + "User": getUsername(r), } if err := h.tmpl.Render(w, "products/show.html", data); err != nil { @@ -250,6 +271,7 @@ func (h *PageHandler) ProductsEdit(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Product": product, + "User": getUsername(r), } if err := h.tmpl.Render(w, "products/form.html", data); err != nil { @@ -261,6 +283,7 @@ func (h *PageHandler) ProductsSearch(w http.ResponseWriter, r *http.Request) { // Similar to CustomersSearch but for products data := map[string]interface{}{ "Products": []db.Product{}, + "User": getUsername(r), } w.Header().Set("Content-Type", "text/html") @@ -273,6 +296,7 @@ func (h *PageHandler) ProductsSearch(w http.ResponseWriter, r *http.Request) { func (h *PageHandler) PurchaseOrdersIndex(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrders": []db.PurchaseOrder{}, + "User": getUsername(r), } if err := h.tmpl.Render(w, "purchase-orders/index.html", data); err != nil { @@ -283,6 +307,7 @@ func (h *PageHandler) PurchaseOrdersIndex(w http.ResponseWriter, r *http.Request func (h *PageHandler) PurchaseOrdersNew(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrder": db.PurchaseOrder{}, + "User": getUsername(r), } if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { @@ -306,6 +331,7 @@ func (h *PageHandler) PurchaseOrdersShow(w http.ResponseWriter, r *http.Request) data := map[string]interface{}{ "PurchaseOrder": purchaseOrder, + "User": getUsername(r), } if err := h.tmpl.Render(w, "purchase-orders/show.html", data); err != nil { @@ -329,6 +355,7 @@ func (h *PageHandler) PurchaseOrdersEdit(w http.ResponseWriter, r *http.Request) data := map[string]interface{}{ "PurchaseOrder": purchaseOrder, + "User": getUsername(r), } if err := h.tmpl.Render(w, "purchase-orders/form.html", data); err != nil { @@ -339,6 +366,7 @@ func (h *PageHandler) PurchaseOrdersEdit(w http.ResponseWriter, r *http.Request) func (h *PageHandler) PurchaseOrdersSearch(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "PurchaseOrders": []db.PurchaseOrder{}, + "User": getUsername(r), } w.Header().Set("Content-Type", "text/html") @@ -409,6 +437,7 @@ func (h *PageHandler) EnquiriesIndex(w http.ResponseWriter, r *http.Request) { "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, + "User": getUsername(r), } // Check if this is an HTMX request @@ -456,6 +485,7 @@ func (h *PageHandler) EnquiriesNew(w http.ResponseWriter, r *http.Request) { "Principles": principles, "States": states, "Countries": countries, + "User": getUsername(r), } if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { @@ -479,6 +509,7 @@ func (h *PageHandler) EnquiriesShow(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Enquiry": enquiry, + "User": getUsername(r), } if err := h.tmpl.Render(w, "enquiries/show.html", data); err != nil { @@ -531,6 +562,7 @@ func (h *PageHandler) EnquiriesEdit(w http.ResponseWriter, r *http.Request) { "Principles": principles, "States": states, "Countries": countries, + "User": getUsername(r), } if err := h.tmpl.Render(w, "enquiries/form.html", data); err != nil { @@ -595,6 +627,7 @@ func (h *PageHandler) EnquiriesSearch(w http.ResponseWriter, r *http.Request) { "PrevPage": page - 1, "NextPage": page + 1, "HasMore": hasMore, + "User": getUsername(r), } w.Header().Set("Content-Type", "text/html") @@ -651,6 +684,7 @@ func (h *PageHandler) DocumentsIndex(w http.ResponseWriter, r *http.Request) { "Users": users, "Page": page, "DocType": docType, + "User": getUsername(r), } // Check if this is an HTMX request @@ -683,6 +717,7 @@ func (h *PageHandler) DocumentsShow(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Document": document, + "User": getUsername(r), } if err := h.tmpl.Render(w, "documents/show.html", data); err != nil { @@ -699,7 +734,7 @@ func (h *PageHandler) DocumentsSearch(w http.ResponseWriter, r *http.Request) { // For now, just return all documents until search is implemented limit := 20 offset := 0 - + documents, err = h.queries.ListDocuments(r.Context(), db.ListDocumentsParams{ Limit: int32(limit), Offset: int32(offset), @@ -711,6 +746,7 @@ func (h *PageHandler) DocumentsSearch(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{ "Documents": documents, + "User": getUsername(r), } w.Header().Set("Content-Type", "text/html") @@ -746,6 +782,7 @@ func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) { "Document": document, "DocType": string(document.Type), "LineItems": lineItems, + "User": getUsername(r), } // Add document type specific data @@ -776,3 +813,10 @@ func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) } } + +// Quotes view page +func (h *PageHandler) QuotesView(w http.ResponseWriter, r *http.Request) { + if err := h.tmpl.Render(w, "quotes/index.html", nil); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/go-app/internal/cmc/templates/templates.go b/go-app/internal/cmc/templates/templates.go index ba9071e5..9446744e 100644 --- a/go-app/internal/cmc/templates/templates.go +++ b/go-app/internal/cmc/templates/templates.go @@ -20,9 +20,10 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) { // Define template functions funcMap := template.FuncMap{ - "formatDate": formatDate, - "truncate": truncate, - "currency": formatCurrency, + "formatDate": formatDate, + "truncate": truncate, + "currency": formatCurrency, + "currentYear": func() int { return time.Now().Year() }, } // Load all templates @@ -38,6 +39,7 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) { // Load page templates pages := []string{ + "index.html", "customers/index.html", "customers/show.html", "customers/form.html", @@ -63,14 +65,14 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) { "documents/purchase-order-view.html", "documents/orderack-view.html", "documents/packinglist-view.html", - "index.html", + "quotes/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) @@ -80,7 +82,7 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) { files = append(files, tablePath) } } - + // For documents view page, include all document type elements if page == "documents/view.html" { docElements := []string{ @@ -97,12 +99,12 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) { } } } - + tmpl, err := template.New(filepath.Base(page)).Funcs(funcMap).ParseFiles(files...) if err != nil { return nil, err } - + tm.templates[page] = tmpl } @@ -114,7 +116,7 @@ func (tm *TemplateManager) Render(w io.Writer, name string, data interface{}) er if !ok { return template.New("error").Execute(w, "Template not found") } - + return tmpl.ExecuteTemplate(w, "base", data) } @@ -123,7 +125,7 @@ func (tm *TemplateManager) RenderPartial(w io.Writer, templateFile, templateName if !ok { return template.New("error").Execute(w, "Template not found") } - + return tmpl.ExecuteTemplate(w, templateName, data) } @@ -151,4 +153,4 @@ func truncate(s string, n int) string { 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 index 35d84982..bc4ee30e 100755 Binary files a/go-app/server and b/go-app/server differ diff --git a/go-app/templates/layouts/base.html b/go-app/templates/layouts/base.html index a9e75312..8fa82987 100644 --- a/go-app/templates/layouts/base.html +++ b/go-app/templates/layouts/base.html @@ -6,8 +6,32 @@ {{block "title" .}}CMC Sales{{end}} - - + + + + @@ -20,65 +44,153 @@ {{block "head" .}}{{end}} - - - -
-
+
+
{{block "content" .}}{{end}}
- -