565 lines
16 KiB
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>`)
|
|
} |