cmc-sales/go/internal/cmc/handlers/pages.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)
}
}