diff --git a/Readme.md b/Readme.md index aafb400..4ed3dda 100644 --- a/Readme.md +++ b/Readme.md @@ -3,7 +3,7 @@ ## Project Structure ```project_root/ -├── cmd/ +├── apps/ │ ├── cli/ │ │ └── main.go │ └── web/ diff --git a/internal/api/attachments.go b/internal/api/attachments.go deleted file mode 100644 index da96146..0000000 --- a/internal/api/attachments.go +++ /dev/null @@ -1,72 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "marmic/servicetrade-toolbox/internal/auth" - "strings" -) - -func GetAttachmentsForJob(session *auth.Session, jobID string) (map[string]interface{}, error) { - url := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID) - req, err := AuthenticatedRequest(session, "GET", url, nil) - if err != nil { - return nil, fmt.Errorf("error creating request: %v", err) - } - - resp, err := DoAuthenticatedRequest(session, req) - if err != nil { - return nil, fmt.Errorf("error sending request: %v", err) - } - defer resp.Body.Close() - - var result map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("error decoding response: %v", err) - } - - return result, nil -} - -func GenerateDeleteEndpoints(data map[string]interface{}, filenames []string) []string { - var endpoints []string - filenamesToDeleteMap := make(map[string]struct{}) - for _, name := range filenames { - filenamesToDeleteMap[strings.ToLower(strings.TrimSpace(name))] = struct{}{} - } - - if dataMap, ok := data["data"].(map[string]interface{}); ok { - if attachments, ok := dataMap["attachments"].([]interface{}); ok { - for _, item := range attachments { - attachment := item.(map[string]interface{}) - if filename, ok := attachment["fileName"].(string); ok { - trimmedFilename := strings.ToLower(strings.TrimSpace(filename)) - if _, exists := filenamesToDeleteMap[trimmedFilename]; exists { - endpoints = append(endpoints, fmt.Sprintf("%s/attachment/%d", BaseURL, int64(attachment["id"].(float64)))) - } - } - } - } - } - - return endpoints -} - -func DeleteAttachment(session *auth.Session, endpoint string) error { - req, err := AuthenticatedRequest(session, "DELETE", endpoint, nil) - if err != nil { - return fmt.Errorf("failed to create DELETE request: %v", err) - } - - resp, err := DoAuthenticatedRequest(session, req) - if err != nil { - return fmt.Errorf("failed to send DELETE request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 && resp.StatusCode != 204 { - return fmt.Errorf("failed to delete attachment: %s", resp.Status) - } - - return nil -} diff --git a/internal/api/common.go b/internal/api/common.go deleted file mode 100644 index 3245cb6..0000000 --- a/internal/api/common.go +++ /dev/null @@ -1,20 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "net/http" -) - -const BaseURL = "https://api.servicetrade.com/api" - -// DecodeJSONResponse decodes a JSON response into the provided interface -func DecodeJSONResponse(resp *http.Response, v interface{}) error { - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - - return json.NewDecoder(resp.Body).Decode(v) -} diff --git a/internal/api/invoices.go b/internal/api/invoices.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/api/jobs.go b/internal/api/jobs.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/api/session_store.go b/internal/api/session_store.go deleted file mode 100644 index 425a3b9..0000000 --- a/internal/api/session_store.go +++ /dev/null @@ -1,46 +0,0 @@ -package api - -import ( - "sync" - "time" -) - -type SessionStore struct { - sessions map[string]*Session - mu sync.RWMutex -} - -func NewSessionStore() *SessionStore { - return &SessionStore{ - sessions: make(map[string]*Session), - } -} - -func (store *SessionStore) Set(sessionID string, session *Session) { - store.mu.Lock() - defer store.mu.Unlock() - store.sessions[sessionID] = session -} - -func (store *SessionStore) Get(sessionID string) (*Session, bool) { - store.mu.RLock() - defer store.mu.RUnlock() - session, ok := store.sessions[sessionID] - return session, ok -} - -func (store *SessionStore) Delete(sessionID string) { - store.mu.Lock() - defer store.mu.Unlock() - delete(store.sessions, sessionID) -} - -func (store *SessionStore) CleanupSessions() { - store.mu.Lock() - defer store.mu.Unlock() - for id, session := range store.sessions { - if time.Since(session.LastAccessed) > 24*time.Hour { - delete(store.sessions, id) - } - } -} diff --git a/internal/handlers/admin.go b/internal/handlers/cli/admin.go similarity index 100% rename from internal/handlers/admin.go rename to internal/handlers/cli/admin.go diff --git a/internal/handlers/assets.go b/internal/handlers/cli/assets.go similarity index 100% rename from internal/handlers/assets.go rename to internal/handlers/cli/assets.go diff --git a/internal/handlers/companies.go b/internal/handlers/cli/companies.go similarity index 100% rename from internal/handlers/companies.go rename to internal/handlers/cli/companies.go diff --git a/internal/handlers/contacts.go b/internal/handlers/cli/contacts.go similarity index 100% rename from internal/handlers/contacts.go rename to internal/handlers/cli/contacts.go diff --git a/internal/handlers/contracts.go b/internal/handlers/cli/contracts.go similarity index 100% rename from internal/handlers/contracts.go rename to internal/handlers/cli/contracts.go diff --git a/internal/handlers/dashboard.go b/internal/handlers/cli/dashboard.go similarity index 100% rename from internal/handlers/dashboard.go rename to internal/handlers/cli/dashboard.go diff --git a/internal/handlers/generic.go b/internal/handlers/cli/generic.go similarity index 100% rename from internal/handlers/generic.go rename to internal/handlers/cli/generic.go diff --git a/internal/handlers/invoices.go b/internal/handlers/cli/invoices.go similarity index 100% rename from internal/handlers/invoices.go rename to internal/handlers/cli/invoices.go diff --git a/internal/handlers/jobs.go b/internal/handlers/cli/jobs.go similarity index 100% rename from internal/handlers/jobs.go rename to internal/handlers/cli/jobs.go diff --git a/internal/handlers/locations.go b/internal/handlers/cli/locations.go similarity index 100% rename from internal/handlers/locations.go rename to internal/handlers/cli/locations.go diff --git a/internal/handlers/login.go b/internal/handlers/cli/login.go similarity index 100% rename from internal/handlers/login.go rename to internal/handlers/cli/login.go diff --git a/internal/handlers/notifications.go b/internal/handlers/cli/notifications.go similarity index 100% rename from internal/handlers/notifications.go rename to internal/handlers/cli/notifications.go diff --git a/internal/handlers/quotes.go b/internal/handlers/cli/quotes.go similarity index 100% rename from internal/handlers/quotes.go rename to internal/handlers/cli/quotes.go diff --git a/internal/handlers/services.go b/internal/handlers/cli/services.go similarity index 100% rename from internal/handlers/services.go rename to internal/handlers/cli/services.go diff --git a/internal/handlers/tags.go b/internal/handlers/cli/tags.go similarity index 100% rename from internal/handlers/tags.go rename to internal/handlers/cli/tags.go diff --git a/internal/handlers/users.go b/internal/handlers/cli/users.go similarity index 100% rename from internal/handlers/users.go rename to internal/handlers/cli/users.go diff --git a/internal/handlers/web/admin.go b/internal/handlers/web/admin.go new file mode 100644 index 0000000..2ca106b --- /dev/null +++ b/internal/handlers/web/admin.go @@ -0,0 +1,24 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func AdminHandler(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("HX-Request") == "true" { + // This is an HTMX request, return only the jobs partial + tmpl := template.Must(template.ParseFiles("templates/partials/jobs.html")) + jobs := r.Cookies() // Replace with actual data fetching + tmpl.Execute(w, jobs) + } else { + // This is a full page request + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/admin.html")) + jobs := []string{"Job 1", "Job 2", "Job 3"} // Replace with actual data fetching + + tmpl.Execute(w, map[string]interface{}{ + "Title": "Jobs", + "Jobs": jobs, + }) + } +} diff --git a/internal/handlers/web/assets.go b/internal/handlers/web/assets.go new file mode 100644 index 0000000..1f7d07f --- /dev/null +++ b/internal/handlers/web/assets.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func AssetsHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/assets.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/companies.go b/internal/handlers/web/companies.go new file mode 100644 index 0000000..744f039 --- /dev/null +++ b/internal/handlers/web/companies.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func CompaniesHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/companies.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/contacts.go b/internal/handlers/web/contacts.go new file mode 100644 index 0000000..46addef --- /dev/null +++ b/internal/handlers/web/contacts.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func ContactsHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/contacts.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/contracts.go b/internal/handlers/web/contracts.go new file mode 100644 index 0000000..20c3ccd --- /dev/null +++ b/internal/handlers/web/contracts.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func ContractsHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/contracts.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/dashboard.go b/internal/handlers/web/dashboard.go new file mode 100644 index 0000000..ecd687a --- /dev/null +++ b/internal/handlers/web/dashboard.go @@ -0,0 +1,34 @@ +package handlers + +import ( + "html/template" + "log" + "net/http" +) + +func DashboardHandler(w http.ResponseWriter, r *http.Request) { + tmpl, err := template.ParseFiles( + "templates/layout.html", + "templates/dashboard.html", + "templates/partials/invoice_search.html", + "templates/partials/invoice_search_results.html", + ) + if err != nil { + log.Printf("Template parsing error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + data := struct{}{} // Empty struct as data + + if r.Header.Get("HX-Request") == "true" { + err = tmpl.ExecuteTemplate(w, "content", data) + } else { + err = tmpl.ExecuteTemplate(w, "layout.html", 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/generic.go b/internal/handlers/web/generic.go new file mode 100644 index 0000000..efe5640 --- /dev/null +++ b/internal/handlers/web/generic.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func GenericHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/generic.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/invoices.go b/internal/handlers/web/invoices.go new file mode 100644 index 0000000..3e6e0bd --- /dev/null +++ b/internal/handlers/web/invoices.go @@ -0,0 +1,57 @@ +package handlers + +import ( + "html/template" + "log" + "marmic/servicetrade-toolbox/internal/api" + "net/http" + "strings" +) + +func InvoicesHandler(w http.ResponseWriter, r *http.Request) { + session, ok := r.Context().Value("session").(*api.Session) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Handle the search request + if r.Method == "GET" && r.URL.Query().Get("search") != "" { + handleInvoiceSearch(w, r, session) + return + } + + // Handle the initial page load + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/invoices.html")) + err := tmpl.Execute(w, nil) + if err != nil { + log.Printf("Error executing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +func handleInvoiceSearch(w http.ResponseWriter, r *http.Request, session *api.Session) { + searchTerm := strings.TrimSpace(r.URL.Query().Get("search")) + + if searchTerm == "" { + log.Println("Empty search term, returning empty response") + w.WriteHeader(http.StatusOK) + return + } + + invoice, err := session.GetInvoice(searchTerm) + if err != nil { + log.Printf("Error fetching invoice: %v", err) + w.WriteHeader(http.StatusInternalServerError) + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) + tmpl.ExecuteTemplate(w, "invoice_search_results", map[string]interface{}{"Error": err.Error()}) + return + } + + tmpl := template.Must(template.ParseFiles("templates/partials/invoice_search_results.html")) + err = tmpl.ExecuteTemplate(w, "invoice_search_results", invoice) + if err != nil { + log.Printf("Error executing template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} diff --git a/internal/handlers/web/jobs.go b/internal/handlers/web/jobs.go new file mode 100644 index 0000000..6a08fc0 --- /dev/null +++ b/internal/handlers/web/jobs.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func JobsHandler(w http.ResponseWriter, r *http.Request) { + jobs := []string{"Job 1", "Job 2", "Job 3"} // Replace with actual data fetching + + if r.Header.Get("HX-Request") == "true" { + // This is an HTMX request, return the jobs content wrapped in the content template + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/jobs.html")) + tmpl.ExecuteTemplate(w, "content", map[string]interface{}{ + "Title": "Jobs", + "Jobs": jobs, + }) + } else { + // This is a full page request + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/partials/jobs.html")) + tmpl.Execute(w, map[string]interface{}{ + "Title": "Jobs", + "Jobs": jobs, + }) + } +} diff --git a/internal/handlers/web/locations.go b/internal/handlers/web/locations.go new file mode 100644 index 0000000..000a488 --- /dev/null +++ b/internal/handlers/web/locations.go @@ -0,0 +1,11 @@ +package handlers + +import ( + "html/template" + "net/http" +) + +func LocationsHandler(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFiles("templates/layout.html", "templates/locations.html")) + tmpl.Execute(w, nil) +} diff --git a/internal/handlers/web/login.go b/internal/handlers/web/login.go new file mode 100644 index 0000000..3f31a4e --- /dev/null +++ b/internal/handlers/web/login.go @@ -0,0 +1,104 @@ +package handlers + +import ( + "html/template" + "log" + "marmic/servicetrade-toolbox/internal/api" + "marmic/servicetrade-toolbox/internal/middleware" + "net/http" + "strings" +) + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == "GET" { + tmpl := template.Must(template.ParseFiles("templates/login.html")) + tmpl.Execute(w, nil) + return + } + + if r.Method == "POST" { + email := r.FormValue("email") + password := r.FormValue("password") + + session := api.NewSession() + err := session.Login(email, password) + if err != nil { + if r.Header.Get("HX-Request") == "true" { + w.Write([]byte("