prod #123
|
|
@ -52,8 +52,8 @@ func main() {
|
|||
log.Fatal("Failed to initialize templates:", err)
|
||||
}
|
||||
|
||||
// Only pageHandler is needed
|
||||
pageHandler := handlers.NewPageHandler(queries, tmpl)
|
||||
// Load handlers
|
||||
quoteHandler := handlers.ExpiringQuotesHandler(queries, tmpl)
|
||||
|
||||
// Setup routes
|
||||
r := mux.NewRouter()
|
||||
|
|
@ -65,7 +65,7 @@ func main() {
|
|||
// PDF files
|
||||
goRouter.PathPrefix("/pdf/").Handler(http.StripPrefix("/go/pdf/", http.FileServer(http.Dir("webroot/pdf"))))
|
||||
|
||||
goRouter.HandleFunc("/quotes", pageHandler.QuotesView).Methods("GET")
|
||||
goRouter.HandleFunc("/quotes", quoteHandler.ExpiringQuotesView).Methods("GET")
|
||||
|
||||
// The following routes are currently disabled:
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -333,6 +333,28 @@ type PurchaseOrder struct {
|
|||
ParentPurchaseOrderID int32 `json:"parent_purchase_order_id"`
|
||||
}
|
||||
|
||||
type Quote struct {
|
||||
Created time.Time `json:"created"`
|
||||
Modified time.Time `json:"modified"`
|
||||
ID int32 `json:"id"`
|
||||
EnquiryID int32 `json:"enquiry_id"`
|
||||
CurrencyID int32 `json:"currency_id"`
|
||||
// limited at 5 digits. Really, you're not going to have more revisions of a single quote than that
|
||||
Revision int32 `json:"revision"`
|
||||
// estimated delivery time for quote
|
||||
DeliveryTime string `json:"delivery_time"`
|
||||
DeliveryTimeFrame string `json:"delivery_time_frame"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
DaysValid int32 `json:"days_valid"`
|
||||
DateIssued time.Time `json:"date_issued"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
DeliveryPoint string `json:"delivery_point"`
|
||||
ExchangeRate string `json:"exchange_rate"`
|
||||
CustomsDuty string `json:"customs_duty"`
|
||||
DocumentID int32 `json:"document_id"`
|
||||
CommercialComments sql.NullString `json:"commercial_comments"`
|
||||
}
|
||||
|
||||
type State struct {
|
||||
ID int32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type Querier interface {
|
|||
GetEnquiriesByCustomer(ctx context.Context, arg GetEnquiriesByCustomerParams) ([]GetEnquiriesByCustomerRow, error)
|
||||
GetEnquiriesByUser(ctx context.Context, arg GetEnquiriesByUserParams) ([]GetEnquiriesByUserRow, error)
|
||||
GetEnquiry(ctx context.Context, id int32) (GetEnquiryRow, error)
|
||||
GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]GetExpiringSoonQuotesRow, error)
|
||||
GetLineItem(ctx context.Context, id int32) (LineItem, error)
|
||||
GetLineItemsByProduct(ctx context.Context, productID sql.NullInt32) ([]LineItem, error)
|
||||
GetLineItemsTable(ctx context.Context, documentID int32) ([]GetLineItemsTableRow, error)
|
||||
|
|
@ -74,6 +75,7 @@ type Querier interface {
|
|||
GetPurchaseOrderRevisions(ctx context.Context, parentPurchaseOrderID int32) ([]PurchaseOrder, error)
|
||||
GetPurchaseOrdersByPrinciple(ctx context.Context, arg GetPurchaseOrdersByPrincipleParams) ([]PurchaseOrder, error)
|
||||
GetRecentDocuments(ctx context.Context, limit int32) ([]GetRecentDocumentsRow, error)
|
||||
GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesRow, error)
|
||||
GetState(ctx context.Context, id int32) (State, error)
|
||||
GetStatus(ctx context.Context, id int32) (Status, error)
|
||||
GetUser(ctx context.Context, id int32) (GetUserRow, error)
|
||||
|
|
|
|||
109
go-app/internal/cmc/db/quotes.sql.go
Normal file
109
go-app/internal/cmc/db/quotes.sql.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: quotes.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
const getExpiringSoonQuotes = `-- name: GetExpiringSoonQuotes :many
|
||||
SELECT d.id, u.username, e.id, e.title, q.date_issued, q.valid_until
|
||||
FROM quotes q
|
||||
JOIN documents d on d.id = q.document_id
|
||||
JOIN users u on u.id = d.user_id
|
||||
JOIN enquiries e on e.id = q.enquiry_id
|
||||
WHERE valid_until >= CURRENT_DATE AND valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
ORDER BY valid_until DESC
|
||||
`
|
||||
|
||||
type GetExpiringSoonQuotesRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
ID_2 int32 `json:"id_2"`
|
||||
Title string `json:"title"`
|
||||
DateIssued time.Time `json:"date_issued"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetExpiringSoonQuotes(ctx context.Context, dateADD interface{}) ([]GetExpiringSoonQuotesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getExpiringSoonQuotes, dateADD)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetExpiringSoonQuotesRow{}
|
||||
for rows.Next() {
|
||||
var i GetExpiringSoonQuotesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.ID_2,
|
||||
&i.Title,
|
||||
&i.DateIssued,
|
||||
&i.ValidUntil,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getRecentlyExpiredQuotes = `-- name: GetRecentlyExpiredQuotes :many
|
||||
SELECT d.id, u.username, e.id, e.title, q.date_issued, q.valid_until
|
||||
FROM quotes q
|
||||
JOIN documents d on d.id = q.document_id
|
||||
JOIN users u on u.id = d.user_id
|
||||
JOIN enquiries e on e.id = q.enquiry_id
|
||||
WHERE valid_until < CURRENT_DATE AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
ORDER BY valid_until DESC
|
||||
`
|
||||
|
||||
type GetRecentlyExpiredQuotesRow struct {
|
||||
ID int32 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
ID_2 int32 `json:"id_2"`
|
||||
Title string `json:"title"`
|
||||
DateIssued time.Time `json:"date_issued"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetRecentlyExpiredQuotes(ctx context.Context, dateSUB interface{}) ([]GetRecentlyExpiredQuotesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getRecentlyExpiredQuotes, dateSUB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetRecentlyExpiredQuotesRow{}
|
||||
for rows.Next() {
|
||||
var i GetRecentlyExpiredQuotesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Username,
|
||||
&i.ID_2,
|
||||
&i.Title,
|
||||
&i.DateIssued,
|
||||
&i.ValidUntil,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
|
@ -813,10 +813,3 @@ func (h *PageHandler) DocumentsView(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Quotes view page
|
||||
func (h *PageHandler) QuotesView(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.tmpl.Render(w, "quotes/index.html", nil); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
110
go-app/internal/cmc/handlers/quotes.go
Normal file
110
go-app/internal/cmc/handlers/quotes.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
|
||||
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/templates"
|
||||
)
|
||||
|
||||
type QuotesHandler struct {
|
||||
queries *db.Queries
|
||||
tmpl *templates.TemplateManager
|
||||
}
|
||||
|
||||
func ExpiringQuotesHandler(queries *db.Queries, tmpl *templates.TemplateManager) *QuotesHandler {
|
||||
return &QuotesHandler{
|
||||
queries: queries,
|
||||
tmpl: tmpl,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuotesHandler) ExpiringQuotesView(w http.ResponseWriter, r *http.Request) {
|
||||
days := int32(14)
|
||||
|
||||
recentlyExpiredQuotes, err := h.queries.GetRecentlyExpiredQuotes(r.Context(), days)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
expiringSoonQuotes, err := h.queries.GetExpiringSoonQuotes(r.Context(), days)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
calcExpiryInfo := func(validUntil time.Time) (string, int, int) {
|
||||
now := time.Now()
|
||||
daysUntil := int(validUntil.Sub(now).Hours() / 24)
|
||||
daysSince := int(now.Sub(validUntil).Hours() / 24)
|
||||
var relative string
|
||||
if validUntil.After(now) {
|
||||
if daysUntil == 0 {
|
||||
relative = "expires today"
|
||||
} else if daysUntil == 1 {
|
||||
relative = "in 1 day"
|
||||
} else {
|
||||
relative = "in " + strconv.Itoa(daysUntil) + " days"
|
||||
}
|
||||
} else {
|
||||
if daysSince == 0 {
|
||||
relative = "expired today"
|
||||
} else if daysSince == 1 {
|
||||
relative = "expired 1 day ago"
|
||||
} else {
|
||||
relative = "expired " + strconv.Itoa(daysSince) + " days ago"
|
||||
}
|
||||
}
|
||||
return relative, daysUntil, daysSince
|
||||
}
|
||||
|
||||
formatQuote := func(q db.GetRecentlyExpiredQuotesRow) map[string]interface{} {
|
||||
relative, daysUntil, daysSince := calcExpiryInfo(q.ValidUntil)
|
||||
return map[string]interface{}{
|
||||
"ID": q.ID,
|
||||
"Username": strings.Title(q.Username),
|
||||
"EnquiryID": q.ID_2,
|
||||
"EnquiryTitle": q.Title,
|
||||
"DateIssued": q.DateIssued.Format("2006-01-02"),
|
||||
"ValidUntil": q.ValidUntil.Format("2006-01-02"),
|
||||
"ValidUntilRelative": relative,
|
||||
"DaysUntilExpiry": daysUntil,
|
||||
"DaysSinceExpiry": daysSince,
|
||||
}
|
||||
}
|
||||
formatSoonQuote := func(q db.GetExpiringSoonQuotesRow) map[string]interface{} {
|
||||
relative, daysUntil, daysSince := calcExpiryInfo(q.ValidUntil)
|
||||
return map[string]interface{}{
|
||||
"ID": q.ID,
|
||||
"Username": strings.ToUpper(q.Username),
|
||||
"EnquiryID": q.ID_2,
|
||||
"EnquiryTitle": q.Title,
|
||||
"DateIssued": q.DateIssued.Format("2006-01-02"),
|
||||
"ValidUntil": q.ValidUntil.Format("2006-01-02"),
|
||||
"ValidUntilRelative": relative,
|
||||
"DaysUntilExpiry": daysUntil,
|
||||
"DaysSinceExpiry": daysSince,
|
||||
}
|
||||
}
|
||||
|
||||
var expiredRows []map[string]interface{}
|
||||
for _, q := range recentlyExpiredQuotes {
|
||||
expiredRows = append(expiredRows, formatQuote(q))
|
||||
}
|
||||
var soonRows []map[string]interface{}
|
||||
for _, q := range expiringSoonQuotes {
|
||||
soonRows = append(soonRows, formatSoonQuote(q))
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"RecentlyExpiredQuotes": expiredRows,
|
||||
"ExpiringSoonQuotes": soonRows,
|
||||
}
|
||||
if err := h.tmpl.Render(w, "quotes/index.html", data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
17
go-app/sql/queries/quotes.sql
Normal file
17
go-app/sql/queries/quotes.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-- name: GetRecentlyExpiredQuotes :many
|
||||
SELECT d.id, u.username, e.id, e.title, q.date_issued, q.valid_until
|
||||
FROM quotes q
|
||||
JOIN documents d on d.id = q.document_id
|
||||
JOIN users u on u.id = d.user_id
|
||||
JOIN enquiries e on e.id = q.enquiry_id
|
||||
WHERE valid_until < CURRENT_DATE AND valid_until >= DATE_SUB(CURRENT_DATE, INTERVAL ? DAY)
|
||||
ORDER BY valid_until DESC;
|
||||
|
||||
-- name: GetExpiringSoonQuotes :many
|
||||
SELECT d.id, u.username, e.id, e.title, q.date_issued, q.valid_until
|
||||
FROM quotes q
|
||||
JOIN documents d on d.id = q.document_id
|
||||
JOIN users u on u.id = d.user_id
|
||||
JOIN enquiries e on e.id = q.enquiry_id
|
||||
WHERE valid_until >= CURRENT_DATE AND valid_until <= DATE_ADD(CURRENT_DATE, INTERVAL ? DAY)
|
||||
ORDER BY valid_until DESC;
|
||||
22
go-app/sql/schema/014_quotes.sql
Normal file
22
go-app/sql/schema/014_quotes.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- cmc.quotes definition (matches existing database)
|
||||
|
||||
CREATE TABLE `quotes` (
|
||||
`created` datetime NOT NULL,
|
||||
`modified` datetime NOT NULL,
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`enquiry_id` int(50) NOT NULL,
|
||||
`currency_id` int(11) NOT NULL,
|
||||
`revision` int(5) NOT NULL COMMENT 'limited at 5 digits. Really, you''re not going to have more revisions of a single quote than that',
|
||||
`delivery_time` varchar(400) NOT NULL COMMENT 'estimated delivery time for quote',
|
||||
`delivery_time_frame` varchar(100) NOT NULL,
|
||||
`payment_terms` varchar(400) NOT NULL,
|
||||
`days_valid` int(3) NOT NULL,
|
||||
`date_issued` date NOT NULL,
|
||||
`valid_until` date NOT NULL,
|
||||
`delivery_point` varchar(400) NOT NULL,
|
||||
`exchange_rate` varchar(255) NOT NULL,
|
||||
`customs_duty` varchar(255) NOT NULL,
|
||||
`document_id` int(11) NOT NULL,
|
||||
`commercial_comments` text DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=MyISAM AUTO_INCREMENT=18245 DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
|
||||
|
|
@ -1,4 +1,61 @@
|
|||
{{define "content"}}
|
||||
<h1 class="text-2xl font-bold mb-4">Quotes</h1>
|
||||
<p>Welcome to the Quotes page. Here you can view and manage your quotes.</p>
|
||||
<h1 class="text-3xl font-bold mb-4 text-gray-800">Quotes Expiring</h1>
|
||||
<div class="pl-4">
|
||||
<!-- Expiring Soon Section -->
|
||||
<h2 class="text-xl font-semibold mt-6 mb-2">Expiring Soon</h2>
|
||||
<table class="min-w-full border text-center align-middle mt-1 ml-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Issued By</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Enquiry</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Date Issued</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Valid Until</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .ExpiringSoonQuotes}}
|
||||
<tr class="hover:bg-slate-50 transition">
|
||||
<td class="px-4 py-2 border align-middle"><a href="/documents/view/{{.ID}}" class="text-blue-600 underline">{{.ID}}</a></td>
|
||||
<td class="px-4 py-2 border align-middle">{{.Username}}</td>
|
||||
<td class="px-4 py-2 border align-middle"><a href="/enquiries/{{.EnquiryID}}" class="text-blue-600 underline">{{.EnquiryTitle}}</a></td>
|
||||
<td class="px-4 py-2 border align-middle">{{.DateIssued}}</td>
|
||||
<td class="px-4 py-2 border align-middle">{{.ValidUntil}} <span class="text-gray-500">({{.ValidUntilRelative}})</span></td>
|
||||
<td class="px-4 py-2 border align-middle"><span class="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-gray-200 text-gray-700">Unsent</span></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6" class="px-4 py-2 border text-center align-middle">No quotes expiring soon.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Recently Expired Quotes Section -->
|
||||
<h2 class="text-xl font-semibold mt-6 mb-2">Recently Expired</h2>
|
||||
<table class="min-w-full border mb-6 text-center align-middle mt-1 ml-4">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Quote</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Issued By</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Enquiry</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Date Issued</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Valid Until</th>
|
||||
<th class="px-4 py-3 border font-semibold text-gray-700 align-middle">Reminder</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .RecentlyExpiredQuotes}}
|
||||
<tr class="hover:bg-slate-50 transition">
|
||||
<td class="px-4 py-2 border align-middle"><a href="/documents/view/{{.ID}}" class="text-blue-600 underline">{{.ID}}</a></td>
|
||||
<td class="px-4 py-2 border align-middle">{{.Username}}</td>
|
||||
<td class="px-4 py-2 border align-middle"><a href="/enquiries/view/{{.EnquiryID}}" class="text-blue-600 underline">{{.EnquiryTitle}}</a></td>
|
||||
<td class="px-4 py-2 border align-middle">{{.DateIssued}}</td>
|
||||
<td class="px-4 py-2 border align-middle">{{.ValidUntil}} <span class="text-gray-500">({{.ValidUntilRelative}})</span></td>
|
||||
<td class="px-4 py-2 border align-middle"><span class="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-gray-200 text-gray-700">Unsent</span></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6" class="px-4 py-2 border text-center align-middle">No recently expired quotes.</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue