Browse Source

chore: removed cli stuff

document-upload-removal-layout-update
nic 1 year ago
parent
commit
fcbc156512
  1. 95
      apps/cli/main.go
  2. 7
      apps/web/main.go
  3. 11
      go.mod
  4. 8
      go.sum
  5. 5
      internal/api/auth.go
  6. 3
      internal/api/deficiencies.go
  7. 209
      internal/api/jobs.go
  8. 82
      internal/auth/auth.go
  9. 48
      internal/handlers/cli/admin.go
  10. 67
      internal/handlers/cli/assets.go
  11. 67
      internal/handlers/cli/companies.go
  12. 67
      internal/handlers/cli/contacts.go
  13. 67
      internal/handlers/cli/contracts.go
  14. 57
      internal/handlers/cli/generic.go
  15. 70
      internal/handlers/cli/invoices.go
  16. 116
      internal/handlers/cli/jobs.go
  17. 67
      internal/handlers/cli/locations.go
  18. 104
      internal/handlers/cli/login.go
  19. 57
      internal/handlers/cli/notifications.go
  20. 67
      internal/handlers/cli/quotes.go
  21. 67
      internal/handlers/cli/services.go
  22. 67
      internal/handlers/cli/tags.go
  23. 67
      internal/handlers/cli/users.go
  24. 25
      internal/handlers/web/dashboard.go
  25. 42
      internal/handlers/web/jobs.go
  26. 37
      internal/ui/ui.go
  27. 107
      internal/utils/utils.go
  28. 145
      static/css/styles.css
  29. 10
      templates/layout.html
  30. 13
      templates/partials/csv_upload.html
  31. 24
      templates/partials/document_upload_form.html
  32. 27
      templates/partials/upload_actions.html
  33. 35
      web_templates.go

95
apps/cli/main.go

@ -1,95 +0,0 @@
package main
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/handlers/cli"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
"os"
)
func main() {
ui.DisplayStartScreen()
email, password, err := utils.PromptCredentials()
if err != nil {
ui.DisplayError("Error getting credentials:", err)
os.Exit(1)
}
session := api.NewSession()
err = session.Login(email, password)
if err != nil {
ui.DisplayError("Authentication failed:", err)
os.Exit(1)
}
fmt.Println("Login successful!")
for {
ui.ClearScreen()
fmt.Println("Main Menu:")
fmt.Println("1. Jobs")
fmt.Println("2. Invoices")
fmt.Println("3. Companies")
fmt.Println("4. Assets")
fmt.Println("5. Contacts")
fmt.Println("6. Contracts")
fmt.Println("7. Generic Tools")
fmt.Println("8. Locations")
fmt.Println("9. Notifications")
fmt.Println("10. Quotes")
fmt.Println("11. Services")
fmt.Println("12. Tags")
fmt.Println("13. Users")
fmt.Println("14. Admin")
fmt.Println("15. Logout")
choice, err := utils.GetUserChoice(15)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
cli.HandleJobs(session)
case 2:
cli.HandleInvoices(session)
case 3:
cli.HandleCompanies(session)
case 4:
cli.HandleAssets(session)
case 5:
cli.HandleContacts(session)
case 6:
cli.HandleContracts(session)
case 7:
cli.HandleGenericTools(session)
case 8:
cli.HandleLocations(session)
case 9:
cli.HandleNotifications(session)
case 10:
cli.HandleQuotes(session)
case 11:
cli.HandleServices(session)
case 12:
cli.HandleTags(session)
case 13:
cli.HandleUsers(session)
case 14:
cli.HandleAdmin(session)
case 15:
err := session.Logout()
if err != nil {
ui.DisplayError("Error during logout: ", err)
} else {
fmt.Println("Logout successful.")
}
fmt.Println("Exiting ServiceTrade CLI Toolbox. Goodbye!")
return
}
}
}

7
apps/web/main.go

@ -58,6 +58,13 @@ func main() {
protected.HandleFunc("/tags", web.TagsHandler).Methods("GET")
protected.HandleFunc("/users", web.UsersHandler).Methods("GET")
// Document upload routes
protected.HandleFunc("/documents", web.DocumentsHandler).Methods("GET")
protected.HandleFunc("/process-csv", web.ProcessCSVHandler).Methods("POST")
protected.HandleFunc("/upload-documents", web.UploadDocumentsHandler).Methods("POST")
protected.HandleFunc("/document-field-add", web.DocumentFieldAddHandler).Methods("GET")
protected.HandleFunc("/document-field-remove", web.DocumentFieldRemoveHandler).Methods("GET")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}

11
go.mod

@ -2,13 +2,4 @@ module marmic/servicetrade-toolbox
go 1.22.1
require (
github.com/gorilla/mux v1.8.1
github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3
)
require (
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
)
require github.com/gorilla/mux v1.8.1

8
go.sum

@ -1,10 +1,2 @@
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 h1:fO9A67/izFYFYky7l1pDP5Dr0BTCRkaQJUG6Jm5ehsk=
github.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3/go.mod h1:Ey4uAp+LvIl+s5jRbOHLcZpUDnkjLBROl15fZLwPlTM=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=

5
internal/api/auth.go

