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") var documents interface{} var err error if docType != "" { // Convert string to DocumentsType enum documents, err = h.queries.ListDocumentsByType(ctx, db.DocumentsType(docType)) } else { documents, err = h.queries.ListDocuments(ctx) } 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.GetDocumentByID(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) { 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() err = h.queries.ArchiveDocument(ctx, int32(id)) if err != nil { log.Printf("Error archiving document %d: %v", id, err) http.Error(w, "Failed to archive document", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Document archived successfully", }) } // Unarchive handles PUT /api/documents/{id}/unarchive func (h *DocumentHandler) Unarchive(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() err = h.queries.UnarchiveDocument(ctx, db.UnarchiveDocumentParams{ Column1: int32(id), Column2: int32(id), }) if err != nil { log.Printf("Error unarchiving document %d: %v", id, err) http.Error(w, "Failed to unarchive document", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "message": "Document unarchived successfully", }) } // Search handles GET /api/documents/search?q=query func (h *DocumentHandler) Search(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { http.Error(w, "Search query is required", http.StatusBadRequest) return } ctx := context.Background() searchPattern := "%" + query + "%" documents, err := h.queries.SearchDocuments(ctx, db.SearchDocumentsParams{ PdfFilename: searchPattern, CmcReference: searchPattern, Name: searchPattern, Title: searchPattern, Username: searchPattern, }) if err != nil { log.Printf("Error searching documents: %v", err) http.Error(w, "Failed to search documents", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(documents) } // 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.GetDocumentByID(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 } // Update document with PDF filename and timestamp now := time.Now() err = h.queries.UpdateDocumentPDFInfo(ctx, db.UpdateDocumentPDFInfoParams{ PdfFilename: filename, PdfCreatedAt: now, PdfCreatedByUserID: 1, // TODO: Get current user ID ID: int32(id), }) if err != nil { log.Printf("Error updating document PDF info: %v", err) // Don't fail the request, PDF was generated successfully } // 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) }