Adding go basic layout

This commit is contained in:
Finley Ghosh 2025-07-13 22:50:47 +10:00
parent d804a88d15
commit 4cd67eaf6c
13 changed files with 462 additions and 328 deletions

View file

@ -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"]
CMD ["air", "-c", ".air.toml"]

View file

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

View file

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

25
go-app/.air.toml Normal file
View file

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

View file

@ -52,39 +52,31 @@ 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"))))
goRouter.HandleFunc("/quotes", pageHandler.QuotesView).Methods("GET")
// The following routes are currently disabled:
/*
// 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")
@ -92,7 +84,6 @@ func main() {
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")
@ -100,7 +91,6 @@ func main() {
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")
@ -111,7 +101,6 @@ func main() {
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")
@ -120,7 +109,6 @@ func main() {
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")
@ -128,7 +116,6 @@ func main() {
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")
@ -136,7 +123,6 @@ func main() {
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")
@ -144,7 +130,6 @@ func main() {
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")
@ -152,7 +137,6 @@ func main() {
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")
@ -160,54 +144,45 @@ func main() {
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")
@ -216,7 +191,6 @@ func main() {
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")
@ -224,7 +198,6 @@ func main() {
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")
@ -232,7 +205,6 @@ func main() {
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")
@ -240,7 +212,6 @@ func main() {
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")
@ -248,31 +219,33 @@ func main() {
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")

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@ func NewTemplateManager(templatesDir string) (*TemplateManager, error) {
"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,7 +65,7 @@ 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 {

Binary file not shown.

View file

@ -6,8 +6,32 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{block "title" .}}CMC Sales{{end}}</title>
<!-- Bulma CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
cmcblue: '#4686c3',
}
}
}
}
</script>
<style>
/* Additional CSS to assist with more complicated transitions for the nav bar items */
.navbar-dropdown {
opacity: 0;
visibility: hidden;
transition: opacity 80ms ease, visibility 80ms ease;
}
.group:hover .navbar-dropdown,
.group:focus-within .navbar-dropdown {
opacity: 1;
visibility: visible;
}
</style>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@ -20,65 +44,153 @@
{{block "head" .}}{{end}}
</head>
<body>
<!-- Navigation -->
<nav class="navbar is-primary" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<strong>CMC Sales</strong>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMain">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<body class="min-h-screen flex flex-col">
<!-- Cute Light Blue Navigation -->
<nav class="bg-cmcblue shadow flex items-center justify-between px-4 py-2" role="navigation" aria-label="main navigation">
<div class="flex items-center space-x-2">
<a class="flex items-center space-x-2 text-white font-bold text-lg" href="/">
<span><i class="fas fa-flask text-cmcblue bg-white rounded p-1"></i></span>
<span>CMC Sales</span>
</a>
<span class="mx-4 h-8 w-px bg-white opacity-40"></span>
</div>
<div id="navbarMain" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/">
<span class="icon"><i class="fas fa-home"></i></span>
<span>Dashboard</span>
</a>
<a class="navbar-item" href="/customers">
<span class="icon"><i class="fas fa-users"></i></span>
<span>Customers</span>
</a>
<a class="navbar-item" href="/products">
<span class="icon"><i class="fas fa-box"></i></span>
<span>Products</span>
</a>
<a class="navbar-item" href="/purchase-orders">
<span class="icon"><i class="fas fa-file-invoice"></i></span>
<span>Purchase Orders</span>
</a>
<a class="navbar-item" href="/enquiries">
<span class="icon"><i class="fas fa-envelope"></i></span>
<div class="flex-1 flex items-center">
<ul class="flex space-x-2">
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/enquiries/index">
<span>Enquiries</span>
<span><i class="ml-1 fas fa-envelope text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/enquiries/index">Enquiry Register</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/documents/index">
<span>Documents</span>
<span><i class="ml-1 fas fa-file-alt text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/documents/index">Documents Index</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/go/quotes">Quotes</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/jobs/index">
<span>Jobs</span>
<span><i class="ml-1 fas fa-briefcase text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/jobs/reports">Reports</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/jobs/index">Job List</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/shipments/index">
<span>Shipments</span>
<span><i class="ml-1 fas fa-truck text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-64 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/index">All Shipments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/index/import">Import Shipments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/index/direct">Direct Shipments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/index/export">Export Shipments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/index/local">Local Shipments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/shipments/reports">Monthly Deferred GST</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/freight_forwarders">Freight Forwarders</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/customers/index">
<span>Customers</span>
<span><i class="ml-1 fas fa-users text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-56 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/customers/index">Customer Index</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/customers/add">Add Customer</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/industries/index">Industries</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/purchase_orders/index">
<span>POs</span>
<span><i class="ml-1 fas fa-file-invoice text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/purchase_orders/index">PO Index</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/invoices/index">
<span>Invoices</span>
<span><i class="ml-1 fas fa-receipt text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-56 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/invoices/index">Invoices Index</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/invoices/printView">Print View</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/products/index">
<span>Products</span>
<span><i class="ml-1 fas fa-box text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/products/index">Product Index</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/products/add">Add Product</a></li>
</ul>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/principles/index">
<span>Principles</span>
<span><i class="ml-1 fas fa-user-tie text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-56 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/principles/index">Principle Index</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/attachments/index">Attachments</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/principles/add">Add Principle</a></li>
</ul>
</li>
<li>
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/enquiries/search" id="searchLink">
<span>Search</span>
<span><i class="ml-1 fas fa-search text-white"></i></span>
</a>
</li>
<li class="relative group">
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/pages/about">
<span>Help</span>
<span><i class="ml-1 fas fa-question-circle text-white"></i></span>
</a>
<ul class="navbar-dropdown absolute left-0 top-[calc(100%_-_2px)] w-48 bg-white shadow-lg rounded z-10">
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/pages/bug">Raise a bug</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="https://gitlab.com/minimalist.software/cmc-sales/issues">Issue tracker</a></li>
<li><a class="block px-4 py-2 text-cmcblue hover:bg-cmcblue/10" href="/pages/about">About</a></li>
</ul>
</li>
<li>
<a class="flex items-center space-x-1 text-white px-2 py-1 bg-cmcblue cursor-pointer rounded transition duration-200 ease-in-out" href="/admin">
<span>Admin</span>
<span><i class="ml-1 fas fa-cog text-white"></i></span>
</a>
</li>
</ul>
</div>
<div class="flex items-center space-x-2">
<span><i class="fas fa-user text-cmcblue bg-white rounded p-1"></i></span>
<span class="text-white">{{if .User}}{{.User}}{{else}}Guest{{end}}</span>
</div>
</nav>
<!-- Main Content -->
<section class="section">
<div class="container">
<section class="section flex-1">
<div class="container p-4">
{{block "content" .}}{{end}}
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="content has-text-centered">
<p>
<strong>CMC Sales</strong> &copy; 2024 CMC Technologies
</p>
</div>
<footer class="bg-slate-500 text-white text-center py-2 text-sm mt-8 w-full">
<strong>CMC Sales</strong> &copy; {{ currentYear }}
</footer>
<!-- Custom JS -->

View file

@ -0,0 +1,4 @@
{{define "content"}}
<h1 class="text-2xl font-bold mb-4">Quotes</h1>
<p>Welcome to the Quotes page. Here you can view and manage your quotes.</p>
{{end}}

1
go-app/tmp/stdout Normal file
View file

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1