@ -2,12 +2,11 @@ package api
import (
"io"
"marmic/servicetrade-toolbox/internal/auth"
"net/http"
)
// AuthenticatedRequest wraps an http.Request with the session cookie
func AuthenticatedRequest(session *auth.Session, method, url string, body io.Reader) (*http.Request, error) {
func AuthenticatedRequest(session *Session, method, url string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
@ -17,6 +16,6 @@ func AuthenticatedRequest(session *auth.Session, method, url string, body io.Rea
}
// DoAuthenticatedRequest performs an authenticated request and returns the response
func DoAuthenticatedRequest(session *auth.Session, req *http.Request) (*http.Response, error) {
func DoAuthenticatedRequest(session *Session, req *http.Request) (*http.Response, error) {
return session.Client.Do(req)
}

3
internal/api/deficiencies.go

@ -5,10 +5,9 @@ import (
"encoding/json"
"fmt"
"io"
"marmic/servicetrade-toolbox/internal/auth"
)
func GetDeficiencyById(session *auth.Session, deficiencyId string) (map[string]interface{}, error) {
func GetDeficiencyById(session *Session, deficiencyId string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/deficiency/%s", BaseURL, deficiencyId)
req, err := AuthenticatedRequest(session, "GET", url, nil)
if err != nil {

209
internal/api/jobs.go

@ -5,153 +5,120 @@ import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"time"
)
type JobsResponse struct {
Data struct {
Jobs []Job `json:"jobs"`
} `json:"data"`
// SearchJobsParams defines parameters for searching jobs
type SearchJobsParams struct {
Query string
Status string
StartDate time.Time
EndDate time.Time
Page int
Limit int
IncludeArchived bool
}
type Job struct {
ID int64 `json:"id"`
Name string `json:"name"`
CustomName *string `json:"customName"`
Type string `json:"type"`
JobTypeWeight int `json:"jobTypeWeight"`
Status string `json:"status"`
Visibility []string `json:"visibility"`
Number int `json:"number"`
RefNumber string `json:"refNumber"`
Description *string `json:"description"`
ScheduledDate *int64 `json:"scheduledDate"`
CompletedOn *int64 `json:"completedOn"`
ServiceLine string `json:"serviceLine"`
EstimatedPrice *float64 `json:"estimatedPrice"`
Vendor Vendor `json:"vendor"`
Customer Customer `json:"customer"`
Location Location `json:"location"`
Owner Owner `json:"owner"`
Tags []Tag `json:"tags"`
Appointments []Appointment `json:"appointments"`
CurrentAppointment Appointment `json:"currentAppointment"`
AssignedOffice Location `json:"assignedOffice"`
Offices []Location `json:"offices"`
Terms *Term `json:"terms"`
Contract *Contract `json:"contract"`
PrimaryContact *PrimaryContact `json:"primaryContact"`
}
// SearchJobs searches for jobs based on the provided parameters
func (s *Session) SearchJobs(params SearchJobsParams) ([]map[string]interface{}, error) {
queryValues := url.Values{}
type Vendor struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
Status string `json:"status"`
}
if params.Query != "" {
queryValues.Add("q", params.Query)
}
type Customer struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
Status string `json:"status"`
}
if params.Status != "" {
queryValues.Add("status", params.Status)
}
type Location struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
RefNumber string `json:"refNumber"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Address Address `json:"address"`
}
if !params.StartDate.IsZero() {
queryValues.Add("start", params.StartDate.Format("2006-01-02"))
}
type Address struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
PostalCode string `json:"postalCode"`
}
if !params.EndDate.IsZero() {
queryValues.Add("end", params.EndDate.Format("2006-01-02"))
}
func (a Address) String() string {
return fmt.Sprintf("%s, %s, %s %s", a.Street, a.City, a.State, a.PostalCode)
}
if params.Page > 0 {
queryValues.Add("page", strconv.Itoa(params.Page))
} else {
queryValues.Add("page", "1")
}
type Owner struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
Status string `json:"status"`
Email string `json:"email"`
}
if params.Limit > 0 {
queryValues.Add("limit", strconv.Itoa(params.Limit))
} else {
queryValues.Add("limit", "50")
}
type Tag struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
}
if params.IncludeArchived {
queryValues.Add("include", "archived")
}
type Appointment struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Name string `json:"name"`
Status string `json:"status"`
WindowStart *int64 `json:"windowStart"`
WindowEnd *int64 `json:"windowEnd"`
Techs []Tech `json:"techs"`
Released bool `json:"released"`
}
endpoint := fmt.Sprintf("/job?%s", queryValues.Encode())
resp, err := s.DoRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
type Tech struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
type Term struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
}
type Contract struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
var result struct {
Data struct {
Jobs []map[string]interface{} `json:"records"`
} `json:"data"`
}
type PrimaryContact struct {
ID int64 `json:"id"`
URI string `json:"uri"`
Email string `json:"email"`
}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error unmarshalling response: %v", err)
}
func (s *Session) SearchJobs(filters url.Values) ([]Job, error) {
endpoint := "/job?"
query := filters.Encode()
url := endpoint + query
log.Printf("Found %d jobs", len(result.Data.Jobs))
log.Printf("Parsed Data: %+v", result.Data)
return result.Data.Jobs, nil
}
resp, err := s.DoRequest("GET", url, nil)
// GetJobDetails retrieves detailed information about a specific job
func (s *Session) GetJobDetails(jobID string) (map[string]interface{}, error) {
endpoint := fmt.Sprintf("/job/%s", jobID)
resp, err := s.DoRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Printf("Raw API Response: %s", string(body))
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to search jobs: %s, response: %s", resp.Status, string(body))
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
}
var result struct {
Data map[string]interface{} `json:"data"`
}
var result JobsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error unmarshalling response: %v", err)
}
log.Printf("Parsed Data: %+v", result.Data)
return result.Data.Jobs, nil
return result.Data, nil
}
// GetAttachmentsForJob retrieves attachments for a specific job
func (s *Session) GetAttachmentsForJob(jobID string) (map[string]interface{}, error) {
resp, err := s.DoRequest("GET", fmt.Sprintf("/job/%s/paperwork", jobID), nil)
if err != nil {
@ -171,21 +138,7 @@ func (s *Session) GetAttachmentsForJob(jobID string) (map[string]interface{}, er
return result, nil
}
func (s *Session) DeleteAttachment(attachmentID string) error {
resp, err := s.DoRequest("DELETE", fmt.Sprintf("/attachment/%s", attachmentID), nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 204 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to delete attachment: %s, response: %s", resp.Status, string(body))
}
return nil
}
// GetDeficiencyInfoForJob retrieves deficiency information for a specific job
func (s *Session) GetDeficiencyInfoForJob(jobID string) ([]map[string]interface{}, error) {
resp, err := s.DoRequest("GET", fmt.Sprintf("/deficiency/%s", jobID), nil)
if err != nil {

82
internal/auth/auth.go

@ -1,82 +0,0 @@
package auth
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type Session struct {
Client *http.Client
Cookie string
}
func NewSession() *Session {
return &Session{Client: &http.Client{}}
}
func (s *Session) Login(email, password string) error {
url := "https://api.servicetrade.com/api/auth"
payload := map[string]string{
"username": email,
"password": password,
}
payloadBytes, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
if err != nil {
return fmt.Errorf("error creating request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.Client.Do(req)
if err != nil {
return fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return fmt.Errorf("failed to authenticate: %s, response: %s", resp.Status, string(body))
}
for _, cookie := range resp.Cookies() {
if strings.Contains(cookie.Name, "PHPSESSID") {
s.Cookie = cookie.String()
return nil
}
}
return fmt.Errorf("failed to retrieve session cookie; authentication may have failed")
}
func (s *Session) Logout() error {
if s.Cookie == "" {
return fmt.Errorf("no active session to end")
}
url := "https://api.servicetrade.com/api/auth"
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return fmt.Errorf("failed to create DELETE request to end session: %v", err)
}
req.Header.Set("Cookie", s.Cookie)
resp, err := s.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to send DELETE request to end session: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 && resp.StatusCode != 204 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to end session: %s, response: %s", resp.Status, string(body))
}
s.Cookie = ""
return nil
}

48
internal/handlers/cli/admin.go

@ -1,48 +0,0 @@
package cli
import (
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleAdmin(session *api.Session) {
for {
ui.ClearScreen()
ui.DisplayMenu([]string{
"User Management",
"System Settings",
"Back to Main Menu",
}, "Admin Menu")
choice, err := utils.GetUserChoice(3)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
userManagement(session)
case 2:
systemSettings(session)
case 3:
return
}
}
}
func userManagement(session *api.Session) {
ui.ClearScreen()
ui.DisplayMessage("User Management")
// TODO: Implement user management logic
utils.PressEnterToContinue()
}
func systemSettings(session *api.Session) {
ui.ClearScreen()
ui.DisplayMessage("System Settings")
// TODO: Implement system settings logic
utils.PressEnterToContinue()
}

67
internal/handlers/cli/assets.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleAssets(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Assets Menu:")
fmt.Println("1. List Assets")
fmt.Println("2. Add Asset")
fmt.Println("3. Edit Asset")
fmt.Println("4. Delete Asset")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listAssets(session)
case 2:
addAsset(session)
case 3:
editAsset(session)
case 4:
deleteAsset(session)
case 5:
return
}
}
}
func listAssets(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing assets...")
// TODO: Implement asset listing logic using the API
utils.PressEnterToContinue()
}
func addAsset(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new asset...")
// TODO: Implement asset creation logic using the API
utils.PressEnterToContinue()
}
func editAsset(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing an asset...")
// TODO: Implement asset editing logic using the API
utils.PressEnterToContinue()
}
func deleteAsset(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting an asset...")
// TODO: Implement asset deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/companies.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleCompanies(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Companies Menu:")
fmt.Println("1. List Companies")
fmt.Println("2. Add Company")
fmt.Println("3. Edit Company")
fmt.Println("4. Delete Company")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listCompanies(session)
case 2:
addCompany(session)
case 3:
editCompany(session)
case 4:
deleteCompany(session)
case 5:
return
}
}
}
func listCompanies(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing companies...")
// TODO: Implement company listing logic using the API
utils.PressEnterToContinue()
}
func addCompany(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new company...")
// TODO: Implement company creation logic using the API
utils.PressEnterToContinue()
}
func editCompany(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a company...")
// TODO: Implement company editing logic using the API
utils.PressEnterToContinue()
}
func deleteCompany(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a company...")
// TODO: Implement company deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/contacts.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleContacts(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Contacts Menu:")
fmt.Println("1. List Contacts")
fmt.Println("2. Add Contact")
fmt.Println("3. Edit Contact")
fmt.Println("4. Delete Contact")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listContacts(session)
case 2:
addContact(session)
case 3:
editContact(session)
case 4:
deleteContact(session)
case 5:
return
}
}
}
func listContacts(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing contacts...")
// TODO: Implement contact listing logic using the API
utils.PressEnterToContinue()
}
func addContact(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new contact...")
// TODO: Implement contact creation logic using the API
utils.PressEnterToContinue()
}
func editContact(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a contact...")
// TODO: Implement contact editing logic using the API
utils.PressEnterToContinue()
}
func deleteContact(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a contact...")
// TODO: Implement contact deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/contracts.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleContracts(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Contracts Menu:")
fmt.Println("1. List Contracts")
fmt.Println("2. Add Contract")
fmt.Println("3. Edit Contract")
fmt.Println("4. Delete Contract")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listContracts(session)
case 2:
addContract(session)
case 3:
editContract(session)
case 4:
deleteContract(session)
case 5:
return
}
}
}
func listContracts(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing contracts...")
// TODO: Implement contract listing logic using the API
utils.PressEnterToContinue()
}
func addContract(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new contract...")
// TODO: Implement contract creation logic using the API
utils.PressEnterToContinue()
}
func editContract(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a contract...")
// TODO: Implement contract editing logic using the API
utils.PressEnterToContinue()
}
func deleteContract(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a contract...")
// TODO: Implement contract deletion logic using the API
utils.PressEnterToContinue()
}

57
internal/handlers/cli/generic.go

@ -1,57 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleGenericTools(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Generic Tools Menu:")
fmt.Println("1. Tool 1")
fmt.Println("2. Tool 2")
fmt.Println("3. Tool 3")
fmt.Println("4. Back to Main Menu")
choice, err := utils.GetUserChoice(4)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
runTool1(session)
case 2:
runTool2(session)
case 3:
runTool3(session)
case 4:
return
}
}
}
func runTool1(session *api.Session) {
ui.ClearScreen()
fmt.Println("Running Tool 1...")
// TODO: Implement Tool 1 logic
utils.PressEnterToContinue()
}
func runTool2(session *api.Session) {
ui.ClearScreen()
fmt.Println("Running Tool 2...")
// TODO: Implement Tool 2 logic
utils.PressEnterToContinue()
}
func runTool3(session *api.Session) {
ui.ClearScreen()
fmt.Println("Running Tool 3...")
// TODO: Implement Tool 3 logic
utils.PressEnterToContinue()
}

70
internal/handlers/cli/invoices.go

@ -1,70 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleInvoices(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Invoices Menu:")
fmt.Println("1. Search Invoice")
fmt.Println("2. List Recent Invoices")
fmt.Println("3. Create Invoice")
fmt.Println("4. Back to Main Menu")
choice, err := utils.GetUserChoice(4)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
searchInvoice(session)
case 2:
listRecentInvoices(session)
case 3:
createInvoice(session)
case 4:
return
}
}
}
func searchInvoice(session *api.Session) {
ui.ClearScreen()
fmt.Println("Search Invoice:")
identifier := utils.PromptForInput("Enter Invoice Number or ID: ")
invoice, err := session.GetInvoice(identifier)
if err != nil {
fmt.Printf("Error fetching invoice: %v\n", err)
utils.PressEnterToContinue()
return
}
fmt.Println("Invoice Details:")
fmt.Printf("Invoice Number: %v\n", invoice["invoiceNumber"])
fmt.Printf("Total Price: $%v\n", invoice["totalPrice"])
fmt.Printf("Status: %v\n", invoice["status"])
utils.PressEnterToContinue()
}
func listRecentInvoices(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing recent invoices...")
// TODO: Implement recent invoices listing logic using the API
utils.PressEnterToContinue()
}
func createInvoice(session *api.Session) {
ui.ClearScreen()
fmt.Println("Creating a new invoice...")
// TODO: Implement invoice creation logic using the API
utils.PressEnterToContinue()
}

116
internal/handlers/cli/jobs.go

@ -1,116 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleJobs(session *api.Session) {
for {
ui.DisplayMenu([]string{
"Search Job by ID",
"List Recent Jobs",
"Create New Job",
"Manage Job Attachments",
"View Deficiencies",
"Back to Main Menu",
}, "Jobs Menu")
choice, err := utils.GetUserChoice(6)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
searchJobByID(session)
case 2:
listRecentJobs(session)
case 3:
createNewJob(session)
case 4:
manageJobAttachments(session)
case 5:
viewDeficiencyById(session)
case 6:
return
}
}
}
func searchJobByID(session *api.Session) {
ui.ClearScreen()
ui.DisplayMessage("Search Job by ID:")
jobID := utils.PromptForInput("Enter Job ID: ")
// TODO: Implement job search logic using the API
ui.DisplayMessage(fmt.Sprintf("Searching for job with ID: %s", jobID))
utils.PressEnterToContinue()
}
func listRecentJobs(session *api.Session) {
ui.ClearScreen()
ui.DisplayMessage("Listing recent jobs...")
// TODO: Implement recent jobs listing logic using the API
utils.PressEnterToContinue()
}
func createNewJob(session *api.Session) {
ui.ClearScreen()
ui.DisplayMessage("Creating a new job...")
// TODO: Implement job creation logic using the API
utils.PressEnterToContinue()
}
func manageJobAttachments(session *api.Session) {
ui.ClearScreen()
jobID := utils.PromptForInput("Enter Job ID: ")
attachments, err := session.GetAttachmentsForJob(jobID)
if err != nil {
ui.DisplayError("Failed to retrieve attachments:", err)
utils.PressEnterToContinue()
return
}
ui.DisplayMessage(fmt.Sprintf("Attachments for Job %s:", jobID))
if dataMap, ok := attachments["data"].(map[string]interface{}); ok {
if attachmentsList, ok := dataMap["attachments"].([]interface{}); ok {
for i, attachment := range attachmentsList {
if att, ok := attachment.(map[string]interface{}); ok {
ui.DisplayMessage(fmt.Sprintf("%d. %s", i+1, att["fileName"]))
}
}
}
}
// TODO: Implement attachment deletion logic
utils.PressEnterToContinue()
}
func viewDeficiencyById(session *api.Session) {
ui.ClearScreen()
deficiencyID := utils.PromptForInput("Enter Deficiency ID: ")
ui.DisplayMessage(fmt.Sprintf("Fetching information for Deficiency %s...", deficiencyID))
deficiencies, err := session.GetDeficiencyInfoForJob(deficiencyID)
if err != nil {
ui.DisplayError("Failed to retrieve deficiency information:", err)
utils.PressEnterToContinue()
return
}
for _, deficiency := range deficiencies {
ui.DisplayMessage(fmt.Sprintf("ID: %v", deficiency["id"]))
ui.DisplayMessage(fmt.Sprintf("Description: %v", deficiency["description"]))
ui.DisplayMessage(fmt.Sprintf("Status: %v", deficiency["status"]))
ui.DisplayMessage("---")
}
utils.PressEnterToContinue()
}

67
internal/handlers/cli/locations.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleLocations(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Locations Menu:")
fmt.Println("1. List Locations")
fmt.Println("2. Add Location")
fmt.Println("3. Edit Location")
fmt.Println("4. Delete Location")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listLocations(session)
case 2:
addLocation(session)
case 3:
editLocation(session)
case 4:
deleteLocation(session)
case 5:
return
}
}
}
func listLocations(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing locations...")
// TODO: Implement location listing logic using the API
utils.PressEnterToContinue()
}
func addLocation(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new location...")
// TODO: Implement location creation logic using the API
utils.PressEnterToContinue()
}
func editLocation(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a location...")
// TODO: Implement location editing logic using the API
utils.PressEnterToContinue()
}
func deleteLocation(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a location...")
// TODO: Implement location deletion logic using the API
utils.PressEnterToContinue()
}

104
internal/handlers/cli/login.go

@ -1,104 +0,0 @@
package cli
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("<div class='error'>Login failed: " + err.Error() + "</div>"))
} else {
http.Error(w, "Login failed", http.StatusUnauthorized)
}
return
}
cookieParts := strings.Split(session.Cookie, ";")
sessionID := strings.TrimPrefix(cookieParts[0], "PHPSESSID=")
middleware.SessionStore.Set(sessionID, session)
http.SetCookie(w, &http.Cookie{
Name: "PHPSESSID",
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
})
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Redirect", "/")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Login successful"))
} else {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
}
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("PHPSESSID")
if err != nil {
log.Printf("No session cookie found: %v", err)
redirectToLogin(w, r)
return
}
sessionID := cookie.Value
session, exists := middleware.SessionStore.Get(sessionID)
if !exists {
log.Println("No session found in store")
redirectToLogin(w, r)
return
}
err = session.Logout()
if err != nil {
log.Printf("Logout failed: %v", err)
http.Error(w, "Logout failed", http.StatusInternalServerError)
return
}
middleware.SessionStore.Delete(sessionID)
http.SetCookie(w, &http.Cookie{
Name: "PHPSESSID",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: r.TLS != nil,
SameSite: http.SameSiteLaxMode,
})
log.Println("Logout successful, redirecting to login page")
redirectToLogin(w, r)
}
func redirectToLogin(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("HX-Request") != "" {
w.Header().Set("HX-Redirect", "/login")
w.WriteHeader(http.StatusOK)
} else {
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
}

57
internal/handlers/cli/notifications.go

@ -1,57 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleNotifications(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Notifications Menu:")
fmt.Println("1. View Notifications")
fmt.Println("2. Mark Notification as Read")
fmt.Println("3. Delete Notification")
fmt.Println("4. Back to Main Menu")
choice, err := utils.GetUserChoice(4)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
viewNotifications(session)
case 2:
markNotificationAsRead(session)
case 3:
deleteNotification(session)
case 4:
return
}
}
}
func viewNotifications(session *api.Session) {
ui.ClearScreen()
fmt.Println("Viewing notifications...")
// TODO: Implement notification viewing logic using the API
utils.PressEnterToContinue()
}
func markNotificationAsRead(session *api.Session) {
ui.ClearScreen()
fmt.Println("Marking notification as read...")
// TODO: Implement marking notification as read logic using the API
utils.PressEnterToContinue()
}
func deleteNotification(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting notification...")
// TODO: Implement notification deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/quotes.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleQuotes(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Quotes Menu:")
fmt.Println("1. List Quotes")
fmt.Println("2. Create Quote")
fmt.Println("3. Edit Quote")
fmt.Println("4. Delete Quote")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listQuotes(session)
case 2:
createQuote(session)
case 3:
editQuote(session)
case 4:
deleteQuote(session)
case 5:
return
}
}
}
func listQuotes(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing quotes...")
// TODO: Implement quote listing logic using the API
utils.PressEnterToContinue()
}
func createQuote(session *api.Session) {
ui.ClearScreen()
fmt.Println("Creating a new quote...")
// TODO: Implement quote creation logic using the API
utils.PressEnterToContinue()
}
func editQuote(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a quote...")
// TODO: Implement quote editing logic using the API
utils.PressEnterToContinue()
}
func deleteQuote(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a quote...")
// TODO: Implement quote deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/services.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleServices(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Services Menu:")
fmt.Println("1. List Services")
fmt.Println("2. Add Service")
fmt.Println("3. Edit Service")
fmt.Println("4. Delete Service")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listServices(session)
case 2:
addService(session)
case 3:
editService(session)
case 4:
deleteService(session)
case 5:
return
}
}
}
func listServices(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing services...")
// TODO: Implement service listing logic using the API
utils.PressEnterToContinue()
}
func addService(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new service...")
// TODO: Implement service creation logic using the API
utils.PressEnterToContinue()
}
func editService(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a service...")
// TODO: Implement service editing logic using the API
utils.PressEnterToContinue()
}
func deleteService(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a service...")
// TODO: Implement service deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/tags.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleTags(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Tags Menu:")
fmt.Println("1. List Tags")
fmt.Println("2. Add Tag")
fmt.Println("3. Edit Tag")
fmt.Println("4. Delete Tag")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listTags(session)
case 2:
addTag(session)
case 3:
editTag(session)
case 4:
deleteTag(session)
case 5:
return
}
}
}
func listTags(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing tags...")
// TODO: Implement tag listing logic using the API
utils.PressEnterToContinue()
}
func addTag(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new tag...")
// TODO: Implement tag creation logic using the API
utils.PressEnterToContinue()
}
func editTag(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a tag...")
// TODO: Implement tag editing logic using the API
utils.PressEnterToContinue()
}
func deleteTag(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a tag...")
// TODO: Implement tag deletion logic using the API
utils.PressEnterToContinue()
}

67
internal/handlers/cli/users.go

@ -1,67 +0,0 @@
package cli
import (
"fmt"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/ui"
"marmic/servicetrade-toolbox/internal/utils"
)
func HandleUsers(session *api.Session) {
for {
ui.ClearScreen()
fmt.Println("Users Menu:")
fmt.Println("1. List Users")
fmt.Println("2. Add User")
fmt.Println("3. Edit User")
fmt.Println("4. Delete User")
fmt.Println("5. Back to Main Menu")
choice, err := utils.GetUserChoice(5)
if err != nil {
ui.DisplayError("Invalid input:", err)
utils.PressEnterToContinue()
continue
}
switch choice {
case 1:
listUsers(session)
case 2:
addUser(session)
case 3:
editUser(session)
case 4:
deleteUser(session)
case 5:
return
}
}
}
func listUsers(session *api.Session) {
ui.ClearScreen()
fmt.Println("Listing users...")
// TODO: Implement user listing logic using the API
utils.PressEnterToContinue()
}
func addUser(session *api.Session) {
ui.ClearScreen()
fmt.Println("Adding a new user...")
// TODO: Implement user creation logic using the API
utils.PressEnterToContinue()
}
func editUser(session *api.Session) {
ui.ClearScreen()
fmt.Println("Editing a user...")
// TODO: Implement user editing logic using the API
utils.PressEnterToContinue()
}
func deleteUser(session *api.Session) {
ui.ClearScreen()
fmt.Println("Deleting a user...")
// TODO: Implement user deletion logic using the API
utils.PressEnterToContinue()
}

25
internal/handlers/web/dashboard.go

@ -7,18 +7,25 @@ import (
)
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
var err error
tmpl := root.WebTemplates
data := struct{}{} // Empty struct as data
data := map[string]interface{}{
"Title": "Dashboard",
}
if r.Header.Get("HX-Request") == "true" {
err = tmpl.ExecuteTemplate(w, "content", data)
// 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 {
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
// 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
}
}
}

42
internal/handlers/web/jobs.go

@ -7,6 +7,7 @@ import (
"marmic/servicetrade-toolbox/internal/api"
"net/http"
"net/url"
"strconv"
"time"
)
@ -60,6 +61,7 @@ func JobsHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Found", http.StatusNotFound)
}
}
func convertDatesToUnix(params url.Values) url.Values {
cleanedParams := url.Values{}
@ -93,11 +95,47 @@ func convertDatesToUnix(params url.Values) url.Values {
return cleanedParams
}
func handleJobSearch(r *http.Request, session *api.Session) ([]api.Job, error) {
func handleJobSearch(r *http.Request, session *api.Session) ([]map[string]interface{}, error) {
queryParams := r.URL.Query()
cleanedParams := convertDatesToUnix(queryParams)
jobs, err := session.SearchJobs(cleanedParams)
// Convert url.Values to SearchJobsParams
params := api.SearchJobsParams{
Query: cleanedParams.Get("q"),
Status: cleanedParams.Get("status"),
}
// Convert page and limit if present
if pageStr := cleanedParams.Get("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil {
params.Page = page
}
}
if limitStr := cleanedParams.Get("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
params.Limit = limit
}
}
// Handle start/end dates
if startStr := cleanedParams.Get("start"); startStr != "" {
if start, err := time.Parse("2006-01-02", startStr); err == nil {
params.StartDate = start
}
}
if endStr := cleanedParams.Get("end"); endStr != "" {
if end, err := time.Parse("2006-01-02", endStr); err == nil {
params.EndDate = end
}
}
// Handle include archived
params.IncludeArchived = cleanedParams.Get("include") == "archived"
jobs, err := session.SearchJobs(params)
if err != nil {
return nil, fmt.Errorf("error fetching jobs: %w", err)
}

37
internal/ui/ui.go

@ -1,37 +0,0 @@
package ui
import (
"fmt"
"github.com/inancgumus/screen"
)
func ClearScreen() {
screen.Clear()
screen.MoveTopLeft()
}
func DisplayStartScreen() {
ClearScreen()
fmt.Println("========================================")
fmt.Println(" Welcome to ServiceTrade CLI")
fmt.Println("========================================")
fmt.Println("Please log in with your ServiceTrade credentials to continue.")
fmt.Println()
}
func DisplayMessage(message string) {
fmt.Println(message)
}
func DisplayError(prefix string, err error) {
fmt.Printf("%s %v\n", prefix, err)
}
func DisplayMenu(items []string, title string) {
ClearScreen()
fmt.Printf("\n%s:\n", title)
for i, item := range items {
fmt.Printf("%d. %s\n", i+1, item)
}
}

107
internal/utils/utils.go

@ -1,53 +1,11 @@
package utils
import (
"bufio"
"encoding/csv"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
func PromptCredentials() (string, string, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your email: ")
email, _ := reader.ReadString('\n')
email = strings.TrimSpace(email)
fmt.Print("Enter your password: ")
password, _ := reader.ReadString('\n')
password = strings.TrimSpace(password)
return email, password, nil
}
func GetUserChoice(max int) (int, error) {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("\nEnter your choice (1-%d): ", max)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
choice, err := strconv.Atoi(input)
if err != nil || choice < 1 || choice > max {
return 0, fmt.Errorf("invalid input")
}
return choice, nil
}
func PromptForInput(prompt string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Print(prompt)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
func PressEnterToContinue() {
fmt.Println("Press Enter to continue...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
}
// ReadCSV reads a CSV file and returns its contents as a slice of string slices
func ReadCSV(filename string) ([][]string, error) {
file, err := os.Open(filename)
@ -74,51 +32,36 @@ func WriteCSV(filename string, data [][]string) error {
return writer.WriteAll(data)
}
// ChooseFile allows the user to select a file from the current directory
func ChooseFile(extension string) (string, error) {
currentDir, err := os.Getwd()
if err != nil {
return "", err
}
files, err := filepath.Glob(filepath.Join(currentDir, "*"+extension))
if err != nil {
return "", err
}
if len(files) == 0 {
return "", fmt.Errorf("no %s files found in the current directory", extension)
}
fmt.Printf("Available %s files:\n", extension)
for i, file := range files {
fmt.Printf("%d. %s\n", i+1, filepath.Base(file))
}
// ReadCSVFromBytes reads CSV data from a byte slice
func ReadCSVFromBytes(data []byte) ([][]string, error) {
reader := csv.NewReader(strings.NewReader(string(data)))
return reader.ReadAll()
}
var choice int
fmt.Print("Enter the number of the file you want to select: ")
_, err = fmt.Scanf("%d", &choice)
if err != nil || choice < 1 || choice > len(files) {
return "", fmt.Errorf("invalid selection")
// CSVToMap converts CSV data to a map of maps for easier access
// The first row is assumed to be headers
func CSVToMap(data [][]string) ([]map[string]string, error) {
if len(data) < 2 {
return nil, nil // No data or just headers
}
return files[choice-1], nil
}
// PromptYesNo prompts the user for a yes/no response
func PromptYesNo(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s (y/n): ", prompt)
response, _ := reader.ReadString('\n')
response = strings.ToLower(strings.TrimSpace(response))
headers := data[0]
result := make([]map[string]string, 0, len(data)-1)
if response == "y" || response == "yes" {
return true
} else if response == "n" || response == "no" {
return false
for i := 1; i < len(data); i++ {
row := data[i]
if len(row) == 0 {
continue // Skip empty rows
}
fmt.Println("Please answer with 'y' or 'n'.")
rowMap := make(map[string]string)
for j, value := range row {
if j < len(headers) {
rowMap[headers[j]] = value
}
}
result = append(result, rowMap)
}
return result, nil
}

145
static/css/styles.css

@ -472,21 +472,115 @@ html {
/* Progress bar styling */
.progress {
background-color: var(--progress-bg);
height: 1rem;
border-radius: 0.25rem;
height: 20px;
margin-bottom: 10px;
overflow: hidden;
margin: 0.5rem 0;
background-color: var(--progress-bg, #e9ecef);
border-radius: 4px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.progress-bar {
float: left;
width: 0;
height: 100%;
background-color: var(--progress-fill);
transition: width 0.3s ease;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background-color: var(--progress-bar-bg, #0d6efd);
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
transition: width 0.6s ease;
}
#upload-progress {
margin-top: 1rem;
/* Upload results styling */
.upload-results {
margin-top: 20px;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 15px;
background-color: var(--card-bg);
}
.upload-results h4 {
margin-top: 0;
margin-bottom: 15px;
color: var(--heading-color);
}
.job-result {
margin-bottom: 15px;
}
.job-result h5 {
margin-top: 0;
margin-bottom: 10px;
color: var(--subheading-color);
}
.job-result ul {
list-style-type: none;
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
}
.job-result li {
padding: 5px 0;
}
.job-result li.success {
color: var(--success-color, #198754);
}
.job-result li.error {
color: var(--error-color, #dc3545);
}
/* Loading indicator */
.loading-indicator {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(0, 0, 0, 0.3);
border-radius: 50%;
border-top-color: var(--progress-bar-bg, #0d6efd);
animation: spin 1s ease-in-out infinite;
vertical-align: middle;
margin-left: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Job list styling */
.job-list {
list-style-type: none;
padding-left: 0;
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px;
background-color: var(--input-bg);
}
.job-list li {
padding: 5px 10px;
cursor: pointer;
transition: background-color 0.2s;
}
.job-list li:hover {
background-color: var(--hover-bg);
}
.job-list li.selected {
background-color: var(--selected-bg);
color: var(--selected-text);
}
/* Style for error and not-found message display */
@ -535,41 +629,6 @@ html {
content: "🔍";
}
/* Loading indicator */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: flex;
align-items: center;
}
.htmx-request.htmx-indicator {
display: flex;
align-items: center;
}
.loading-indicator {
width: 1.5rem;
height: 1.5rem;
border: 0.25rem solid var(--progress-bg);
border-top: 0.25rem solid var(--progress-fill);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 0.5rem;
}
/* Fade effect for existing content */
.htmx-request .fade-me-out {
opacity: 0.5;

10
templates/layout.html

@ -42,8 +42,16 @@
</header>
<!-- Dynamic content area -->
<div id="content" class="content">{{template "content" .}}</div>
<div id="content" class="content">
{{if .BodyContent}}
{{.BodyContent}}
{{else}}
{{template "content" .}}
{{end}}
</div>
</main>
<script src="/static/js/htmx.min.js"></script>
</body>
</html>

13
templates/partials/csv_upload.html

@ -1,11 +1,14 @@
{{define "csv_upload"}}
<div class="content">
<h3 class="submenu-header">Step 1: Upload Jobs CSV</h3>
<h3 class="submenu-header">Step 1: Upload CSV file with Job Numbers</h3>
<div>
<label>Select CSV file with job numbers:</label>
<input class="card-input" type="file" id="csv-file" name="csv-file" accept=".csv" required>
<input class="card-input" type="file" id="csv-file" name="csvFile" accept=".csv" required>
<button type="button" id="upload-csv" class="btn-primary">Upload CSV</button>
<button class="btn-primary" hx-post="/process-csv" hx-target="#csv-preview-content"
hx-encoding="multipart/form-data" hx-include="#csv-file" hx-indicator="#csv-loading-indicator">
Upload CSV
</button>
<div id="csv-loading-indicator" class="htmx-indicator" style="display: none;">
<span>Processing CSV...</span>
@ -15,8 +18,8 @@
<div id="csv-preview" class="fade-me-out" style="display: none; margin-top: 1rem;">
<h4>Detected Jobs</h4>
<div class="job-list">
<!-- This will be populated with JavaScript after CSV upload -->
<div id="csv-preview-content" class="job-list">
<!-- Job numbers will be displayed here -->
<p>No jobs loaded yet</p>
</div>
</div>

24
templates/partials/document_upload_form.html

@ -2,7 +2,7 @@
<div class="content">
<h3 class="submenu-header">Step 2: Upload Documents</h3>
<div id="document-upload-container">
<div class="document-row">
<div class="document-row" id="document-row-1">
<div class="document-field">
<label>Select Document:</label>
<input class="card-input" type="file" id="document-file-1" name="document-file-1">
@ -19,18 +19,26 @@
<label>Document Type:</label>
<select class="card-input" id="document-type-1" name="document-type-1">
<option value="">Select Document Type</option>
<option value="invoice">Invoice</option>
<option value="receipt">Receipt</option>
<option value="contract">Contract</option>
<option value="report">Report</option>
<option value="other">Other</option>
<option value="1">Job Paperwork</option>
<option value="2">Job Vendor Bill</option>
<option value="3">Job Quality Control Picture</option>
<option value="5">Deficiency Repair Proposal</option>
<option value="7">Generic Attachment</option>
<option value="8">Avatar Image</option>
<option value="9">Import</option>
<option value="10">Blank Paperwork</option>
<option value="11">Work Acknowledgement</option>
<option value="12">Logo</option>
<option value="14">Job Invoice</option>
</select>
</div>
</div>
<button type="button" class="remove-document warning-button" style="display: none;">Remove</button>
<button type="button" class="remove-document warning-button" hx-get="/document-field-remove?id=1"
hx-target="#document-row-1" hx-swap="outerHTML" style="display: none;">Remove</button>
</div>
</div>
<button type="button" id="add-document" class="caution-button">Add Another Document</button>
<button type="button" id="add-document" class="caution-button" hx-get="/document-field-add"
hx-target="#document-upload-container" hx-swap="beforeend">Add Another Document</button>
</div>
{{end}}

27
templates/partials/upload_actions.html

@ -2,19 +2,26 @@
<div class="content">
<h3 class="submenu-header">Step 3: Submit Uploads</h3>
<div>
<button type="button" id="upload-all" class="success-button">Upload Documents to Jobs</button>
<form id="upload-form" hx-post="/upload-documents" hx-encoding="multipart/form-data"
hx-include="#csv-preview-content,[name^='document-file'],[name^='document-name'],[name^='document-type']"
hx-target="#upload-results" hx-indicator="#upload-loading-indicator">
<div id="upload-progress" style="display: none; margin-top: 1rem;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;"></div>
</div>
<p id="upload-status">Preparing uploads...</p>
<button type="submit" class="success-button">Upload Documents to Jobs</button>
<div id="upload-progress" style="display: none; margin-top: 1rem;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;"></div>
</div>
<p id="upload-status">Preparing uploads...</p>
<div id="upload-loading-indicator" class="htmx-indicator">
<span>Uploading...</span>
<div class="loading-indicator"></div>
</div>
<div id="upload-loading-indicator" class="htmx-indicator">
<span>Uploading...</span>
<div class="loading-indicator"></div>
<div id="upload-results"></div>
</div>
</div>
</form>
</div>
</div>
{{end}}

35
web_templates.go

@ -13,6 +13,14 @@ var webAssetsFS embed.FS
var WebTemplates *template.Template
// Custom helper functions for templates
var funcMap = template.FuncMap{
"pageContent": func(name string) string {
// This allows us to reference specific content blocks
return "{{template \"" + name + "-content\" .}}"
},
}
// InitializeWebTemplates parses all HTML templates in the embedded filesystem
func InitializeWebTemplates() error {
var err error
@ -21,8 +29,10 @@ func InitializeWebTemplates() error {
}
func parseWebTemplates() (*template.Template, error) {
tmpl := template.New("")
tmpl := template.New("").Funcs(funcMap)
// First collect all template paths
var templatePaths []string
err := fs.WalkDir(webAssetsFS, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
@ -36,19 +46,30 @@ func parseWebTemplates() (*template.Template, error) {
return nil
}
templatePaths = append(templatePaths, path)
return nil
})
if err != nil {
return nil, err
}
// Process each template
for _, path := range templatePaths {
log.Printf("Parsing template: %s", path)
content, err := webAssetsFS.ReadFile(path)
if err != nil {
return err
return nil, err
}
_, err = tmpl.New(filepath.Base(path)).Parse(string(content))
return err
})
filename := filepath.Base(path)
name := filename
if err != nil {
return nil, err
_, err = tmpl.New(name).Parse(string(content))
if err != nil {
return nil, err
}
}
return tmpl, nil

Loading…
Cancel
Save