package handlers import ( "context" "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "strconv" "time" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/pdf" "github.com/gorilla/mux" ) type DocumentHandler struct { queries *db.Queries } func NewDocumentHandler(queries *db.Queries) *DocumentHandler { return &DocumentHandler{queries: queries} } // List handles GET /api/documents func (h *DocumentHandler) List(w http.ResponseWriter, r *http.Request) { ctx := context.Background() // Check for type filter docType := r.URL.Query().Get("type") // Get pagination parameters limit := int32(20) offset := int32(0) if l := r.URL.Query().Get("limit"); l != "" { if val, err := strconv.Atoi(l); err == nil && val > 0 { limit = int32(val) } } if o := r.URL.Query().Get("offset"); o != "" { if val, err := strconv.Atoi(o); err == nil && val >= 0 { offset = int32(val) } } var documents interface{} var err error if docType != "" { // Convert string to DocumentsType enum documents, err = h.queries.ListDocumentsByType(ctx, db.ListDocumentsByTypeParams{ Type: db.DocumentsType(docType), Limit: limit, Offset: offset, }) } else { documents, err = h.queries.ListDocuments(ctx, db.ListDocumentsParams{ Limit: limit, Offset: offset, }) } if err != nil { log.Printf("Error fetching documents: %v", err) http.Error(w, "Failed to fetch documents", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(documents) } // Helper functions to convert between pointer and null types func nullInt32FromPtr(p *int32) sql.NullInt32 { if p == nil { return sql.NullInt32{Valid: false} } return sql.NullInt32{Int32: *p, Valid: true} } func nullStringFromPtr(p *string) sql.NullString { if p == nil { return sql.NullString{Valid: false} } return sql.NullString{String: *p, Valid: true} } func nullTimeFromPtr(p *time.Time) sql.NullTime { if p == nil { return sql.NullTime{Valid: false} } return sql.NullTime{Time: *p, Valid: true} } // Get handles GET /api/documents/{id} func (h *DocumentHandler) Get(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } ctx := context.Background() document, err := h.queries.GetDocument(ctx, int32(id)) if err != nil { log.Printf("Error fetching document %d: %v", id, err) http.Error(w, "Document not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(document) } // Create handles POST /api/documents func (h *DocumentHandler) Create(w http.ResponseWriter, r *http.Request) { var req struct { Type string `json:"type"` UserID int32 `json:"user_id"` DocPageCount int32 `json:"doc_page_count"` CMCReference string `json:"cmc_reference"` PDFFilename string `json:"pdf_filename"` PDFCreatedAt string `json:"pdf_created_at"` PDFCreatedByUserID int32 `json:"pdf_created_by_user_id"` ShippingDetails *string `json:"shipping_details,omitempty"` Revision *int32 `json:"revision,omitempty"` BillTo *string `json:"bill_to,omitempty"` ShipTo *string `json:"ship_to,omitempty"` EmailSentAt string `json:"email_sent_at"` EmailSentByUserID int32 `json:"email_sent_by_user_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Validate required fields if req.Type == "" { http.Error(w, "Type is required", http.StatusBadRequest) return } if req.UserID == 0 { http.Error(w, "User ID is required", http.StatusBadRequest) return } if req.CMCReference == "" { http.Error(w, "CMC Reference is required", http.StatusBadRequest) return } ctx := context.Background() // Parse timestamps pdfCreatedAt, err := time.Parse("2006-01-02 15:04:05", req.PDFCreatedAt) if err != nil { http.Error(w, "Invalid pdf_created_at format", http.StatusBadRequest) return } emailSentAt, err := time.Parse("2006-01-02 15:04:05", req.EmailSentAt) if err != nil { http.Error(w, "Invalid email_sent_at format", http.StatusBadRequest) return } revision := int32(0) if req.Revision != nil { revision = *req.Revision } params := db.CreateDocumentParams{ Type: db.DocumentsType(req.Type), UserID: req.UserID, DocPageCount: req.DocPageCount, CmcReference: req.CMCReference, PdfFilename: req.PDFFilename, PdfCreatedAt: pdfCreatedAt, PdfCreatedByUserID: req.PDFCreatedByUserID, ShippingDetails: nullStringFromPtr(req.ShippingDetails), Revision: revision, BillTo: nullStringFromPtr(req.BillTo), ShipTo: nullStringFromPtr(req.ShipTo), EmailSentAt: emailSentAt, EmailSentByUserID: req.EmailSentByUserID, } result, err := h.queries.CreateDocument(ctx, params) if err != nil { log.Printf("Error creating document: %v", err) http.Error(w, "Failed to create document", http.StatusInternalServerError) return } id, _ := result.LastInsertId() w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "id": id, "message": "Document created successfully", }) } // Update handles PUT /api/documents/{id} func (h *DocumentHandler) Update(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } var req struct { Type string `json:"type"` UserID int32 `json:"user_id"` DocPageCount int32 `json:"doc_page_count"` CMCReference string `json:"cmc_reference"` PDFFilename string `json:"pdf_filename"` PDFCreatedAt string `json:"pdf_created_at"` PDFCreatedByUserID int32 `json:"pdf_created_by_user_id"` ShippingDetails *string `json:"shipping_details,omitempty"` Revision *int32 `json:"revision,omitempty"` BillTo *string `json:"bill_to,omitempty"` ShipTo *string `json:"ship_to,omitempty"` EmailSentAt string `json:"email_sent_at"` EmailSentByUserID int32 `json:"email_sent_by_user_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } ctx := context.Background() // Parse timestamps pdfCreatedAt, err := time.Parse("2006-01-02 15:04:05", req.PDFCreatedAt) if err != nil { http.Error(w, "Invalid pdf_created_at format", http.StatusBadRequest) return } emailSentAt, err := time.Parse("2006-01-02 15:04:05", req.EmailSentAt) if err != nil { http.Error(w, "Invalid email_sent_at format", http.StatusBadRequest) return } revision := int32(0) if req.Revision != nil { revision = *req.Revision } params := db.UpdateDocumentParams{ Type: db.DocumentsType(req.Type), UserID: req.UserID, DocPageCount: req.DocPageCount, CmcReference: req.CMCReference, PdfFilename: req.PDFFilename, PdfCreatedAt: pdfCreatedAt, PdfCreatedByUserID: req.PDFCreatedByUserID, ShippingDetails: nullStringFromPtr(req.ShippingDetails), Revision: revision, BillTo: nullStringFromPtr(req.BillTo), ShipTo: nullStringFromPtr(req.ShipTo), EmailSentAt: emailSentAt, EmailSentByUserID: req.EmailSentByUserID, ID: int32(id), } err = h.queries.UpdateDocument(ctx, params) if err != nil { log.Printf("Error updating document %d: %v", id, err) http.Error(w, "Failed to update document", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Document updated successfully", }) } // Archive handles PUT /api/documents/{id}/archive func (h *DocumentHandler) Archive(w http.ResponseWriter, r *http.Request) { // TODO: Implement archive functionality when query is added http.Error(w, "Archive functionality not yet implemented", http.StatusNotImplemented) } // Unarchive handles PUT /api/documents/{id}/unarchive func (h *DocumentHandler) Unarchive(w http.ResponseWriter, r *http.Request) { // TODO: Implement unarchive functionality when query is added http.Error(w, "Unarchive functionality not yet implemented", http.StatusNotImplemented) } // Search handles GET /api/documents/search?q=query func (h *DocumentHandler) Search(w http.ResponseWriter, r *http.Request) { // TODO: Implement search functionality when query is added http.Error(w, "Search functionality not yet implemented", http.StatusNotImplemented) } // GeneratePDF handles GET /documents/pdf/{id} func (h *DocumentHandler) GeneratePDF(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "Invalid document ID", http.StatusBadRequest) return } ctx := context.Background() // Get document document, err := h.queries.GetDocument(ctx, int32(id)) if err != nil { log.Printf("Error fetching document %d: %v", id, err) http.Error(w, "Document not found", http.StatusNotFound) return } // Get output directory from environment or use default outputDir := os.Getenv("PDF_OUTPUT_DIR") if outputDir == "" { outputDir = "webroot/pdf" } // Ensure output directory exists if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("Error creating PDF directory: %v", err) http.Error(w, "Failed to create PDF directory", http.StatusInternalServerError) return } var filename string // Generate PDF based on document type log.Printf("Generating PDF for document %d, type: %s, outputDir: %s", id, document.Type, outputDir) switch document.Type { case db.DocumentsTypeQuote: // Get quote-specific data // TODO: Get quote data from database // Get enquiry data // TODO: Get enquiry from quote // Get customer data // TODO: Get customer from enquiry // Get user data user, err := h.queries.GetUser(ctx, document.UserID) if err != nil { log.Printf("Error fetching user: %v", err) http.Error(w, "Failed to fetch user data", http.StatusInternalServerError) return } // Get line items lineItems, err := h.queries.GetLineItemsTable(ctx, int32(id)) if err != nil { log.Printf("Error fetching line items: %v", err) http.Error(w, "Failed to fetch line items", http.StatusInternalServerError) return } // For now, create a simplified quote PDF data := &pdf.QuotePDFData{ Document: &document, User: &user, LineItems: lineItems, CurrencySymbol: "$", // Default to AUD ShowGST: true, // Default to showing GST } // Generate PDF log.Printf("Calling GenerateQuotePDF with outputDir: %s", outputDir) filename, err = pdf.GenerateQuotePDF(data, outputDir) if err != nil { log.Printf("Error generating quote PDF: %v", err) http.Error(w, "Failed to generate PDF", http.StatusInternalServerError) return } log.Printf("Successfully generated PDF: %s", filename) case db.DocumentsTypeInvoice: // Get invoice-specific data // TODO: Implement invoice PDF generation http.Error(w, "Invoice PDF generation not yet implemented", http.StatusNotImplemented) return case db.DocumentsTypePurchaseOrder: // Get purchase order data po, err := h.queries.GetPurchaseOrderByDocumentID(ctx, int32(id)) if err != nil { log.Printf("Error fetching purchase order: %v", err) http.Error(w, "Failed to fetch purchase order data", http.StatusInternalServerError) return } // Get principle data principle, err := h.queries.GetPrinciple(ctx, po.PrincipleID) if err != nil { log.Printf("Error fetching principle: %v", err) http.Error(w, "Failed to fetch principle data", http.StatusInternalServerError) return } // Get line items lineItems, err := h.queries.GetLineItemsTable(ctx, int32(id)) if err != nil { log.Printf("Error fetching line items: %v", err) http.Error(w, "Failed to fetch line items", http.StatusInternalServerError) return } // Create purchase order PDF data data := &pdf.PurchaseOrderPDFData{ Document: &document, PurchaseOrder: &po, Principle: &principle, LineItems: lineItems, CurrencySymbol: "$", // Default to AUD ShowGST: true, // Default to showing GST for Australian principles } // Generate PDF filename, err = pdf.GeneratePurchaseOrderPDF(data, outputDir) if err != nil { log.Printf("Error generating purchase order PDF: %v", err) http.Error(w, "Failed to generate PDF", http.StatusInternalServerError) return } default: http.Error(w, "Unsupported document type for PDF generation", http.StatusBadRequest) return } // TODO: Update document with PDF filename and timestamp // This would require a specific query to update just PDF info log.Printf("PDF generated successfully: %s", filename) // Return success response with redirect to document view w.Header().Set("Content-Type", "text/html") fmt.Fprintf(w, `

