cmc-sales/go/internal/cmc/handlers/document.go

565 lines
16 KiB
Go

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, `
<html>
<head>
<script type="text/javascript">
window.location.replace("/documents/view/%d");
</script>
</head>
<body>
<p>PDF generated successfully. <a href="/documents/view/%d">Click here</a> if you are not redirected.</p>
</body>
</html>
`, 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, `<div class="has-text-centered has-text-grey">
<p>No recent activity</p>
</div>`)
return
}
// Build HTML table
fmt.Fprintf(w, `<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>Document</th>
<th>Reference</th>
<th>Created By</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>`)
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(" <span class=\"tag is-light\">Rev %d</span>", doc.Revision)
}
fmt.Fprintf(w, `<tr>
<td>
<span class="tag %s">%s</span>
</td>
<td>%s%s</td>
<td>%s</td>
<td><small>%s</small></td>
<td>
<a href="/documents/%d" class="button is-small is-outlined">
<span class="icon is-small">
<i class="fas fa-eye"></i>
</span>
<span>View</span>
</a>
</td>
</tr>`, typeClass, doc.DisplayName, doc.CmcReference, revisionText, createdBy, createdDate, doc.ID)
}
fmt.Fprintf(w, `</tbody></table>`)
// Add a link to view all documents
fmt.Fprintf(w, `<div class="has-text-centered mt-4">
<a href="/documents" class="button is-link is-outlined">
<span>View All Documents</span>
<span class="icon is-small">
<i class="fas fa-arrow-right"></i>
</span>
</a>
</div>`)
}