1221 lines
32 KiB
Go
1221 lines
32 KiB
Go
package handlers
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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 {
|
|
queries *db.Queries
|
|
tmpl *templates.TemplateManager
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewPageHandler(queries *db.Queries, tmpl *templates.TemplateManager, database *sql.DB) *PageHandler {
|
|
return &PageHandler{
|
|
queries: queries,
|
|
tmpl: tmpl,
|
|
db: database,
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
// 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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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{},
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
// 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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
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,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.tmpl.RenderPartial(w, "enquiries/table.html", "enquiry-table", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// Document page handlers
|
|
func (h *PageHandler) DocumentsIndex(w http.ResponseWriter, r *http.Request) {
|
|
page := 1
|
|
if p := r.URL.Query().Get("page"); p != "" {
|
|
if val, err := strconv.Atoi(p); err == nil && val > 0 {
|
|
page = val
|
|
}
|
|
}
|
|
|
|
// Get document type filter
|
|
docType := r.URL.Query().Get("type")
|
|
|
|
limit := 20
|
|
offset := (page - 1) * limit
|
|
|
|
var documents interface{}
|
|
var err error
|
|
|
|
if docType != "" {
|
|
documents, err = h.queries.ListDocumentsByType(r.Context(), db.ListDocumentsByTypeParams{
|
|
Type: db.DocumentsType(docType),
|
|
Limit: int32(limit + 1),
|
|
Offset: int32(offset),
|
|
})
|
|
} else {
|
|
documents, err = h.queries.ListDocuments(r.Context(), db.ListDocumentsParams{
|
|
Limit: int32(limit + 1),
|
|
Offset: int32(offset),
|
|
})
|
|
}
|
|
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Get users list for display names (if needed)
|
|
users, err := h.queries.GetAllUsers(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Documents": documents,
|
|
"Users": users,
|
|
"Page": page,
|
|
"DocType": docType,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
// Check if this is an HTMX request
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := h.tmpl.Render(w, "documents/index.html", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) DocumentsShow(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
document, err := h.queries.GetDocumentWithUser(r.Context(), int32(id))
|
|
if err != nil {
|
|
log.Printf("Error fetching document %d: %v", id, err)
|
|
http.Error(w, "Document not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Document": document,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
if err := h.tmpl.Render(w, "documents/show.html", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) DocumentsSearch(w http.ResponseWriter, r *http.Request) {
|
|
// query := r.URL.Query().Get("search") // TODO: Use when search is implemented
|
|
|
|
var documents interface{}
|
|
var err error
|
|
|
|
// 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),
|
|
})
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Documents": documents,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.tmpl.RenderPartial(w, "documents/table.html", "document-table", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid document ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
document, err := h.queries.GetDocumentWithUser(r.Context(), int32(id))
|
|
if err != nil {
|
|
http.Error(w, "Document not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Load line items for this document
|
|
lineItems, err := h.queries.ListLineItemsByDocument(r.Context(), int32(id))
|
|
if err != nil {
|
|
log.Printf("Error loading line items for document %d: %v", id, err)
|
|
// Don't fail the entire page if line items can't be loaded
|
|
lineItems = []db.LineItem{}
|
|
}
|
|
|
|
// Prepare data based on document type
|
|
data := map[string]interface{}{
|
|
"Document": document,
|
|
"DocType": string(document.Type),
|
|
"LineItems": lineItems,
|
|
"User": getUsername(r),
|
|
}
|
|
|
|
// Add document type specific data
|
|
switch document.Type {
|
|
case db.DocumentsTypeQuote:
|
|
// For quotes, we might need to load enquiry data
|
|
if document.CmcReference != "" {
|
|
// The CmcReference for quotes is the enquiry title
|
|
data["EnquiryTitle"] = document.CmcReference
|
|
}
|
|
|
|
case db.DocumentsTypeInvoice:
|
|
// For invoices, load job and customer data if needed
|
|
data["ShowPaymentButton"] = true
|
|
|
|
case db.DocumentsTypePurchaseOrder:
|
|
// For purchase orders, load principle data if needed
|
|
|
|
case db.DocumentsTypeOrderAck:
|
|
// For order acknowledgements, load job data if needed
|
|
|
|
case db.DocumentsTypePackingList:
|
|
// For packing lists, load job data if needed
|
|
}
|
|
|
|
// Render the appropriate template
|
|
if err := h.tmpl.Render(w, "documents/view.html", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// Email page handlers
|
|
func (h *PageHandler) EmailsIndex(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 := 30
|
|
offset := (page - 1) * limit
|
|
search := r.URL.Query().Get("search")
|
|
filter := r.URL.Query().Get("filter")
|
|
|
|
// Build SQL query based on filters
|
|
query := `
|
|
SELECT e.id, e.subject, e.user_id, e.created, e.gmail_message_id,
|
|
e.email_attachment_count, e.is_downloaded,
|
|
u.email as user_email, u.first_name, u.last_name
|
|
FROM emails e
|
|
LEFT JOIN users u ON e.user_id = u.id`
|
|
|
|
var args []interface{}
|
|
var conditions []string
|
|
|
|
// Apply search filter
|
|
if search != "" {
|
|
conditions = append(conditions, "(e.subject LIKE ? OR e.raw_headers LIKE ?)")
|
|
searchTerm := "%" + search + "%"
|
|
args = append(args, searchTerm, searchTerm)
|
|
}
|
|
|
|
// Apply type filter
|
|
switch filter {
|
|
case "downloaded":
|
|
conditions = append(conditions, "e.is_downloaded = 1")
|
|
case "gmail":
|
|
conditions = append(conditions, "e.gmail_message_id IS NOT NULL")
|
|
case "unassociated":
|
|
conditions = append(conditions, `NOT EXISTS (
|
|
SELECT 1 FROM emails_enquiries WHERE email_id = e.id
|
|
UNION SELECT 1 FROM emails_invoices WHERE email_id = e.id
|
|
UNION SELECT 1 FROM emails_purchase_orders WHERE email_id = e.id
|
|
UNION SELECT 1 FROM emails_jobs WHERE email_id = e.id
|
|
)`)
|
|
}
|
|
|
|
if len(conditions) > 0 {
|
|
query += " WHERE " + joinConditions(conditions, " AND ")
|
|
}
|
|
|
|
query += " ORDER BY e.id DESC LIMIT ? OFFSET ?"
|
|
args = append(args, limit+1, offset) // Get one extra to check if there are more
|
|
|
|
// Execute the query to get emails
|
|
rows, err := h.db.Query(query, args...)
|
|
if err != nil {
|
|
log.Printf("Error querying emails: %v", err)
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type EmailWithUser struct {
|
|
ID int32 `json:"id"`
|
|
Subject string `json:"subject"`
|
|
UserID int32 `json:"user_id"`
|
|
Created time.Time `json:"created"`
|
|
GmailMessageID *string `json:"gmail_message_id"`
|
|
AttachmentCount int32 `json:"attachment_count"`
|
|
IsDownloaded *bool `json:"is_downloaded"`
|
|
UserEmail *string `json:"user_email"`
|
|
FirstName *string `json:"first_name"`
|
|
LastName *string `json:"last_name"`
|
|
}
|
|
|
|
var emails []EmailWithUser
|
|
for rows.Next() {
|
|
var email EmailWithUser
|
|
var gmailMessageID, userEmail, firstName, lastName sql.NullString
|
|
var isDownloaded sql.NullBool
|
|
|
|
err := rows.Scan(
|
|
&email.ID,
|
|
&email.Subject,
|
|
&email.UserID,
|
|
&email.Created,
|
|
&gmailMessageID,
|
|
&email.AttachmentCount,
|
|
&isDownloaded,
|
|
&userEmail,
|
|
&firstName,
|
|
&lastName,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Error scanning email row: %v", err)
|
|
continue
|
|
}
|
|
|
|
if gmailMessageID.Valid {
|
|
email.GmailMessageID = &gmailMessageID.String
|
|
}
|
|
if isDownloaded.Valid {
|
|
email.IsDownloaded = &isDownloaded.Bool
|
|
}
|
|
if userEmail.Valid {
|
|
email.UserEmail = &userEmail.String
|
|
}
|
|
if firstName.Valid {
|
|
email.FirstName = &firstName.String
|
|
}
|
|
if lastName.Valid {
|
|
email.LastName = &lastName.String
|
|
}
|
|
|
|
emails = append(emails, email)
|
|
}
|
|
|
|
hasMore := len(emails) > limit
|
|
if hasMore {
|
|
emails = emails[:limit]
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Emails": emails,
|
|
"Page": page,
|
|
"PrevPage": page - 1,
|
|
"NextPage": page + 1,
|
|
"HasMore": hasMore,
|
|
"TotalPages": ((len(emails) + limit - 1) / limit),
|
|
}
|
|
|
|
// Check if this is an HTMX request
|
|
if r.Header.Get("HX-Request") == "true" {
|
|
if err := h.tmpl.RenderPartial(w, "emails/table.html", "email-table", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err := h.tmpl.Render(w, "emails/index.html", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) EmailsShow(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
id, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get email details from database
|
|
emailQuery := `
|
|
SELECT e.id, e.subject, e.user_id, e.created, e.gmail_message_id,
|
|
e.gmail_thread_id, e.raw_headers, e.is_downloaded,
|
|
u.email as user_email, u.first_name, u.last_name
|
|
FROM emails e
|
|
LEFT JOIN users u ON e.user_id = u.id
|
|
WHERE e.id = ?`
|
|
|
|
var email struct {
|
|
ID int32 `json:"id"`
|
|
Subject string `json:"subject"`
|
|
UserID int32 `json:"user_id"`
|
|
Created time.Time `json:"created"`
|
|
GmailMessageID *string `json:"gmail_message_id"`
|
|
GmailThreadID *string `json:"gmail_thread_id"`
|
|
RawHeaders *string `json:"raw_headers"`
|
|
IsDownloaded *bool `json:"is_downloaded"`
|
|
User *struct {
|
|
Email string `json:"email"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
} `json:"user"`
|
|
Enquiries []int32 `json:"enquiries"`
|
|
Invoices []int32 `json:"invoices"`
|
|
PurchaseOrders []int32 `json:"purchase_orders"`
|
|
Jobs []int32 `json:"jobs"`
|
|
}
|
|
|
|
var gmailMessageID, gmailThreadID, rawHeaders sql.NullString
|
|
var isDownloaded sql.NullBool
|
|
var userEmail, firstName, lastName sql.NullString
|
|
|
|
err = h.db.QueryRow(emailQuery, id).Scan(
|
|
&email.ID,
|
|
&email.Subject,
|
|
&email.UserID,
|
|
&email.Created,
|
|
&gmailMessageID,
|
|
&gmailThreadID,
|
|
&rawHeaders,
|
|
&isDownloaded,
|
|
&userEmail,
|
|
&firstName,
|
|
&lastName,
|
|
)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
http.Error(w, "Email not found", http.StatusNotFound)
|
|
} else {
|
|
log.Printf("Error fetching email %d: %v", id, err)
|
|
http.Error(w, "Database error", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Set nullable fields
|
|
if gmailMessageID.Valid {
|
|
email.GmailMessageID = &gmailMessageID.String
|
|
}
|
|
if gmailThreadID.Valid {
|
|
email.GmailThreadID = &gmailThreadID.String
|
|
}
|
|
if rawHeaders.Valid {
|
|
email.RawHeaders = &rawHeaders.String
|
|
}
|
|
if isDownloaded.Valid {
|
|
email.IsDownloaded = &isDownloaded.Bool
|
|
}
|
|
|
|
// Set user info if available
|
|
if userEmail.Valid {
|
|
email.User = &struct {
|
|
Email string `json:"email"`
|
|
FirstName string `json:"first_name"`
|
|
LastName string `json:"last_name"`
|
|
}{
|
|
Email: userEmail.String,
|
|
FirstName: firstName.String,
|
|
LastName: lastName.String,
|
|
}
|
|
}
|
|
|
|
// Get email attachments
|
|
attachmentQuery := `
|
|
SELECT id, name, type, size, filename, is_message_body, gmail_attachment_id, created
|
|
FROM email_attachments
|
|
WHERE email_id = ?
|
|
ORDER BY is_message_body DESC, created ASC`
|
|
|
|
attachmentRows, err := h.db.Query(attachmentQuery, id)
|
|
if err != nil {
|
|
log.Printf("Error fetching attachments for email %d: %v", id, err)
|
|
}
|
|
|
|
type EmailAttachment struct {
|
|
ID int32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Size int32 `json:"size"`
|
|
Filename string `json:"filename"`
|
|
IsMessageBody bool `json:"is_message_body"`
|
|
GmailAttachmentID *string `json:"gmail_attachment_id"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
var attachments []EmailAttachment
|
|
hasStoredAttachments := false
|
|
|
|
if attachmentRows != nil {
|
|
defer attachmentRows.Close()
|
|
for attachmentRows.Next() {
|
|
hasStoredAttachments = true
|
|
var attachment EmailAttachment
|
|
var gmailAttachmentID sql.NullString
|
|
|
|
err := attachmentRows.Scan(
|
|
&attachment.ID,
|
|
&attachment.Name,
|
|
&attachment.Type,
|
|
&attachment.Size,
|
|
&attachment.Filename,
|
|
&attachment.IsMessageBody,
|
|
&gmailAttachmentID,
|
|
&attachment.Created,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Error scanning attachment: %v", err)
|
|
continue
|
|
}
|
|
|
|
if gmailAttachmentID.Valid {
|
|
attachment.GmailAttachmentID = &gmailAttachmentID.String
|
|
}
|
|
|
|
attachments = append(attachments, attachment)
|
|
}
|
|
}
|
|
|
|
// If no stored attachments and this is a Gmail email, show a notice
|
|
if !hasStoredAttachments && email.GmailMessageID != nil {
|
|
// For the page view, we'll just show a notice that attachments can be fetched
|
|
// The actual fetching will happen via the API endpoint when needed
|
|
log.Printf("Email %d is a Gmail email without indexed attachments", id)
|
|
}
|
|
|
|
// Get associated records (simplified queries for now)
|
|
// Enquiries
|
|
enquiryRows, err := h.db.Query("SELECT enquiry_id FROM emails_enquiries WHERE email_id = ?", id)
|
|
if err == nil {
|
|
defer enquiryRows.Close()
|
|
for enquiryRows.Next() {
|
|
var enquiryID int32
|
|
if enquiryRows.Scan(&enquiryID) == nil {
|
|
email.Enquiries = append(email.Enquiries, enquiryID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Invoices
|
|
invoiceRows, err := h.db.Query("SELECT invoice_id FROM emails_invoices WHERE email_id = ?", id)
|
|
if err == nil {
|
|
defer invoiceRows.Close()
|
|
for invoiceRows.Next() {
|
|
var invoiceID int32
|
|
if invoiceRows.Scan(&invoiceID) == nil {
|
|
email.Invoices = append(email.Invoices, invoiceID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Purchase Orders
|
|
poRows, err := h.db.Query("SELECT purchase_order_id FROM emails_purchase_orders WHERE email_id = ?", id)
|
|
if err == nil {
|
|
defer poRows.Close()
|
|
for poRows.Next() {
|
|
var poID int32
|
|
if poRows.Scan(&poID) == nil {
|
|
email.PurchaseOrders = append(email.PurchaseOrders, poID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Jobs
|
|
jobRows, err := h.db.Query("SELECT job_id FROM emails_jobs WHERE email_id = ?", id)
|
|
if err == nil {
|
|
defer jobRows.Close()
|
|
for jobRows.Next() {
|
|
var jobID int32
|
|
if jobRows.Scan(&jobID) == nil {
|
|
email.Jobs = append(email.Jobs, jobID)
|
|
}
|
|
}
|
|
}
|
|
|
|
data := map[string]interface{}{
|
|
"Email": email,
|
|
"Attachments": attachments,
|
|
}
|
|
|
|
if err := h.tmpl.Render(w, "emails/show.html", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) EmailsSearch(w http.ResponseWriter, r *http.Request) {
|
|
_ = r.URL.Query().Get("search") // TODO: Implement search functionality
|
|
|
|
// Empty result for now - would need proper implementation
|
|
emails := []interface{}{}
|
|
|
|
data := map[string]interface{}{
|
|
"Emails": emails,
|
|
"Page": 1,
|
|
"PrevPage": 0,
|
|
"NextPage": 2,
|
|
"HasMore": false,
|
|
"TotalPages": 1,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.tmpl.RenderPartial(w, "emails/table.html", "email-table", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (h *PageHandler) EmailsAttachments(w http.ResponseWriter, r *http.Request) {
|
|
vars := mux.Vars(r)
|
|
_, err := strconv.Atoi(vars["id"])
|
|
if err != nil {
|
|
http.Error(w, "Invalid email ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Empty attachments for now - would need proper implementation
|
|
attachments := []interface{}{}
|
|
|
|
data := map[string]interface{}{
|
|
"Attachments": attachments,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
if err := h.tmpl.RenderPartial(w, "emails/attachments.html", "email-attachments", data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|