PDF generated successfully. Click here if you are not redirected.

`, id, id) } // GetRecentActivity returns recent documents as HTML for the dashboard func (h *DocumentHandler) GetRecentActivity(w http.ResponseWriter, r *http.Request) { // Get the last 10 documents documents, err := h.queries.GetRecentDocuments(r.Context(), 10) if err != nil { log.Printf("Error fetching recent activity: %v", err) http.Error(w, "Failed to fetch recent activity", http.StatusInternalServerError) return } // Format the response as HTML w.Header().Set("Content-Type", "text/html") if len(documents) == 0 { fmt.Fprintf(w, `

No recent activity

`) return } // Build HTML table fmt.Fprintf(w, ``) for _, doc := range documents { // Format the document type badge var typeClass string switch doc.Type { case "quote": typeClass = "is-info" case "invoice": typeClass = "is-success" case "purchaseOrder": typeClass = "is-warning" case "orderAck": typeClass = "is-primary" case "packingList": typeClass = "is-link" default: typeClass = "is-light" } // Format the date createdDate := doc.Created.Format("Jan 2, 2006 3:04 PM") // Handle null username createdBy := "Unknown" if doc.CreatedByUsername.Valid { createdBy = doc.CreatedByUsername.String } // Add revision indicator if applicable revisionText := "" if doc.Revision > 0 { revisionText = fmt.Sprintf(" Rev %d", doc.Revision) } fmt.Fprintf(w, ``, typeClass, doc.DisplayName, doc.CmcReference, revisionText, createdBy, createdDate, doc.ID) } fmt.Fprintf(w, `
Document Reference Created By Date Actions
%s %s%s %s %s View
`) // Add a link to view all documents fmt.Fprintf(w, `
View All Documents
`) }