Add service request detail page from service request list

This commit is contained in:
Eli Ribble 2026-02-17 19:51:50 +00:00
parent b6264da972
commit b78837cbab
No known key found for this signature in database
5 changed files with 280 additions and 256 deletions

View file

@ -20,6 +20,7 @@ import (
func addFuncMap(t *template.Template) {
funcMap := template.FuncMap{
"bigNumber": bigNumber,
"duration": duration,
"hasPassed": hasPassed,
"html": unescapeHTML,
"json": unescapeJS,
@ -53,6 +54,54 @@ func bigNumber(n int) string {
return result.String()
}
func duration(d time.Duration) string {
seconds := int(d.Seconds())
if seconds < 60 {
if seconds == 1 {
return "1 second ago"
}
return fmt.Sprintf("%d seconds ago", seconds)
}
minutes := int(d.Minutes())
if minutes < 60 {
if minutes == 1 {
return "1 minute ago"
}
return fmt.Sprintf("%d minutes ago", minutes)
}
hours := int(d.Hours())
if hours < 24 {
if hours == 1 {
return "1 hour ago"
}
return fmt.Sprintf("%d hours ago", hours)
}
days := hours / 24
if days < 30 {
if days == 1 {
return "1 day ago"
}
return fmt.Sprintf("%d days ago", days)
}
months := days / 30
if months < 12 {
if months == 1 {
return "1 month ago"
}
return fmt.Sprintf("%d months ago", months)
}
years := days / 365
if years == 1 {
return "1 year ago"
}
return fmt.Sprintf("%d years ago", years)
}
func publicReportID(s string) string {
if len(s) != 12 {
return s

View file

@ -1,6 +1,6 @@
{{ template "sync/layout/base.html" . }}
{{ template "sync/layout/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "title" }}Service Request Detail{{ end }}
{{ define "extraheader" }}
<style>
.district-logo {
@ -89,25 +89,6 @@
</style>
{{ end }}
{{ define "content" }}
<!-- Header -->
<header class="bg-light py-3">
<div class="container">
<div class="row align-items-center">
<div class="col-md-6">
<h1 class="district-name">[District Name]</h1>
</div>
<div class="col-md-6 text-md-end">
<img
src="placeholder-logo.png"
alt="District Logo"
class="district-logo"
/>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="py-5">
<div class="container">
<!-- Report Header -->
@ -366,22 +347,4 @@
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-dark text-white py-4">
<div class="container">
<div class="row">
<div class="col-md-6">
<p class="mb-0">
&copy; 2023 [District Name] Mosquito Management District
</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0">
Contact: (555) 123-4567 | info@mosquitodistrict.gov
</p>
</div>
</div>
</div>
</footer>
{{ end }}

View file

@ -137,134 +137,82 @@
</tr>
</thead>
<tbody>
<tr>
<td>2 hours ago</td>
<td>2 hours ago</td>
<td>
<span class="badge bg-warning text-dark"
>Schedule Appointment</span
>
</td>
<td>123 Main St, Anytown</td>
<td><i class="bi bi-image me-1"></i>3</td>
<td>
<span class="badge badge-nuisance text-white"
>Biting Nuisance</span
>
</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>5 hours ago</td>
<td>1 hour ago</td>
<td>
<span class="badge bg-info text-dark"
>Answer Question</span
>
</td>
<td>456 Elm St, Anytown</td>
<td><i class="bi bi-image me-1"></i>1</td>
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>1 day ago</td>
<td>3 hours ago</td>
<td><span class="badge bg-success">Add to Route</span></td>
<td>789 Oak Ave, Anytown</td>
<td><i class="bi bi-image me-1"></i>4</td>
<td>
<span class="badge badge-breeding text-white"
>Active Breeding</span
>
</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>2 days ago</td>
<td>6 hours ago</td>
<td><span class="badge bg-secondary">Review</span></td>
<td>101 Pine Lane, Anytown</td>
<td><i class="bi bi-image me-1"></i>2</td>
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>3 days ago</td>
<td>1 day ago</td>
<td>
<span class="badge bg-danger">Confirm Details</span>
</td>
<td>202 Maple Dr, Anytown</td>
<td><i class="bi bi-image me-1"></i>0</td>
<td>
<span class="badge badge-nuisance text-white"
>Biting Nuisance</span
>
</td>
<td>
<button class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
{{ range .C.ActiveRequests }}
<tr>
<td>{{ .Created|timeRelative }}</td>
<td>{{ .LastAction|timeRelative }}</td>
{{ if eq .NextStep "schedule-appointment" }}
<td>
<span class="badge bg-warning text-dark"
>Schedule Appointment</span
>
</td>
{{ else if eq .NextStep "answer-question" }}
<td>
<span class="badge bg-info text-dark"></span>Answer
Question
</td>
{{ else if eq .NextStep "add-to-route" }}
<td>
<span class="badge bg-success text-dark"></span>Add to
Route
</td>
{{ else if eq .NextStep "review" }}
<td>
<span class="badge bg-secondary text-dark"></span
>Review
</td>
{{ else if eq .NextStep "confirm-details" }}
<td>
<span class="badge bg-danger text-dark"></span>Confirm
Details
</td>
{{ else }}
<td>
<span class="badge bg-light text-dark"></span>Unknown
</td>
{{ end }}
<td>{{ .Address }}</td>
<td><i class="bi bi-image me-1"></i>{{ .PhotoCount }}</td>
{{ if eq .Type "biting-nuisance" }}
<td>
<span class="badge badge-nuisance text-white"
>Biting Nuisance</span
>
</td>
{{ else if eq .Type "standing-water" }}
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
{{ else if eq .Type "active-breeding" }}
<td>
<span class="badge badge-standing text-white"
>Active Breeding</span
>
</td>
{{ else }}
<td>
<span class="badge badge-unknown text-white"
>Unknown</span
>
</td>
{{ end }}
<td>
<a
href="{{ .URLDetail }}"
class="btn btn-sm btn-outline-info"
><i class="bi bi-eye"></i
></a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<nav>
<ul class="pagination pagination-sm justify-content-end">
<li class="page-item disabled">
<a class="page-link" href="#">Previous</a>
</li>
<li class="page-item active">
<a class="page-link" href="#">1</a>
</li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
@ -290,90 +238,53 @@
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 fs-5"></i>
<span>John Smith</span>
</div>
</td>
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
<td>1 day ago</td>
<td>303 Cedar St, Anytown</td>
<td>3 days</td>
<td>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 fs-5"></i>
<span>Maria Garcia</span>
</div>
</td>
<td>
<span class="badge badge-nuisance text-white"
>Biting Nuisance</span
>
</td>
<td>2 days ago</td>
<td>404 Birch Ave, Anytown</td>
<td>1 day</td>
<td>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 fs-5"></i>
<span>Robert Johnson</span>
</div>
</td>
<td>
<span class="badge badge-breeding text-white"
>Active Breeding</span
>
</td>
<td>4 days ago</td>
<td>505 Spruce Ct, Anytown</td>
<td>5 days</td>
<td>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 fs-5"></i>
<span>Sarah Lee</span>
</div>
</td>
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
<td>1 week ago</td>
<td>606 Willow Way, Anytown</td>
<td>2 days</td>
<td>
<button class="btn btn-sm btn-outline-info">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
{{ range .C.ClosedRequests }}
<tr>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-person-circle me-2 fs-5"></i>
<span>{{ .Employee }}</span>
</div>
</td>
{{ if eq .Type "standing-water" }}
<td>
<span class="badge badge-standing text-white"
>Standing Water</span
>
</td>
{{ else if eq .Type "biting-nuisance" }}
<td>
<span class="badge badge-nuisance text-white"
>Biting Nuisance</span
>
</td>
{{ else if eq .Type "active-breeding" }}
<td>
<span class="badge badge-breeding text-white"
>Active Breeding</span
>
</td>
{{ else }}
<td>
<span class="badge badge-unknown text-white"
>Unknown</span
>
</td>
{{ end }}
<td>{{ .Closed|timeRelative }}</td>
<td>{{ .Address }}/td></td>
<td>{{ .TimeToResolution|duration }}</td>
<td>
<a
href="{{ .URLDetail }}"
class="btn btn-sm btn-outline-info"
><i class="bi bi-eye"></i
></a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>

View file

@ -30,7 +30,6 @@ func Router() chi.Router {
addMock(r, "/mock/report/{code}/evidence", "sync/mock/report-evidence.html")
addMock(r, "/mock/report/{code}/schedule", "sync/mock/report-schedule.html")
addMock(r, "/mock/report/{code}/update", "sync/mock/report-update.html")
addMock(r, "/mock/service-request/{code}", "sync/mock/service-request-detail.html")
// Utility endpoints
r.Get("/oauth/refresh", getOAuthRefresh)
@ -56,6 +55,7 @@ func Router() chi.Router {
r.Method("POST", "/pool/upload", auth.NewEnsureAuth(postPoolUpload))
r.Method("GET", "/radar", auth.NewEnsureAuth(getRadar))
r.Method("GET", "/service-request", authenticatedHandler(getServiceRequestList))
r.Method("GET", "/service-request/{id}", authenticatedHandler(getServiceRequestDetail))
r.Method("GET", "/setting", auth.NewEnsureAuth(getSetting))
r.Method("GET", "/setting/organization", auth.NewEnsureAuth(getSettingOrganization))
r.Method("GET", "/setting/integration", auth.NewEnsureAuth(getSettingIntegration))
@ -83,16 +83,16 @@ func (e *errorWithStatus) Error() string {
return e.Message
}
type handlerFunction func(context.Context, *models.User) (string, interface{}, *errorWithStatus)
type handlerFunction[T any] func(context.Context, *models.User) (string, T, *errorWithStatus)
type wrappedHandler func(http.ResponseWriter, *http.Request)
type contentAuthenticated struct {
C interface{}
type contentAuthenticated[T any] struct {
C T
URL ContentURL
User User
}
// w http.ResponseWriter, r *http.Request, u *models.User) {
func authenticatedHandler(f handlerFunction) http.Handler {
func authenticatedHandler[T any](f handlerFunction[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u *models.User) {
ctx := r.Context()
userContent, err := contentForUser(ctx, u)
@ -106,7 +106,7 @@ func authenticatedHandler(f handlerFunction) http.Handler {
http.Error(w, err.Error(), e.Status)
return
}
html.RenderOrError(w, template, contentAuthenticated{
html.RenderOrError(w, template, contentAuthenticated[T]{
C: content,
URL: newContentURL(),
User: userContent,

View file

@ -2,13 +2,114 @@ package sync
import (
"context"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
)
type contentServiceRequestPlaceholder struct{}
type contentActiveServiceRequest struct {
Created time.Time
LastAction time.Time
NextStep string
Address string
PhotoCount uint
Type string
URLDetail string
}
type contentClosedServiceRequest struct {
Employee string
Type string
Closed time.Time
Address string
TimeToResolution time.Duration
URLDetail string
}
type contentServiceRequestDetail struct{}
type contentServiceRequestList struct {
ActiveRequests []contentActiveServiceRequest
ClosedRequests []contentClosedServiceRequest
}
func getServiceRequestDetail(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) {
content := contentServiceRequestDetail{}
return "sync/service-request-detail.html", content, nil
}
func getServiceRequestList(ctx context.Context, user *models.User) (string, interface{}, *errorWithStatus) {
content := contentServiceRequestPlaceholder{}
now := time.Now()
content := contentServiceRequestList{
ActiveRequests: []contentActiveServiceRequest{
contentActiveServiceRequest{
Created: now.Add(-2 * time.Hour),
LastAction: now.Add(-2 * time.Hour),
NextStep: "schedule-appointment",
Address: "123 Main St, Anytown",
PhotoCount: 3,
Type: "biting-nuisance",
URLDetail: config.MakeURLNidus("/service-request/1"),
},
contentActiveServiceRequest{
Created: now.Add(-5 * time.Hour),
LastAction: now.Add(-1 * time.Hour),
NextStep: "answer-question",
Address: "456 Elm St, Anytown",
PhotoCount: 1,
Type: "standing-water",
URLDetail: config.MakeURLNidus("/service-request/1"),
},
contentActiveServiceRequest{
Created: now.Add(-1 * 24 * time.Hour),
LastAction: now.Add(-3 * time.Hour),
NextStep: "add-to-route",
Address: "789 Oak St, Anytown",
PhotoCount: 4,
Type: "active-breeding",
URLDetail: config.MakeURLNidus("/service-request/1"),
},
contentActiveServiceRequest{
Created: now.Add(-2 * 24 * time.Hour),
LastAction: now.Add(-6 * time.Hour),
NextStep: "review",
Address: "101 Pine Ln, Anytown",
PhotoCount: 0,
Type: "standing-water",
URLDetail: config.MakeURLNidus("/service-request/1"),
},
},
ClosedRequests: []contentClosedServiceRequest{
contentClosedServiceRequest{
Employee: "John Smith",
Type: "standing-water",
Closed: now.Add(-1 * 24 * time.Hour),
Address: "303 Ceder St, Anytown",
TimeToResolution: 3 * 24 * time.Hour,
URLDetail: config.MakeURLNidus("/service-request/2"),
},
contentClosedServiceRequest{
Employee: "Maria Garcia",
Type: "biting-nuisance",
Closed: now.Add(-2 * 24 * time.Hour),
Address: "404 Birch St, Anytown",
TimeToResolution: 1 * 24 * time.Hour,
URLDetail: config.MakeURLNidus("/service-request/2"),
},
contentClosedServiceRequest{
Employee: "Robert Johnson",
Type: "active-breeding",
Closed: now.Add(-4 * 24 * time.Hour),
Address: "404 Birch St, Anytown",
TimeToResolution: 5 * 24 * time.Hour,
URLDetail: config.MakeURLNidus("/service-request/2"),
},
contentClosedServiceRequest{
Employee: "Sarah Lee",
Type: "standing-water",
Closed: now.Add(-7 * 24 * time.Hour),
Address: "606 Willow Way, Anytown",
TimeToResolution: 2 * 24 * time.Hour,
URLDetail: config.MakeURLNidus("/service-request/2"),
},
},
}
return "sync/service-request-list.html", content, nil
}