diff --git a/internal/handlers/web/admin.go b/internal/handlers/web/admin.go index c492237..733db6e 100644 --- a/internal/handlers/web/admin.go +++ b/internal/handlers/web/admin.go @@ -1,29 +1,18 @@ package web import ( - "log" root "marmic/servicetrade-toolbox" - "marmic/servicetrade-toolbox/internal/api" - "marmic/servicetrade-toolbox/internal/middleware" "net/http" ) func AdminHandler(w http.ResponseWriter, r *http.Request) { - session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - tmpl := root.WebTemplates data := map[string]interface{}{ - "Title": "Admin Dashboard", - "Session": session, + "Title": "Admin", } - if err := tmpl.ExecuteTemplate(w, "admin.html", data); err != nil { - log.Printf("Template execution error: %v", err) + err := tmpl.Execute(w, data) + if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return } } diff --git a/internal/handlers/web/assets.go b/internal/handlers/web/assets.go index b7e86a6..95a7c09 100644 --- a/internal/handlers/web/assets.go +++ b/internal/handlers/web/assets.go @@ -1,29 +1,18 @@ package web import ( - "log" root "marmic/servicetrade-toolbox" - "marmic/servicetrade-toolbox/internal/api" - "marmic/servicetrade-toolbox/internal/middleware" "net/http" ) func AssetsHandler(w http.ResponseWriter, r *http.Request) { - session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - tmpl := root.WebTemplates data := map[string]interface{}{ - "Title": "Admin Dashboard", - "Session": session, + "Title": "Assets", } - if err := tmpl.ExecuteTemplate(w, "assets.html", data); err != nil { - log.Printf("Template execution error: %v", err) + err := tmpl.Execute(w, data) + if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return } } diff --git a/internal/handlers/web/companies.go b/internal/handlers/web/companies.go index 940a40c..067c335 100644 --- a/internal/handlers/web/companies.go +++ b/internal/handlers/web/companies.go @@ -1,37 +1,18 @@ package web import ( - "log" root "marmic/servicetrade-toolbox" - "marmic/servicetrade-toolbox/internal/api" - "marmic/servicetrade-toolbox/internal/middleware" "net/http" ) func CompaniesHandler(w http.ResponseWriter, r *http.Request) { - session, ok := r.Context().Value(middleware.SessionKey).(*api.Session) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - tmpl := root.WebTemplates data := map[string]interface{}{ - "Title": "Companies", - "Session": session, - "View": "company_content", - } - - var err error - if r.Header.Get("HX-Request") == "true" { - err = tmpl.ExecuteTemplate(w, "companies.html", data) - } else { - err = tmpl.Execute(w, data) + "Title": "Companies", } + err := tmpl.Execute(w, data) if err != nil { - log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return } } diff --git a/internal/handlers/web/contacts.go b/internal/handlers/web/contacts.go index 468ba55..04ca63d 100644 --- a/internal/handlers/web/contacts.go +++ b/internal/handlers/web/contacts.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func ContactsHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/contacts.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Contacts", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/contracts.go b/internal/handlers/web/contracts.go index 444ffd0..42130fa 100644 --- a/internal/handlers/web/contracts.go +++ b/internal/handlers/web/contracts.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func ContractsHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/contracts.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Contracts", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/dashboard.go b/internal/handlers/web/dashboard.go index 7968bb5..45fa06d 100644 --- a/internal/handlers/web/dashboard.go +++ b/internal/handlers/web/dashboard.go @@ -12,20 +12,10 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { "Title": "Dashboard", } - if r.Header.Get("HX-Request") == "true" { - // For HTMX requests, execute the dashboard template directly - if err := tmpl.ExecuteTemplate(w, "dashboard.html", data); err != nil { - log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - } else { - // For full page requests, we'll use the layout template - // which will include the content template - if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { - log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } + // Always render the full layout with dashboard content + if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { + log.Printf("Template execution error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return } } diff --git a/internal/handlers/web/generic.go b/internal/handlers/web/generic.go index ffa24e0..6f0882b 100644 --- a/internal/handlers/web/generic.go +++ b/internal/handlers/web/generic.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func GenericHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/generic.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ - "Title": "Generic Tools", + "Title": "Generic", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/invoices.go b/internal/handlers/web/invoices.go index 4e88f30..fe2ce9e 100644 --- a/internal/handlers/web/invoices.go +++ b/internal/handlers/web/invoices.go @@ -43,24 +43,36 @@ func InvoicesHandler(w http.ResponseWriter, r *http.Request) { } if r.Method == "GET" { - handleInvoiceSearch(w, r, session) + // Check if this is a search request + searchTerm := strings.TrimSpace(r.URL.Query().Get("search")) + if searchTerm != "" { + handleInvoiceSearch(w, r, session) + return + } + + // This is a request for the main invoices page + tmpl := root.WebTemplates + data := map[string]interface{}{ + "Title": "Invoice Management", + } + + // Always render the full layout with invoices content + if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { + log.Printf("Template execution error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } return } + // Handle other HTTP methods if needed tmpl := root.WebTemplates data := map[string]interface{}{ - "Title": "Invoices", + "Title": "Invoice Management", } - var err error - - if r.Header.Get("HX-Request") == "true" { - err = tmpl.ExecuteTemplate(w, "content", data) - } else { - err = tmpl.ExecuteTemplate(w, "layout.html", data) - } - - if err != nil { + // Always render the full layout + if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return diff --git a/internal/handlers/web/jobs.go b/internal/handlers/web/jobs.go index c5ee264..9e9c514 100644 --- a/internal/handlers/web/jobs.go +++ b/internal/handlers/web/jobs.go @@ -26,6 +26,14 @@ func JobsHandler(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/jobs": + // For the main jobs page, render the full layout + err := tmpl.Execute(w, data) + if err != nil { + log.Printf("Template execution error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + + case "/jobs/search": // Fetch and display search results only jobs, err := handleJobSearch(r, session) if err != nil { diff --git a/internal/handlers/web/locations.go b/internal/handlers/web/locations.go index 734f5b1..34084f5 100644 --- a/internal/handlers/web/locations.go +++ b/internal/handlers/web/locations.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func LocationsHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/locations.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Locations", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/notifications.go b/internal/handlers/web/notifications.go index 1613470..c105d8c 100644 --- a/internal/handlers/web/notifications.go +++ b/internal/handlers/web/notifications.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func NotificationsHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/notifications.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Notifications", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/quotes.go b/internal/handlers/web/quotes.go index 90d8de5..297ef43 100644 --- a/internal/handlers/web/quotes.go +++ b/internal/handlers/web/quotes.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func QuotesHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/quotes.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Quotes", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/services.go b/internal/handlers/web/services.go index d3996eb..5f22bea 100644 --- a/internal/handlers/web/services.go +++ b/internal/handlers/web/services.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func ServicesHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/services.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Services", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/tags.go b/internal/handlers/web/tags.go index 576c8b6..a93c6bb 100644 --- a/internal/handlers/web/tags.go +++ b/internal/handlers/web/tags.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func TagsHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/tags.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Tags", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/internal/handlers/web/users.go b/internal/handlers/web/users.go index f5be8f9..1f6d49b 100644 --- a/internal/handlers/web/users.go +++ b/internal/handlers/web/users.go @@ -1,19 +1,18 @@ package web import ( - "html/template" + root "marmic/servicetrade-toolbox" "net/http" ) func UsersHandler(w http.ResponseWriter, r *http.Request) { - tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/users.html")) + tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Users", } - if r.Header.Get("HX-Request") == "true" { - tmpl.ExecuteTemplate(w, "content", data) - } else { - tmpl.Execute(w, data) + err := tmpl.Execute(w, data) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) } } diff --git a/static/css/styles.css b/static/css/styles.css index 6441eba..9c5dced 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -984,4 +984,552 @@ html { .drag-handle:active { cursor: grabbing; +} + +/* Enhanced Invoices Page Styles */ +.invoices-page { + padding: 0; + height: 100%; + display: flex; + flex-direction: column; +} + +.invoices-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 2px solid var(--content-border); +} + +.invoices-header h2 { + margin: 0; + color: var(--dashboard-header-color); +} + +.view-controls { + display: flex; + gap: 0.5rem; +} + +.view-btn { + padding: 0.5rem 1rem; + border: 2px solid var(--btn-primary-bg); + background: transparent; + color: var(--btn-primary-bg); + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; + transition: all 0.2s ease; +} + +.view-btn:hover { + background: var(--btn-primary-bg); + color: white; +} + +.view-btn.active { + background: var(--btn-primary-bg); + color: white; +} + +/* View Sections */ +.view-section { + display: none; + flex: 1; + min-height: 0; +} + +.view-section.active { + display: flex; + flex-direction: column; +} + +/* Enhanced Search Styles */ +.enhanced-search { + display: flex; + flex-direction: column; + height: 100%; +} + +.search-header { + margin-bottom: 1.5rem; +} + +.search-header h3 { + margin: 0 0 0.5rem 0; + color: var(--dashboard-header-color); +} + +.search-tips { + background: var(--content-bg); + padding: 0.75rem; + border-radius: 0.25rem; + border: 1px solid var(--btn-primary-bg); + color: var(--content-text); + font-size: 0.875rem; +} + +.search-container { + margin-bottom: 1.5rem; +} + +.search-input-group { + position: relative; + display: flex; + align-items: center; +} + +.enhanced-search-input { + width: 100%; + padding: 0.75rem 2.5rem 0.75rem 1rem; + border: 2px solid var(--input-border); + border-radius: 0.5rem; + background: var(--input-bg); + color: var(--input-text); + font-size: 1rem; + transition: border-color 0.2s ease; +} + +.enhanced-search-input:focus { + outline: none; + border-color: var(--btn-primary-bg); + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); +} + +.search-clear-btn { + position: absolute; + right: 0.75rem; + background: none; + border: none; + color: var(--btn-disabled); + cursor: pointer; + font-size: 1.25rem; + padding: 0.25rem; + border-radius: 50%; + transition: all 0.2s ease; +} + +.search-clear-btn:hover { + background: var(--btn-disabled); + color: white; +} + +.search-loading { + display: flex; + align-items: center; + gap: 0.75rem; + margin-top: 1rem; + color: var(--content-text); +} + +.loading-spinner { + width: 20px; + height: 20px; + border: 2px solid var(--progress-bg); + border-top: 2px solid var(--btn-primary-bg); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.search-results { + flex: 1; + overflow-y: auto; + min-height: 400px; +} + +.no-search-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 300px; + text-align: center; + color: var(--label-color); +} + +.no-search-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.no-search-state h4 { + margin: 0 0 0.5rem 0; + color: var(--dashboard-header-color); +} + +/* Kanban Board Styles */ +.board-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.board-header h3 { + margin: 0; + color: var(--dashboard-header-color); +} + +.board-controls { + display: flex; + gap: 0.75rem; +} + +.refresh-board-btn, +.load-recent-btn { + padding: 0.5rem 1rem; + background: var(--btn-success-bg); + color: white; + border: none; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; + transition: background-color 0.2s ease; +} + +.refresh-board-btn:hover, +.load-recent-btn:hover { + background: var(--btn-success-hover); +} + +.invoice-kanban-board { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 1rem; + height: calc(100vh - 280px); + min-height: 400px; +} + +.kanban-column { + background: var(--dashboard-bg); + border-radius: 0.5rem; + box-shadow: var(--dashboard-shadow); + display: flex; + flex-direction: column; + min-height: 0; +} + +.column-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid var(--content-border); + background: var(--content-bg); + border-radius: 0.5rem 0.5rem 0 0; +} + +.column-header h4 { + margin: 0; + font-size: 0.875rem; + color: var(--dashboard-header-color); +} + +.invoice-count { + background: var(--btn-primary-bg); + color: white; + padding: 0.25rem 0.5rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: bold; + min-width: 1.5rem; + text-align: center; +} + +.invoice-drop-zone { + flex: 1; + padding: 0.5rem; + min-height: 200px; + transition: background-color 0.2s ease; + overflow-y: auto; +} + +.invoice-drop-zone.drag-over { + background: rgba(66, 153, 225, 0.1); + border: 2px dashed var(--btn-primary-bg); + border-radius: 0.25rem; +} + +.empty-column-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 150px; + text-align: center; + color: var(--label-color); + opacity: 0.6; +} + +.empty-icon { + font-size: 2rem; + margin-bottom: 0.5rem; +} + +.empty-column-message p { + margin: 0; + font-size: 0.875rem; +} + +/* Board Invoice Card Styles */ +.board-invoice-card { + background: var(--input-bg); + border-radius: 0.5rem; + padding: 0.75rem; + margin-bottom: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + cursor: grab; + transition: all 0.2s ease; + position: relative; +} + +.board-invoice-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.board-invoice-card.dragging { + opacity: 0.5; + transform: rotate(5deg); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); +} + +.board-invoice-card:active { + cursor: grabbing; +} + +.board-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.invoice-number { + font-weight: bold; + color: var(--dashboard-header-color); + font-size: 0.875rem; +} + +.card-drag-handle { + color: var(--label-color); + font-size: 0.75rem; + cursor: grab; +} + +.card-drag-handle:active { + cursor: grabbing; +} + +.board-card-content { + margin-bottom: 0.75rem; +} + +.card-customer, +.card-job, +.card-total { + font-size: 0.75rem; + color: var(--content-text); + margin-bottom: 0.25rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.card-total { + font-weight: bold; + color: var(--btn-success-bg); +} + +.board-card-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.status-indicator { + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.625rem; + font-weight: bold; + text-transform: uppercase; +} + +.status-indicator.status-draft { + background: var(--btn-primary-bg); + color: white; +} + +.status-indicator.status-ok { + background: var(--btn-success-bg); + color: white; +} + +.status-indicator.status-pending_accounting { + background: var(--btn-caution-bg); + color: var(--text-color); +} + +.status-indicator.status-processed { + background: var(--btn-primary-bg); + color: white; + opacity: 0.8; +} + +.status-indicator.status-failed { + background: var(--btn-warning-bg); + color: white; +} + +.status-indicator.status-void { + background: var(--btn-disabled); + color: white; +} + +.card-details-btn { + background: none; + border: none; + cursor: pointer; + padding: 0.25rem; + border-radius: 0.25rem; + transition: background-color 0.2s ease; +} + +.card-details-btn:hover { + background: var(--content-bg); +} + +.board-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 2rem; + color: var(--content-text); +} + +.card-loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.5rem; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .invoice-kanban-board { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (max-width: 768px) { + .invoices-header { + flex-direction: column; + gap: 1rem; + align-items: stretch; + } + + .view-controls { + justify-content: center; + } + + .invoice-kanban-board { + grid-template-columns: repeat(2, 1fr); + } + + .board-controls { + flex-direction: column; + gap: 0.5rem; + } +} + +@media (max-width: 480px) { + .invoice-kanban-board { + grid-template-columns: 1fr; + } +} + +/* Page Header Styles */ +.page-header { + margin-bottom: 2rem; +} + +.page-header h2 { + color: var(--dashboard-header-color); + margin: 0 0 0.5rem 0; + font-size: 1.875rem; + font-weight: bold; +} + +.page-header p { + color: var(--content-text); + margin: 0; + font-size: 1rem; + opacity: 0.8; +} + +.page-content { + flex-grow: 1; +} + +.submenu-header { + color: var(--dashboard-header-color); + margin: 0 0 1rem 0; + font-size: 1.25rem; + font-weight: 600; +} + +/* Placeholder Content Styles */ +.placeholder-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + text-align: center; + background: var(--dashboard-bg); + border-radius: 0.75rem; + box-shadow: var(--dashboard-shadow); + margin-top: 2rem; +} + +.placeholder-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.placeholder-content h4 { + color: var(--dashboard-header-color); + margin: 0 0 0.5rem 0; + font-size: 1.25rem; + font-weight: 600; +} + +.placeholder-content p { + color: var(--content-text); + margin: 0; + font-size: 0.875rem; + opacity: 0.7; + max-width: 400px; + line-height: 1.5; } \ No newline at end of file diff --git a/static/js/invoices.js b/static/js/invoices.js new file mode 100644 index 0000000..56ef2cd --- /dev/null +++ b/static/js/invoices.js @@ -0,0 +1,716 @@ +// Enhanced Invoices Page Functionality +let currentView = 'search'; +let boardInvoices = new Map(); // Store invoice data for board view +let draggedInvoice = null; +let lastSearchResults = []; // Store the most recent search results + +// Initialize when page loads +document.addEventListener('DOMContentLoaded', function () { + initializeInvoicesPage(); +}); + +function initializeInvoicesPage() { + // Setup search input enhancements + const searchInput = document.getElementById('invoice-search-input'); + if (searchInput) { + setupSearchEnhancements(searchInput); + } + + // Initialize drag and drop for board view + initializeBoardDragDrop(); + + // Setup HTMX event listeners to capture search results + setupSearchResultCapture(); + + console.log('Invoices page initialized'); +} + +// Setup listeners to capture search results from HTMX +function setupSearchResultCapture() { + // Listen for HTMX after swap events to capture search results + document.body.addEventListener('htmx:afterSwap', function (e) { + if (e.detail.target && e.detail.target.id === 'invoice-search-results') { + console.log('Search results updated, extracting invoice data...'); + extractInvoiceDataFromSearchResults(); + } + }); +} + +// Extract invoice data from search results HTML +function extractInvoiceDataFromSearchResults() { + const resultsDiv = document.getElementById('invoice-search-results'); + const invoiceCards = resultsDiv.querySelectorAll('.invoice-card'); + + lastSearchResults = []; + + invoiceCards.forEach(card => { + const invoiceData = extractInvoiceDataFromCard(card); + if (invoiceData) { + lastSearchResults.push(invoiceData); + console.log('Extracted invoice data:', invoiceData); + } + }); + + console.log(`Captured ${lastSearchResults.length} invoices from search results`); + + // If we're currently in board view, update it immediately + if (currentView === 'board') { + populateBoardWithSearchResults(); + } +} + +// Extract invoice data from a single invoice card element +function extractInvoiceDataFromCard(card) { + try { + // Get invoice number from header + const headerElement = card.querySelector('.invoice-header h4'); + const invoiceNumberMatch = headerElement ? headerElement.textContent.match(/Invoice #(.+)/) : null; + const invoiceNumber = invoiceNumberMatch ? invoiceNumberMatch[1] : null; + + // Get status + const statusElement = card.querySelector('.invoice-status'); + const status = statusElement ? statusElement.textContent.trim() : null; + + // Get the real invoice ID from data attribute (this is the numeric ID we need for API calls) + const realInvoiceId = card.dataset.invoiceId; + + // If we don't have the real ID from data attribute, try to extract from card ID + let id = realInvoiceId; + if (!id) { + const cardId = card.id; + const idMatch = cardId.match(/invoice-card-(.+)/); + id = idMatch ? idMatch[1] : invoiceNumber; + } + + console.log(`Extracted invoice data: ID=${id}, Number=${invoiceNumber}, Status=${status}`); + + // Extract customer name + const customerElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => + p.innerHTML.includes('Customer:') + ); + const customerName = customerElement ? + customerElement.innerHTML.replace('Customer:', '').trim() : null; + + // Extract job name + const jobElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => + p.innerHTML.includes('Job:') + ); + const jobName = jobElement ? + jobElement.innerHTML.replace('Job:', '').trim() : null; + + // Extract total price + const totalElement = Array.from(card.querySelectorAll('.invoice-info p')).find(p => + p.innerHTML.includes('Total:') + ); + const totalMatch = totalElement ? + totalElement.innerHTML.match(/Total:<\/strong>\s*\$(.+)/) : null; + const totalPrice = totalMatch ? totalMatch[1].trim() : '0.00'; + + return { + id: id, + invoiceNumber: invoiceNumber, + status: status, + customer: customerName ? { name: customerName } : null, + job: jobName ? { name: jobName } : null, + totalPrice: totalPrice + }; + } catch (error) { + console.error('Error extracting invoice data from card:', error); + return null; + } +} + +// Search functionality enhancements +function setupSearchEnhancements(searchInput) { + const clearBtn = document.querySelector('.search-clear-btn'); + + searchInput.addEventListener('input', function () { + if (this.value.length > 0) { + clearBtn.style.display = 'block'; + } else { + clearBtn.style.display = 'none'; + } + }); + + // Add keyboard shortcuts + searchInput.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + clearSearch(); + } + }); +} + +function clearSearch() { + const searchInput = document.getElementById('invoice-search-input'); + const clearBtn = document.querySelector('.search-clear-btn'); + const resultsDiv = document.getElementById('invoice-search-results'); + + searchInput.value = ''; + clearBtn.style.display = 'none'; + + // Clear stored search results + lastSearchResults = []; + + // Reset to no search state + resultsDiv.innerHTML = ` +
+
๐Ÿ“‹
+

Start searching for invoices

+

Enter an invoice ID, customer name, or job number above to begin

+
+ `; + + // Clear board if currently in board view + if (currentView === 'board') { + clearBoard(); + } +} + +// View switching functionality +function switchView(viewName) { + const searchView = document.getElementById('search-view'); + const boardView = document.getElementById('board-view'); + const searchBtn = document.getElementById('search-view-btn'); + const boardBtn = document.getElementById('board-view-btn'); + + // Update active states + searchBtn.classList.toggle('active', viewName === 'search'); + boardBtn.classList.toggle('active', viewName === 'board'); + + // Show/hide views + searchView.classList.toggle('active', viewName === 'search'); + boardView.classList.toggle('active', viewName === 'board'); + + currentView = viewName; + + // If switching to board view, populate it with search results + if (viewName === 'board') { + if (lastSearchResults.length > 0) { + populateBoardWithSearchResults(); + } else { + checkBoardState(); + } + } + + console.log(`Switched to ${viewName} view`); +} + +// Clear all invoices from the board +function clearBoard() { + document.querySelectorAll('.invoice-drop-zone').forEach(zone => { + // Remove all invoice cards + const cards = zone.querySelectorAll('.board-invoice-card'); + cards.forEach(card => card.remove()); + + // Show empty message + const emptyMessage = zone.querySelector('.empty-column-message'); + if (emptyMessage) { + emptyMessage.style.display = 'block'; + } + }); + + // Clear stored data + boardInvoices.clear(); + + // Update counts + updateColumnCounts(); +} + +// Populate board with search results +function populateBoardWithSearchResults() { + console.log(`Populating board with ${lastSearchResults.length} search results`); + + // Clear existing board content + clearBoard(); + + // Add each search result to the board + lastSearchResults.forEach(invoiceData => { + addInvoiceToBoard(invoiceData); + }); + + // Debug the board state after population + debugBoardState(); + + showMessage(`Added ${lastSearchResults.length} invoices to board`, 'success'); +} + +// Board functionality +function refreshBoard() { + console.log('Refreshing board...'); + + // If we have search results, refresh from those + if (lastSearchResults.length > 0) { + showBoardLoading(true); + setTimeout(() => { + populateBoardWithSearchResults(); + showBoardLoading(false); + showMessage('Board refreshed with search results', 'success'); + }, 500); + } else { + showBoardLoading(true); + setTimeout(() => { + showBoardLoading(false); + showMessage('No search results to refresh. Search for invoices first.', 'info'); + }, 500); + } +} + +function loadRecentInvoices() { + console.log('Loading recent invoices...'); + showBoardLoading(true); + + // TODO: Implement actual API call to load recent invoices + // For now, show a helpful message + setTimeout(() => { + showBoardLoading(false); + showMessage('Search for invoices in the Search View to populate the board', 'info'); + }, 1000); +} + +function checkBoardState() { + const board = document.getElementById('invoice-board'); + const invoiceCards = board.querySelectorAll('.board-invoice-card'); + + if (invoiceCards.length === 0) { + console.log('Board is empty, showing helpful message'); + } +} + +function showBoardLoading(show) { + const loading = document.getElementById('board-loading'); + loading.style.display = show ? 'flex' : 'none'; +} + +// Kanban drag and drop functionality +function initializeBoardDragDrop() { + console.log('Initializing board drag and drop'); + + // Setup drop zones + const dropZones = document.querySelectorAll('.invoice-drop-zone'); + dropZones.forEach(zone => { + zone.addEventListener('dragover', handleDragOver); + zone.addEventListener('drop', handleDrop); + zone.addEventListener('dragenter', handleDragEnter); + zone.addEventListener('dragleave', handleDragLeave); + }); +} + +// Drag and drop event handlers +function handleDragStart(e) { + draggedInvoice = e.target.closest('.board-invoice-card'); + + // Only handle drag start for invoice board cards + if (!draggedInvoice) { + console.log('Drag start: No board invoice card found'); + return; + } + + draggedInvoice.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', draggedInvoice.dataset.invoiceId); + + console.log(`Started dragging invoice ${draggedInvoice.dataset.invoiceId} with status ${draggedInvoice.dataset.status}`); +} + +function handleDragOver(e) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; +} + +function handleDragEnter(e) { + e.preventDefault(); + const dropZone = e.target.closest('.invoice-drop-zone'); + + // Only handle if this is an invoice drop zone and we have a dragged invoice + if (dropZone && draggedInvoice && draggedInvoice.classList.contains('board-invoice-card')) { + dropZone.classList.add('drag-over'); + console.log(`Drag enter: ${dropZone.dataset.status} zone`); + + // Hide empty message when dragging over + const emptyMessage = dropZone.querySelector('.empty-column-message'); + if (emptyMessage) { + emptyMessage.style.display = 'none'; + } + } +} + +function handleDragLeave(e) { + const dropZone = e.target.closest('.invoice-drop-zone'); + + // Only handle if this is an invoice drop zone + if (dropZone) { + // Only remove drag-over if we're actually leaving the drop zone + const rect = dropZone.getBoundingClientRect(); + const x = e.clientX; + const y = e.clientY; + + if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { + dropZone.classList.remove('drag-over'); + + // Show empty message again if no cards in zone + const cards = dropZone.querySelectorAll('.board-invoice-card'); + const emptyMessage = dropZone.querySelector('.empty-column-message'); + if (cards.length === 0 && emptyMessage) { + emptyMessage.style.display = 'block'; + } + } + } +} + +function handleDrop(e) { + e.preventDefault(); + + const dropZone = e.target.closest('.invoice-drop-zone'); + + // If this isn't an invoice drop zone, ignore the event (let other handlers process it) + if (!dropZone) { + return; + } + + const newStatus = dropZone.dataset.status; + + if (!draggedInvoice || !newStatus) { + resetDragState(); + return; + } + + const invoiceId = draggedInvoice.dataset.invoiceId; + const currentStatus = draggedInvoice.dataset.status; + + console.log(`Dropping invoice ${invoiceId} into ${newStatus} column`); + + // Check if status change is valid + if (!isValidStatusTransition(currentStatus, newStatus)) { + showMessage(`Cannot move invoice from ${currentStatus} to ${newStatus}`, 'error'); + resetDragState(); + return; + } + + // If same status, just move visually + if (currentStatus === newStatus) { + moveInvoiceToColumn(draggedInvoice, dropZone); + resetDragState(); + return; + } + + // Update invoice status via API (resetDragState is called in the finally block) + updateInvoiceStatus(invoiceId, newStatus, dropZone); +} + +function handleDragEnd(e) { + resetDragState(); +} + +function resetDragState() { + if (draggedInvoice) { + draggedInvoice.classList.remove('dragging'); + draggedInvoice = null; + } + + // Remove drag-over classes + document.querySelectorAll('.invoice-drop-zone').forEach(zone => { + zone.classList.remove('drag-over'); + + // Show empty messages where appropriate + const cards = zone.querySelectorAll('.board-invoice-card'); + const emptyMessage = zone.querySelector('.empty-column-message'); + if (cards.length === 0 && emptyMessage) { + emptyMessage.style.display = 'block'; + } + }); +} + +function moveInvoiceToColumn(invoiceCard, dropZone) { + // Hide empty message + const emptyMessage = dropZone.querySelector('.empty-column-message'); + if (emptyMessage) { + emptyMessage.style.display = 'none'; + } + + // Move card to new column + dropZone.appendChild(invoiceCard); + + // Update column counts + updateColumnCounts(); + + console.log(`Moved invoice card to column ${dropZone.dataset.status}`); +} + +function updateInvoiceStatus(invoiceId, newStatus, dropZone) { + console.log(`Updating invoice ${invoiceId} to status ${newStatus}`); + + // Store reference to dragged invoice before any async operations + const invoiceCard = draggedInvoice; + + // Show loading state on the card + const loadingOverlay = document.createElement('div'); + loadingOverlay.className = 'card-loading-overlay'; + loadingOverlay.innerHTML = '
'; + invoiceCard.appendChild(loadingOverlay); + + // Make API call to update status + fetch(`/${newStatus}-invoice/${invoiceId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => { + console.log(`API response status: ${response.status}`); + if (response.ok) { + // Update successful - update the card's data and visual elements + invoiceCard.dataset.status = newStatus; + + // Update the status indicator inside the card + const statusIndicator = invoiceCard.querySelector('.status-indicator'); + if (statusIndicator) { + statusIndicator.className = `status-indicator status-${newStatus}`; + statusIndicator.textContent = newStatus; + } + + // Move card to new column + moveInvoiceToColumn(invoiceCard, dropZone); + + // Update the stored search results to reflect the status change + updateStoredSearchResults(invoiceId, newStatus); + + // Update the stored board invoice data + let boardInvoiceData = boardInvoices.get(invoiceId); + if (!boardInvoiceData) { + // Try looking up by invoice number if ID lookup failed + const invoiceNumber = invoiceCard.querySelector('.invoice-number')?.textContent?.replace('#', ''); + boardInvoiceData = boardInvoices.get(invoiceNumber); + } + + if (boardInvoiceData) { + boardInvoiceData.status = newStatus; + boardInvoices.set(invoiceId, boardInvoiceData); + // Also update by invoice number if we have it + if (boardInvoiceData.invoiceNumber) { + boardInvoices.set(boardInvoiceData.invoiceNumber, boardInvoiceData); + } + console.log(`Updated board invoice data for ${invoiceId} to status ${newStatus}`); + } else { + console.log(`Warning: Could not find board invoice data for ${invoiceId}`); + } + + showMessage(`Invoice moved to ${newStatus}`, 'success'); + console.log(`Successfully updated invoice ${invoiceId} to ${newStatus}`); + + // Reset drag state after successful update + resetDragState(); + } else { + // Try to get error details from response + return response.text().then(errorText => { + console.error(`API error: ${response.status} - ${errorText}`); + throw new Error(`Server error: ${response.status} - ${errorText}`); + }); + } + }) + .catch(error => { + console.error('Error updating invoice status:', error); + showMessage(`Failed to update invoice status: ${error.message}`, 'error'); + // Reset drag state on error + resetDragState(); + }) + .finally(() => { + // Remove loading overlay + if (loadingOverlay && loadingOverlay.parentNode) { + loadingOverlay.parentNode.removeChild(loadingOverlay); + } + }); +} + +// Update stored search results when status changes +function updateStoredSearchResults(invoiceId, newStatus) { + console.log(`Searching for invoice ${invoiceId} in stored search results...`); + + for (let i = 0; i < lastSearchResults.length; i++) { + const result = lastSearchResults[i]; + // Check both ID and invoice number for match + if (result.id === invoiceId || result.id === invoiceId.toString() || + result.invoiceNumber === invoiceId || result.invoiceNumber === invoiceId.toString()) { + lastSearchResults[i].status = newStatus; + console.log(`Updated stored search result for invoice ${invoiceId} to status ${newStatus}`); + return; + } + } + console.log(`Warning: Could not find invoice ${invoiceId} in stored search results`); +} + +// Status transition validation +function isValidStatusTransition(from, to) { + const transitions = { + 'draft': ['ok', 'void'], + 'ok': ['draft', 'failed', 'pending_accounting', 'processed', 'void'], + 'failed': ['draft', 'ok', 'void'], + 'pending_accounting': ['failed', 'processed', 'void'], + 'processed': ['void'], + 'void': [] // Cannot transition from void + }; + + return transitions[from] && transitions[from].includes(to); +} + +function updateColumnCounts() { + document.querySelectorAll('.kanban-column').forEach(column => { + const status = column.dataset.status; + const cards = column.querySelectorAll('.board-invoice-card'); + const countElement = column.querySelector('.invoice-count'); + + if (countElement) { + countElement.textContent = cards.length; + } + }); +} + +// Add invoice to board (called when loading invoices) +function addInvoiceToBoard(invoiceData) { + const status = invoiceData.status; + const dropZone = document.querySelector(`[data-status="${status}"] .invoice-drop-zone`); + + if (!dropZone) { + console.error(`No drop zone found for status: ${status}`); + return; + } + + // Create invoice card element + const cardHTML = createBoardCardHTML(invoiceData); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = cardHTML; + const card = tempDiv.firstElementChild; + + // Add drag event listeners + card.addEventListener('dragstart', handleDragStart); + card.addEventListener('dragend', handleDragEnd); + + console.log(`Added drag event listeners to invoice card ${invoiceData.id}`); + console.log(`Card draggable attribute: ${card.getAttribute('draggable')}`); + console.log(`Card data-invoice-id: ${card.dataset.invoiceId}`); + console.log(`Card data-status: ${card.dataset.status}`); + + // Hide empty message + const emptyMessage = dropZone.querySelector('.empty-column-message'); + if (emptyMessage) { + emptyMessage.style.display = 'none'; + } + + // Add to drop zone + dropZone.appendChild(card); + + // Store invoice data with both ID and invoice number as keys for easier lookup + const invoiceId = invoiceData.id; + boardInvoices.set(invoiceId, invoiceData); + + // Also store by invoice number if different from ID + if (invoiceData.invoiceNumber && invoiceData.invoiceNumber !== invoiceId) { + boardInvoices.set(invoiceData.invoiceNumber, invoiceData); + } + + console.log(`Stored board invoice data for ID: ${invoiceId}, Number: ${invoiceData.invoiceNumber}`); + + // Update counts + updateColumnCounts(); +} + +function createBoardCardHTML(invoice) { + return ` +
+
+
#${invoice.invoiceNumber}
+
โ‹ฎโ‹ฎ
+
+ +
+ ${invoice.customer ? `
๐Ÿ‘ค ${invoice.customer.name}
` : ''} + ${invoice.job ? `
๐Ÿ”ง ${invoice.job.name}
` : ''} +
๐Ÿ’ฐ $${invoice.totalPrice}
+
+ + +
+ `; +} + +// Invoice details modal (placeholder) +function showInvoiceDetails(invoiceId) { + console.log(`Showing details for invoice ${invoiceId}`); + // TODO: Implement invoice details modal + showMessage(`Details for invoice ${invoiceId}`, 'info'); +} + +// Utility function for showing messages +function showMessage(text, type = 'info') { + const msg = document.createElement('div'); + msg.textContent = text; + msg.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 12px 20px; + border-radius: 4px; + color: white; + font-weight: bold; + z-index: 10000; + transform: translateX(300px); + transition: transform 0.3s ease; + background: ${getMessageColor(type)}; + `; + + document.body.appendChild(msg); + + // Show message + setTimeout(() => msg.style.transform = 'translateX(0)', 100); + + // Hide message + setTimeout(() => { + msg.style.transform = 'translateX(300px)'; + setTimeout(() => { + if (document.body.contains(msg)) { + document.body.removeChild(msg); + } + }, 300); + }, 3000); +} + +function getMessageColor(type) { + const colors = { + 'success': '#10b981', + 'error': '#ef4444', + 'warning': '#f59e0b', + 'info': '#3b82f6' + }; + return colors[type] || colors.info; +} + +// Debug function to check board state +function debugBoardState() { + console.log('=== BOARD DEBUG INFO ==='); + console.log(`Current view: ${currentView}`); + console.log(`Last search results count: ${lastSearchResults.length}`); + console.log(`Board invoices stored: ${boardInvoices.size}`); + + const allCards = document.querySelectorAll('.board-invoice-card'); + console.log(`Board cards in DOM: ${allCards.length}`); + + allCards.forEach((card, index) => { + console.log(`Card ${index}: ID=${card.dataset.invoiceId}, Status=${card.dataset.status}, Draggable=${card.getAttribute('draggable')}`); + }); + + const dropZones = document.querySelectorAll('.invoice-drop-zone'); + dropZones.forEach(zone => { + const status = zone.dataset.status; + const cardsInZone = zone.querySelectorAll('.board-invoice-card').length; + console.log(`Drop zone ${status}: ${cardsInZone} cards`); + }); + + console.log('========================'); +} + +// Make debug function globally accessible for browser console +window.debugBoardState = debugBoardState; \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..03ce0ae --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,10 @@ +{{define "admin_content"}} + + +
+ {{template "admin" .}} +
+{{end}} \ No newline at end of file diff --git a/templates/assets.html b/templates/assets.html new file mode 100644 index 0000000..385c032 --- /dev/null +++ b/templates/assets.html @@ -0,0 +1,10 @@ +{{define "assets_content"}} + + +
+ {{template "assets" .}} +
+{{end}} \ No newline at end of file diff --git a/templates/companies.html b/templates/companies.html new file mode 100644 index 0000000..1c1dd03 --- /dev/null +++ b/templates/companies.html @@ -0,0 +1,10 @@ +{{define "companies_content"}} + + +
+ {{template "companies" .}} +
+{{end}} \ No newline at end of file diff --git a/templates/contacts.html b/templates/contacts.html new file mode 100644 index 0000000..54f8ba4 --- /dev/null +++ b/templates/contacts.html @@ -0,0 +1,18 @@ +{{define "contacts_content"}} + + +
+
+ +

Contact management functionality will be implemented here.

+
+
๐Ÿ‘ฅ
+

Coming Soon

+

Contact management features are under development.

+
+
+
+{{end}} \ No newline at end of file diff --git a/templates/contracts.html b/templates/contracts.html new file mode 100644 index 0000000..92dce56 --- /dev/null +++ b/templates/contracts.html @@ -0,0 +1,18 @@ +{{define "contracts_content"}} + + +
+
+ +

Contract management functionality will be implemented here.

+
+
๐Ÿ“‹
+

Coming Soon

+

Contract management features are under development.

+
+
+
+{{end}} \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index 05572b8..0aadd91 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,4 +1,4 @@ -{{define "content"}} +{{define "dashboard_content"}}

Dashboard

+ +
+ + + +
+ +
+ + +
+
+

Invoice Status Board

+
+

๐Ÿ’ก Tip: Search for invoices in the Search View first, then switch + to Board View to see them organized by status.

+
+
+ + +
+
+ +
+
+
+

๐Ÿ“ Draft

+ 0 +
+
+
+
๐Ÿ“
+

No draft invoices

+
+
+
+ +
+
+

โœ… OK

+ 0 +
+
+
+
โœ…
+

No OK invoices

+
+
+
+ +
+
+

๐Ÿงพ Pending

+ 0 +
+
+
+
๐Ÿงพ
+

No pending invoices

+
+
+
+ +
+
+

๐Ÿ’ฐ Processed

+ 0 +
+
+
+
๐Ÿ’ฐ
+

No processed invoices

+
+
+
+ +
+
+

โŒ Failed

+ 0 +
+
+
+
โŒ
+

No failed invoices

+
+
+
+ +
+
+

๐Ÿ’ฃ Void

+ 0 +
+
+
+
๐Ÿ’ฃ
+

No void invoices

+
+
+
+
+ + +
+ + + +{{end}} \ No newline at end of file diff --git a/templates/jobs.html b/templates/jobs.html new file mode 100644 index 0000000..08e0e82 --- /dev/null +++ b/templates/jobs.html @@ -0,0 +1,10 @@ +{{define "jobs_content"}} + + +
+ {{template "jobs" .}} +
+{{end}} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 0509e08..c246776 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -17,21 +17,21 @@

ServiceTrade Tools

@@ -47,8 +47,36 @@
{{if .BodyContent}} {{.BodyContent}} + {{else if eq .Title "Invoice Management"}} + {{template "invoices_content" .}} + {{else if eq .Title "Jobs"}} + {{template "jobs_content" .}} + {{else if eq .Title "Assets"}} + {{template "assets_content" .}} + {{else if eq .Title "Companies"}} + {{template "companies_content" .}} + {{else if eq .Title "Contacts"}} + {{template "contacts_content" .}} + {{else if eq .Title "Contracts"}} + {{template "contracts_content" .}} + {{else if eq .Title "Generic"}} + {{template "generic_content" .}} + {{else if eq .Title "Locations"}} + {{template "locations_content" .}} + {{else if eq .Title "Notifications"}} + {{template "notifications_content" .}} + {{else if eq .Title "Quotes"}} + {{template "quotes_content" .}} + {{else if eq .Title "Services"}} + {{template "services_content" .}} + {{else if eq .Title "Tags"}} + {{template "tags_content" .}} + {{else if eq .Title "Users"}} + {{template "users_content" .}} + {{else if eq .Title "Admin"}} + {{template "admin_content" .}} {{else}} - {{template "content" .}} + {{template "dashboard_content" .}} {{end}}
diff --git a/templates/locations.html b/templates/locations.html new file mode 100644 index 0000000..5512233 --- /dev/null +++ b/templates/locations.html @@ -0,0 +1,18 @@ +{{define "locations_content"}} + + +
+
+ +

Location management functionality will be implemented here.

+
+
๐Ÿ“
+

Coming Soon

+

Location management features are under development.

+
+
+
+{{end}} \ No newline at end of file diff --git a/templates/notifications.html b/templates/notifications.html new file mode 100644 index 0000000..7d76c59 --- /dev/null +++ b/templates/notifications.html @@ -0,0 +1,18 @@ +{{define "notifications_content"}} + + +
+
+ +

Notification management functionality will be implemented here.

+
+
๐Ÿ””
+

Coming Soon

+

Notification management features are under development.

+
+
+
+{{end}} \ No newline at end of file diff --git a/templates/partials/document_upload.html b/templates/partials/document_upload.html index f74f76d..e7f6a88 100644 --- a/templates/partials/document_upload.html +++ b/templates/partials/document_upload.html @@ -127,11 +127,16 @@