Rework template system, merge templates

The embedded portion doesn't work yet.
This commit is contained in:
Eli Ribble 2026-02-07 05:51:21 +00:00
parent eb6e54a0f7
commit 0265e9d3ec
No known key found for this signature in database
163 changed files with 11637 additions and 9183 deletions

View file

@ -1,99 +1,124 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Welcome</title> <title>Welcome</title>
<style type="text/css"> <style type="text/css">
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
font-size: 16px; font-size: 16px;
line-height: 1.5; line-height: 1.5;
color: #333333; color: #333333;
} }
.container { .container {
max-width: 600px; max-width: 600px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
.view-browser { .view-browser {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
color: #777777; color: #777777;
padding-bottom: 15px; padding-bottom: 15px;
} }
.view-browser a { .view-browser a {
color: #555555; color: #555555;
text-decoration: underline; text-decoration: underline;
} }
.header { .header {
text-align: center; text-align: center;
padding: 20px 0; padding: 20px 0;
} }
.logo { .logo {
max-width: 150px; max-width: 150px;
height: auto; height: auto;
} }
.content { .content {
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 30px; padding: 30px;
border-radius: 5px; border-radius: 5px;
} }
.button { .button {
display: inline-block; display: inline-block;
background-color: #0066cc; background-color: #0066cc;
color: #ffffff; color: #ffffff;
text-decoration: none; text-decoration: none;
padding: 12px 25px; padding: 12px 25px;
border-radius: 4px; border-radius: 4px;
margin: 20px 0; margin: 20px 0;
font-weight: bold; font-weight: bold;
} }
.footer { .footer {
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
color: #777777; color: #777777;
padding: 20px 0; padding: 20px 0;
} }
.footer a { .footer a {
color: #777777; color: #777777;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
{{if not .IsBrowser}} {{ if not .IsBrowser }}
<div class="view-browser"> <div class="view-browser">
Email not displaying correctly? <a href="{{.C.URLBrowser}}">View it in your browser</a> Email not displaying correctly?
</div> <a href="{{ .C.URLBrowser }}">View it in your browser</a>
{{end}} </div>
{{ end }}
<div class="header">
<!-- Logo Placeholder -->
<img src="{{.C.URLLogo}}" alt="Report Mosquitoes Online Logo" class="logo"></img>
</div>
<div class="content">
<h1>Welcome</h1>
<p>We're sending you this email because it's the first time we've gotten this email address ({{.C.Destination}}).</p>
<p>If you'd rather not receive emails from us you can reply with "Unsubscribe" in the subject or body of the email. You can also use the "Unsubscribe" feature of your mail client, if it supports list unsubscribes.</p>
<p>If instead you'd like to confirm that you're willing to receive emails at this address, you can do so by clicking below:</p>
<div style="text-align: center;">
<a href="{{.C.URLSubscribe}}" class="button">I want emails from Report Mosquitoes Online</a> <div class="header">
<!-- Logo Placeholder -->
<img
src="{{ .C.URLLogo }}"
alt="Report Mosquitoes Online Logo"
class="logo"
/>
</div>
<div class="content">
<h1>Welcome</h1>
<p>
We're sending you this email because it's the first time we've gotten
this email address ({{ .C.Destination }}).
</p>
<p>
If you'd rather not receive emails from us you can reply with
"Unsubscribe" in the subject or body of the email. You can also use
the "Unsubscribe" feature of your mail client, if it supports list
unsubscribes.
</p>
<p>
If instead you'd like to confirm that you're willing to receive emails
at this address, you can do so by clicking below:
</p>
<div style="text-align: center;">
<a href="{{ .C.URLSubscribe }}" class="button"
>I want emails from Report Mosquitoes Online</a
>
</div>
</div>
<div class="footer">
<p>
This email was sent to you because you or someone else gave your email
address to Report Mosquitoes Online.
</p>
<p>
If you no longer wish to receive these updates,
<a href="{{ .C.URLUnsubscribe }}">click here to unsubscribe</a>.
</p>
<p>&copy; 2026 Gleipnir LLC. All rights reserved.</p>
</div> </div>
</div> </div>
</body>
<div class="footer">
<p>This email was sent to you because you or someone else gave your email address to Report Mosquitoes Online.</p>
<p>If you no longer wish to receive these updates, <a href="{{.C.URLUnsubscribe}}">click here to unsubscribe</a>.</p>
<p>&copy; 2026 Gleipnir LLC. All rights reserved.</p>
</div>
</div>
</body>
</html> </html>

106
html/embed.go Normal file
View file

@ -0,0 +1,106 @@
package html
import (
"bytes"
"embed"
//"errors"
"fmt"
"html/template"
//"io"
"io/fs"
//"math"
"net/http"
//"path"
//"strconv"
"strings"
//"time"
//"github.com/Gleipnir-Technology/nidus-sync/config"
//"github.com/aarondl/opt/null"
//"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
//go:embed template/*
var embeddedFiles embed.FS
type templateSystemEmbed struct {
allTemplates *template.Template
nameToTemplate map[string]*template.Template
sourceFS fs.FS
}
func (ts templateSystemEmbed) loadAll() error {
ts.nameToTemplate = make(map[string]*template.Template, 0)
err := fs.WalkDir(ts.sourceFS, "template", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
short_path := removeLeadingDir(path)
new_t, err := loadTemplateEmbedded(ts.sourceFS, short_path)
if err != nil {
return fmt.Errorf("Failed to add load template '%s': %w", short_path, err)
}
_, err = ts.allTemplates.AddParseTree(new_t.Name(), new_t.Tree)
if err != nil {
return fmt.Errorf("Failed to add parsed template '%s': %w", path, err)
}
ts.nameToTemplate[short_path] = new_t
log.Debug().Str("path", path).Str("short_path", short_path).Msg("Loaded template")
return nil
})
if err != nil {
return fmt.Errorf("failed to load embeded templates: %w", err)
}
return nil
}
func (ts templateSystemEmbed) renderOrError(w http.ResponseWriter, template_name string, context interface{}) {
buf := &bytes.Buffer{}
t, err := loadTemplateEmbedded(ts.sourceFS, template_name)
if err != nil {
log.Error().Err(err).Str("template_name", template_name).Msg("Failed to load embedded template")
RespondError(w, "Failed to load template", err, http.StatusInternalServerError)
return
}
for name, templ := range ts.nameToTemplate {
_, err := t.AddParseTree(name, templ.Tree)
if err != nil {
log.Error().Err(err).Str("name", name).Msg("Failed to add parse tree")
RespondError(w, "Failed to add template", err, http.StatusInternalServerError)
return
}
}
err = t.ExecuteTemplate(buf, template_name, context)
if err != nil {
log.Error().Err(err).Str("template_name", template_name).Msg("Failed to render embedded template")
RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
func loadTemplateEmbedded(sourceFS fs.FS, path string) (*template.Template, error) {
content, err := fs.ReadFile(sourceFS, "template/"+path)
if err != nil {
return nil, fmt.Errorf("error reading template template/%s: %w", path, err)
}
new_t := template.New(path)
addFuncMap(new_t)
_, err = new_t.Parse(string(content))
if err != nil {
return nil, fmt.Errorf("error parsing '%s': %w", path, err)
}
return new_t, nil
}
func removeLeadingDir(path string) string {
parts := strings.SplitN(path, "/", 2)
if len(parts) == 2 {
return parts[1]
}
return path
}

244
html/filesystem.go Normal file
View file

@ -0,0 +1,244 @@
package html
import (
"bytes"
//"embed"
//"errors"
"fmt"
"html/template"
//"io"
"io/fs"
//"math"
"net/http"
"os"
//"path"
//"strconv"
//"strings"
//"time"
//"github.com/Gleipnir-Technology/nidus-sync/config"
//"github.com/aarondl/opt/null"
//"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
// The filesystem being used
var templates templateSystem
type templateSystem interface {
loadAll() error
renderOrError(http.ResponseWriter, string, interface{})
}
type templateSystemDisk struct {
sourceFS fs.FS
}
func LoadTemplates() error {
_, err := os.Stat("html/template")
if err == nil {
templates = templateSystemDisk{
sourceFS: os.DirFS("./html/template"),
}
} else {
templates = templateSystemEmbed{
allTemplates: template.New("all"),
sourceFS: embeddedFiles,
}
templates.loadAll()
}
return nil
}
func (ts templateSystemDisk) loadAll() error {
return nil
}
func (ts templateSystemDisk) renderOrError(w http.ResponseWriter, template_name string, context interface{}) {
t, err := ts.parseTemplate(template_name)
if err != nil {
log.Error().Err(err).Str("template_name", template_name).Msg("Failed to parse template")
RespondError(w, "Failed to parse template", err, http.StatusInternalServerError)
return
}
err = ts.addSupportingTemplates(t)
if err != nil {
log.Error().Err(err).Str("template_name", template_name).Msg("Failed to add supporting templates")
RespondError(w, "Failed to add supporting templates", err, http.StatusInternalServerError)
return
}
err = ts.addSVGTemplates(t)
if err != nil {
log.Error().Err(err).Str("template_name", template_name).Msg("Failed to add supporting templates")
RespondError(w, "Failed to add supporting templates", err, http.StatusInternalServerError)
return
}
buf := &bytes.Buffer{}
err = t.Execute(buf, context)
if err != nil {
log.Error().Err(err).Msg("Failed to render template")
RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
func (ts templateSystemDisk) addSupportingTemplates(t *template.Template) error {
err := fs.WalkDir(ts.sourceFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
content, err := fs.ReadFile(ts.sourceFS, path)
if err != nil {
return fmt.Errorf("error reading template %s: %w", path, err)
}
new_t := template.New(path)
addFuncMap(new_t)
_, err = new_t.Parse(string(content))
if err != nil {
return fmt.Errorf("error parsing '%s': %w", path, err)
}
_, err = t.AddParseTree(new_t.Name(), new_t.Tree)
if err != nil {
return fmt.Errorf("error adding parse tree '%s': %w", path, err)
}
log.Debug().Str("path", path).Msg("Read template")
return nil
})
if err != nil {
return fmt.Errorf("error walking template directory: %w", err)
}
return nil
}
func (ts templateSystemDisk) addSVGTemplates(t *template.Template) error {
svg_fs, err := fs.Sub(ts.sourceFS, "svg")
if err != nil {
return fmt.Errorf("Failed to get svg subdir: %w", err)
}
svgs, err := fs.ReadDir(svg_fs, ".")
if err != nil {
log.Warn().Msg("Failed to read svg directory")
return nil
}
for _, svg := range svgs {
content, err := fs.ReadFile(svg_fs, svg.Name())
if err != nil {
return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
}
svg_name := svg.Name()
svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
svg_t, err := template.New(svg_name).Parse(svg_template)
if err != nil {
return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
}
_, err = t.AddParseTree(svg_t.Name(), svg_t.Tree)
if err != nil {
return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
}
log.Debug().Str("name", svg_name).Msg("add svg template")
}
return nil
}
func (ts templateSystemDisk) parseTemplate(filename string) (*template.Template, error) {
t := template.New(filename)
log.Debug().Str("filename", filename).Msg("parsing template")
addFuncMap(t)
content, err := fs.ReadFile(ts.sourceFS, filename)
if err != nil {
return nil, fmt.Errorf("error reading template %s: %w", filename, err)
}
_, err = t.Parse(string(content))
if err != nil {
return nil, fmt.Errorf("error parsing '%s': %w", filename, err)
}
return t, nil
}
func addSVGTemplates(fsys fs.FS, templ *template.Template) error {
svgs, err := fs.ReadDir(fsys, ".")
if err != nil {
log.Warn().Msg("Failed to read svg directory")
return nil
}
for _, svg := range svgs {
content, err := fs.ReadFile(fsys, svg.Name())
if err != nil {
return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
}
svg_name := svg.Name()
svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
svg_t, err := template.New(svg_name).Parse(svg_template)
if err != nil {
return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
}
_, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree)
if err != nil {
return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
}
//log.Debug().Str("name", svg_name).Msg("add svg template")
}
return nil
}
/*
func executeTemplate(w io.Writer, data any) error {
if bt.template == nil {
name := path.Base(bt.files[0])
templ, err := parseFromDisk(bt.subdir, bt.files)
if err != nil {
return fmt.Errorf("Failed to parse template file: %w", err)
}
if templ == nil {
w.Write([]byte("Failed to read from disk: "))
return errors.New("Template parsing failed")
}
//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
return templ.ExecuteTemplate(w, name, data)
} else {
name := path.Base(bt.files[0])
return bt.template.ExecuteTemplate(w, name, data)
}
}
func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
funcMap := makeFuncMap()
// Remap the file names to embedded paths
embeddedFilePaths := make([]string, 0)
for _, f := range files {
embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
}
name := path.Base(embeddedFilePaths[0])
log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
t, err := template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...)
if err != nil {
panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err))
}
svg_fs, err := fs.Sub(embeddedFiles, "template/svg")
if err != nil {
panic(fmt.Sprintf("Failed to read static/svg: %v", err))
}
err = addSVGTemplates(svg_fs, t)
if err != nil {
panic(fmt.Sprintf("Failed to add SVG templates: %v", err))
}
return t
}
func parseFromDisk(subdir string, files []string) (*template.Template, error) {
funcMap := makeFuncMap()
name := path.Base(files[0])
//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
if err != nil {
return nil, fmt.Errorf("Failed to parse %s: %w", files, err)
}
fsys := os.DirFS(subdir + "/template/svg")
err = addSVGTemplates(fsys, templ)
if err != nil {
return nil, fmt.Errorf("Failed to add SVGs from disk: %w", err)
}
return templ, nil
}
*/

211
html/func.go Normal file
View file

@ -0,0 +1,211 @@
package html
import (
//"bytes"
"fmt"
"html/template"
//"io/fs"
"math"
//"net/http"
//"os"
"strconv"
"strings"
"time"
"github.com/aarondl/opt/null"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
func addFuncMap(t *template.Template) {
funcMap := template.FuncMap{
"bigNumber": bigNumber,
"html": unescapeHTML,
"json": unescapeJS,
"GISStatement": gisStatement,
"latLngDisplay": latLngDisplay,
"publicReportID": publicReportID,
"timeAsRelativeDate": timeAsRelativeDate,
"timeDelta": timeDelta,
"timeElapsed": timeElapsed,
"timeInterval": timeInterval,
"timeSince": timeSince,
"timeSincePtr": timeSincePtr,
"uuidShort": uuidShort,
}
t.Funcs(funcMap)
}
func bigNumber(n int) string {
// Convert the number to a string
numStr := strconv.FormatInt(int64(n), 10)
// Add commas every three digits from the right
var result strings.Builder
for i, char := range numStr {
if i > 0 && (len(numStr)-i)%3 == 0 {
result.WriteByte(',')
}
result.WriteRune(char)
}
return result.String()
}
func publicReportID(s string) string {
if len(s) != 12 {
return s
}
return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
}
func timeAsRelativeDate(d time.Time) string {
return d.Format("01-02")
}
// FormatTimeDuration returns a human-readable string representing a time.Duration
// as "X units early" or "X units late"
func timeDelta(d time.Duration) string {
suffix := "late"
if d < 0 {
suffix = "early"
d = -d // Make duration positive for calculations
}
const (
day = 24 * time.Hour
week = 7 * day
)
log.Info().Int64("delta", int64(d)).Str("suffix", suffix).Msg("Time delta")
switch {
case d >= week:
weeks := d / week
if weeks == 1 {
return "1 week " + suffix
}
return fmt.Sprintf("%d weeks %s", weeks, suffix)
case d >= day:
days := d / day
if days == 1 {
return "1 day " + suffix
}
return fmt.Sprintf("%d days %s", days, suffix)
case d >= time.Hour:
hours := d / time.Hour
if hours == 1 {
return "1 hour " + suffix
}
return fmt.Sprintf("%d hours %s", hours, suffix)
case d >= time.Minute:
minutes := d / time.Minute
if minutes == 1 {
return "1 minute " + suffix
}
return fmt.Sprintf("%d minutes %s", minutes, suffix)
default:
seconds := d / time.Second
if seconds == 1 {
return "1 second " + suffix
}
return fmt.Sprintf("%d seconds %s", seconds, suffix)
}
}
func timeElapsed(seconds null.Val[float32]) string {
if !seconds.IsValue() {
return "none"
}
s := int(seconds.MustGet())
hours := s / 3600
remainder := s - (hours * 3600)
minutes := remainder / 60
remainder = remainder - (minutes * 60)
if hours > 0 {
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder)
} else if minutes > 0 {
return fmt.Sprintf("%02d:%02d", minutes, remainder)
} else {
return fmt.Sprintf("%d seconds", remainder)
}
}
func timeInterval(d time.Duration) string {
seconds := d.Seconds()
// Less than 120 seconds -> show in seconds
if seconds < 120 {
return fmt.Sprintf("every %d seconds", int(math.Round(seconds)))
}
minutes := d.Minutes()
// Less than 120 minutes -> show in minutes
if minutes < 120 {
return fmt.Sprintf("every %d minutes", int(math.Round(minutes)))
}
hours := d.Hours()
// Less than 48 hours -> show in hours
if hours < 48 {
return fmt.Sprintf("every %d hours", int(math.Round(hours)))
}
days := hours / 24
// Less than 14 days -> show in days
if days < 14 {
return fmt.Sprintf("every %d days", int(math.Round(days)))
}
weeks := days / 7
// Less than 8 weeks -> show in weeks
if weeks < 8 {
return fmt.Sprintf("every %d weeks", int(math.Round(weeks)))
}
months := days / 30
// Less than 24 months -> show in months
if months < 24 {
return fmt.Sprintf("every %d months", int(math.Round(months)))
}
years := days / 365
return fmt.Sprintf("every %d years", int(math.Round(years)))
}
func timeSincePtr(t *time.Time) string {
if t == nil {
return "never"
}
return timeSince(*t)
}
func timeSince(t time.Time) string {
now := time.Now()
diff := now.Sub(t)
hours := diff.Hours()
if hours < 1 {
minutes := diff.Minutes()
return fmt.Sprintf("%d minutes ago", int(minutes))
} else if hours < 24 {
return fmt.Sprintf("%d hours ago", int(hours))
} else {
days := hours / 24
return fmt.Sprintf("%d days ago", int(days))
}
}
func unescapeHTML(s string) template.HTML {
return template.HTML(s)
}
func unescapeJS(s string) template.JS {
return template.JS(s)
}
func uuidShort(uuid uuid.UUID) string {
s := uuid.String()
if len(s) < 7 {
return s // Return as is if too short
}
return s[:3] + "..." + s[len(s)-4:]
}

View file

@ -1,351 +1,9 @@
package html package html
import ( import (
"bytes"
"embed"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"math"
"net/http" "net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/aarondl/opt/null"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
) )
var TemplatesByFilename = make(map[string]BuiltTemplate, 0) func RenderOrError(w http.ResponseWriter, template_name string, content interface{}) {
templates.renderOrError(w, template_name, content)
type BuiltTemplate struct {
files []string
subdir string
// Nil if we are going to read templates off disk every time we render
// because we are in development mode.
template *template.Template
}
func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error {
if bt.template == nil {
name := path.Base(bt.files[0])
templ, err := parseFromDisk(bt.subdir, bt.files)
if err != nil {
return fmt.Errorf("Failed to parse template file: %w", err)
}
if templ == nil {
w.Write([]byte("Failed to read from disk: "))
return errors.New("Template parsing failed")
}
//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
return templ.ExecuteTemplate(w, name, data)
} else {
name := path.Base(bt.files[0])
return bt.template.ExecuteTemplate(w, name, data)
}
}
func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *BuiltTemplate {
files_on_disk := true
for _, f := range files {
_, err := os.Stat(f)
if err != nil {
files_on_disk = false
if !config.IsProductionEnvironment() {
log.Warn().Str("file", f).Msg("template file is not on disk")
}
break
}
}
var result BuiltTemplate
if files_on_disk {
result = BuiltTemplate{
files: files,
subdir: subdir,
template: nil,
}
} else {
result = BuiltTemplate{
files: files,
subdir: subdir,
template: parseEmbedded(embeddedFiles, subdir, files),
}
}
TemplatesByFilename[path.Base(files[0])] = result
return &result
}
func RenderOrError(w http.ResponseWriter, template *BuiltTemplate, context interface{}) {
buf := &bytes.Buffer{}
err := template.executeTemplate(buf, context)
if err != nil {
log.Error().Err(err).Strs("files", template.files).Msg("Failed to render template")
RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
return
}
buf.WriteTo(w)
}
func bigNumber(n int) string {
// Convert the number to a string
numStr := strconv.FormatInt(int64(n), 10)
// Add commas every three digits from the right
var result strings.Builder
for i, char := range numStr {
if i > 0 && (len(numStr)-i)%3 == 0 {
result.WriteByte(',')
}
result.WriteRune(char)
}
return result.String()
}
func makeFuncMap() template.FuncMap {
funcMap := template.FuncMap{
"bigNumber": bigNumber,
"html": unescapeHTML,
"json": unescapeJS,
"GISStatement": gisStatement,
"latLngDisplay": latLngDisplay,
"publicReportID": publicReportID,
"timeAsRelativeDate": timeAsRelativeDate,
"timeDelta": timeDelta,
"timeElapsed": timeElapsed,
"timeInterval": timeInterval,
"timeSince": timeSince,
"timeSincePtr": timeSincePtr,
"uuidShort": uuidShort,
}
return funcMap
}
func addSVGTemplates(fsys fs.FS, templ *template.Template) error {
svgs, err := fs.ReadDir(fsys, ".")
if err != nil {
log.Warn().Msg("Failed to read svg directory")
return nil
}
for _, svg := range svgs {
content, err := fs.ReadFile(fsys, svg.Name())
if err != nil {
return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err)
}
svg_name := svg.Name()
svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content))
svg_t, err := template.New(svg_name).Parse(svg_template)
if err != nil {
return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)
}
_, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree)
if err != nil {
return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err)
}
//log.Debug().Str("name", svg_name).Msg("add svg template")
}
return nil
}
func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template {
funcMap := makeFuncMap()
// Remap the file names to embedded paths
embeddedFilePaths := make([]string, 0)
for _, f := range files {
embeddedFilePaths = append(embeddedFilePaths, strings.TrimPrefix(f, subdir))
}
name := path.Base(embeddedFilePaths[0])
log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
t, err := template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...)
if err != nil {
panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err))
}
svg_fs, err := fs.Sub(embeddedFiles, "template/svg")
if err != nil {
panic(fmt.Sprintf("Failed to read static/svg: %v", err))
}
err = addSVGTemplates(svg_fs, t)
if err != nil {
panic(fmt.Sprintf("Failed to add SVG templates: %v", err))
}
return t
}
func parseFromDisk(subdir string, files []string) (*template.Template, error) {
funcMap := makeFuncMap()
name := path.Base(files[0])
//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
templ, err := template.New(name).Funcs(funcMap).ParseFiles(files...)
if err != nil {
return nil, fmt.Errorf("Failed to parse %s: %w", files, err)
}
fsys := os.DirFS(subdir + "/template/svg")
err = addSVGTemplates(fsys, templ)
if err != nil {
return nil, fmt.Errorf("Failed to add SVGs from disk: %w", err)
}
return templ, nil
}
func publicReportID(s string) string {
if len(s) != 12 {
return s
}
return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
}
func timeAsRelativeDate(d time.Time) string {
return d.Format("01-02")
}
// FormatTimeDuration returns a human-readable string representing a time.Duration
// as "X units early" or "X units late"
func timeDelta(d time.Duration) string {
suffix := "late"
if d < 0 {
suffix = "early"
d = -d // Make duration positive for calculations
}
const (
day = 24 * time.Hour
week = 7 * day
)
log.Info().Int64("delta", int64(d)).Str("suffix", suffix).Msg("Time delta")
switch {
case d >= week:
weeks := d / week
if weeks == 1 {
return "1 week " + suffix
}
return fmt.Sprintf("%d weeks %s", weeks, suffix)
case d >= day:
days := d / day
if days == 1 {
return "1 day " + suffix
}
return fmt.Sprintf("%d days %s", days, suffix)
case d >= time.Hour:
hours := d / time.Hour
if hours == 1 {
return "1 hour " + suffix
}
return fmt.Sprintf("%d hours %s", hours, suffix)
case d >= time.Minute:
minutes := d / time.Minute
if minutes == 1 {
return "1 minute " + suffix
}
return fmt.Sprintf("%d minutes %s", minutes, suffix)
default:
seconds := d / time.Second
if seconds == 1 {
return "1 second " + suffix
}
return fmt.Sprintf("%d seconds %s", seconds, suffix)
}
}
func timeElapsed(seconds null.Val[float32]) string {
if !seconds.IsValue() {
return "none"
}
s := int(seconds.MustGet())
hours := s / 3600
remainder := s - (hours * 3600)
minutes := remainder / 60
remainder = remainder - (minutes * 60)
if hours > 0 {
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainder)
} else if minutes > 0 {
return fmt.Sprintf("%02d:%02d", minutes, remainder)
} else {
return fmt.Sprintf("%d seconds", remainder)
}
}
func timeInterval(d time.Duration) string {
seconds := d.Seconds()
// Less than 120 seconds -> show in seconds
if seconds < 120 {
return fmt.Sprintf("every %d seconds", int(math.Round(seconds)))
}
minutes := d.Minutes()
// Less than 120 minutes -> show in minutes
if minutes < 120 {
return fmt.Sprintf("every %d minutes", int(math.Round(minutes)))
}
hours := d.Hours()
// Less than 48 hours -> show in hours
if hours < 48 {
return fmt.Sprintf("every %d hours", int(math.Round(hours)))
}
days := hours / 24
// Less than 14 days -> show in days
if days < 14 {
return fmt.Sprintf("every %d days", int(math.Round(days)))
}
weeks := days / 7
// Less than 8 weeks -> show in weeks
if weeks < 8 {
return fmt.Sprintf("every %d weeks", int(math.Round(weeks)))
}
months := days / 30
// Less than 24 months -> show in months
if months < 24 {
return fmt.Sprintf("every %d months", int(math.Round(months)))
}
years := days / 365
return fmt.Sprintf("every %d years", int(math.Round(years)))
}
func timeSincePtr(t *time.Time) string {
if t == nil {
return "never"
}
return timeSince(*t)
}
func timeSince(t time.Time) string {
now := time.Now()
diff := now.Sub(t)
hours := diff.Hours()
if hours < 1 {
minutes := diff.Minutes()
return fmt.Sprintf("%d minutes ago", int(minutes))
} else if hours < 24 {
return fmt.Sprintf("%d hours ago", int(hours))
} else {
days := hours / 24
return fmt.Sprintf("%d days ago", int(days))
}
}
func unescapeHTML(s string) template.HTML {
return template.HTML(s)
}
func unescapeJS(s string) template.JS {
return template.JS(s)
}
func uuidShort(uuid uuid.UUID) string {
s := uuid.String()
if len(s) < 7 {
return s // Return as is if too short
}
return s[:3] + "..." + s[len(s)-4:]
} }

View file

@ -42,7 +42,7 @@
</head> </head>
<body> <body>
{{ template "content" . }} {{ template "content" . }}
{{ template "footer" . }} {{ template "rmo/component/footer.html" . }}
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script> <script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View file

@ -1,4 +1,4 @@
{{ define "footer" }} {{ define "rmo/component/footer.html" }}
<footer class="bg-dark text-white py-4"> <footer class="bg-dark text-white py-4">
<div class="container"> <div class="container">
<div class="row"> <div class="row">

View file

@ -0,0 +1,14 @@
{{ define "rmo/component/header-district.html" }}
<header
class="navbar navbar-expand-lg navbar-dark text-light bg-primary shadow-sm"
>
<div class="container">
<div class="d-flex align-items-center">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="{{ .URLLogo }}" style="height: 48px;" alt="District logo" />
</a>
<h1 class="mb-0 ms-3">{{ .Name }}</h1>
</div>
</div>
</header>
{{ end }}

View file

@ -0,0 +1,17 @@
{{ define "rmo/component/header-rmo.html" }}
<header
class="navbar navbar-expand-lg navbar-dark text-light bg-primary shadow-sm"
>
<div class="container">
<div class="d-flex align-items-center">
<a class="navbar-brand d-flex align-items-center" href="/">
<img
class="banner"
src="/static/img/rmo/banner.jpg"
alt="Report Mosquitoes Online logo"
/>
</a>
</div>
</div>
</header>
{{ end }}

View file

@ -0,0 +1,2 @@
{{ define "rmo/component/map-header.html" }}
{{ end }}

View file

@ -1,4 +1,4 @@
{{ define "map" }} {{ define "rmo/component/map.html" }}
<script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script> <script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script>
<link <link
href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css" href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css"

View file

@ -1,4 +1,4 @@
{{ define "photo-upload-header" }} {{ define "rmo/compnonent/photo-upload-header.html" }}
<style> <style>
.photo-upload-area { .photo-upload-area {
border: 2px dashed #ccc; border: 2px dashed #ccc;

View file

@ -1,4 +1,4 @@
{{ define "photo-upload" }} {{ define "rmo/component/photo-upload.html" }}
<div class="photo-upload-area"> <div class="photo-upload-area">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View file

@ -0,0 +1,29 @@
{{ template "rmo/base.html" . }}
{{ define "title" }}Districts{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
<!-- Main Content -->
<main>
<h1>District List</h1>
<table>
<tbody>
<tr>
<th>Logo</th>
<th>Name</th>
<th>URL</th>
</tr>
{{ range .Districts }}
<tr>
<td>
<a href="{{ .URLWebsite }}"><img src="{{ .URLLogo }}" /></a>
</td>
<td>{{ .Name }}</td>
<td><a href="{{ .URLRMO }}">{{ .URLRMO }}</a></td>
</tr>
{{ end }}
</tbody>
</table>
</main>
{{ end }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Report Nuisance{{ end }} {{ define "title" }}Report Nuisance{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
@ -165,9 +165,9 @@ document.addEventListener('DOMContentLoaded', function() {
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
{{ if (eq .District nil) }} {{ if (eq .District nil) }}
{{ template "header-rmo" . }} {{ template "rmo/component/header-rmo.html" . }}
{{ else }} {{ else }}
{{ template "header-district" .District }} {{ template "rmo/component/header-district.html" .District }}
{{ end }} {{ end }}
<div class="container"> <div class="container">
<div class="row mb-4"> <div class="row mb-4">

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Privacy Policy{{ end }} {{ define "title" }}Privacy Policy{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
{{ end }} {{ end }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Quick Report Complete{{ end }} {{ define "title" }}Quick Report Complete{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Quick Report{{ end }} {{ define "title" }}Quick Report{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Notification Request Complete{{ end }} {{ define "title" }}Notification Request Complete{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Main{{ end }} {{ define "title" }}Main{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -0,0 +1,265 @@
{{ template "rmo/base.html" . }}
{{ define "title" }}Status{{ end }}
{{ define "extraheader" }}
<script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script>
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css"
rel="stylesheet"
/>
<script src="/static/js/geocode.js"></script>
<script src="/static/js/location.js"></script>
<script src="/static/js/map-multipoint.js"></script>
<script src="/static/js/report-table.js"></script>
<style>
.map-container {
background-color: #e9ecef;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 20px;
}
#map {
height: 500px;
width: 100%;
margin-bottom: 10px;
}
#map img {
max-width: none;
min-width: 0px;
height: auto;
}
.search-box {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
@media (max-width: 768px) {
.map-container {
height: 300px;
}
}
</style>
<script>
const MAPBOX_ACCESS_TOKEN = '{{.MapboxToken}}';
var markers = [];
// Because features come from tiled vector data, feature geometries may be split
// or duplicated across tile boundaries. As a result, features may appear
// multiple times in query results.
function getUniqueFeatures(features, comparatorProperty) {
const uniqueIds = new Set();
const uniqueFeatures = [];
for (const feature of features) {
const id = feature.properties[comparatorProperty];
if (!uniqueIds.has(id)) {
uniqueIds.add(id);
uniqueFeatures.push(feature);
}
}
return uniqueFeatures;
}
function renderReports(features) {
console.log("render reports", features);
const report_table = document.querySelector('report-table');
let reports = [];
for (const feature of features) {
reports.push({
address: feature.properties.address,
created: feature.properties.created,
id: feature.properties.public_id,
status: feature.properties.status,
type: feature.properties.table_name
});
}
report_table.reports = reports;
}
function onLoad() {
const map = document.querySelector("map-multipoint");
map.addEventListener("load", (event) => {
map.addSource('tegola-mosquito', {
'type': 'vector',
'tiles': [
'{{.URLTegola}}maps/mosquito/{z}/{x}/{y}'
]
});
map.addLayer({
'id': 'mosquito',
'source': 'tegola-mosquito',
'source-layer': 'report_location',
'type': 'circle',
'paint': {
'circle-color': '#4264fb',
'circle-radius': 7,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
map.on('render', () => {
const features = map.queryRenderedFeatures({target: {layerId: 'mosquito'}});
if (features) {
const uniqueFeatures = getUniqueFeatures(features, 'public_id');
// Populate features for the listing overlay.
renderReports(uniqueFeatures);
}
});
getGeolocation({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}).then(position => {
map.jumpTo({
center: {
lng: position.coords.longitude,
lat: position.coords.latitude,
},
zoom: 14,
});
}).catch(error => {
console.log("location error", error);
})
});
}
document.addEventListener('DOMContentLoaded', onLoad);
</script>
{{ end }}
{{ define "content" }}
<div class="container my-4">
<!-- Search Box -->
<div class="card search-box mb-4">
<div class="card-body">
<form class="row g-3 align-items-center">
<div class="col-md-9">
<label for="addressSearch" class="visually-hidden"
>Search by address</label
>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input
type="text"
class="form-control form-control-lg"
id="addressSearch"
placeholder="Enter an address, neighborhood, or zip code"
/>
</div>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary btn-lg w-100">
Search
</button>
</div>
<div class="col-12">
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="mosquitoNuisance"
checked
/>
<label class="form-check-label" for="mosquitoNuisance"
>Mosquito Nuisance</label
>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="greenPool"
checked
/>
<label class="form-check-label" for="greenPool">Green Pool</label>
</div>
<div class="form-check form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="quickReport"
checked
/>
<label class="form-check-label" for="quickReport"
>Quick Report</label
>
</div>
</div>
</form>
</div>
</div>
<!-- Map Section -->
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h5 class="mb-0"><i class="bi bi-pin-map-fill me-2"></i>Reports Map</h5>
</div>
<div class="card-body p-0">
<map-multipoint
api-key="{{ .MapboxToken }}"
latitude="36.3"
longitude="-119.2"
tegola="{{ .URLTegola }}"
zoom="9"
/>
</div>
</div>
<!-- Results Section -->
<div class="card">
<div
class="card-header bg-primary text-white d-flex justify-content-between align-items-center"
>
<h5 class="mb-0"><i class="bi bi-list-ol me-2"></i>Reports Near You</h5>
<span class="badge bg-light text-dark">15 Reports Found</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<report-table />
</div>
</div>
<!--
<div class="card-footer">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mb-0">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true">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>
<!-- Create Report Button (Fixed at bottom on mobile) -->
<div
class="d-md-none position-fixed bottom-0 start-0 end-0 p-3"
style="z-index: 1030;"
>
<button
class="btn btn-primary btn-lg w-100"
data-bs-toggle="modal"
data-bs-target="#reportModal"
>
<i class="bi bi-plus-circle-fill me-2"></i>Create New Report
</button>
</div>
<!-- Desktop Create Report Button -->
<div class="d-none d-md-block text-center mt-4">
<button
class="btn btn-primary btn-lg"
data-bs-toggle="modal"
data-bs-target="#reportModal"
>
<i class="bi bi-plus-circle-fill me-2"></i>Create New Report
</button>
</div>
</div>
{{ end }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Status of report {{ .Report.ID|publicReportID }}{{ end }} {{ define "title" }}Status of report {{ .Report.ID|publicReportID }}{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
@ -57,9 +57,9 @@ document.addEventListener("DOMContentLoaded", onLoad);
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
{{ if (eq .District nil) }} {{ if (eq .District nil) }}
{{ template "header-rmo" . }} {{ template "rmo/component/header-rmo.html" . }}
{{ else }} {{ else }}
{{ template "header-district" .District }} {{ template "rmo/component/header-district.html" .District }}
{{ end }} {{ end }}
<div class="container my-4"> <div class="container my-4">
<!-- Report ID and Status Section --> <!-- Report ID and Status Section -->

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Status{{ end }} {{ define "title" }}Status{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
@ -168,9 +168,9 @@ document.addEventListener('DOMContentLoaded', onLoad);
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
{{ if (eq .District nil) }} {{ if (eq .District nil) }}
{{ template "header-rmo" . }} {{ template "rmo/component/header-rmo.html" . }}
{{ else }} {{ else }}
{{ template "header-district" .District }} {{ template "rmo/component/header-district.html" .District }}
{{ end }} {{ end }}
<div class="container my-4"> <div class="container my-4">
<!-- Search Box --> <!-- Search Box -->

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Report Submission Complete{{ end }} {{ define "title" }}Report Submission Complete{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Privacy Policy{{ end }} {{ define "title" }}Privacy Policy{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
{{ end }} {{ end }}

View file

@ -1,4 +1,4 @@
{{ template "base.html" . }} {{ template "rmo/base.html" . }}
{{ define "title" }}Report Standing Water{{ end }} {{ define "title" }}Report Standing Water{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
@ -146,9 +146,9 @@ document.addEventListener('DOMContentLoaded', function() {
{{ end }} {{ end }}
{{ define "content" }} {{ define "content" }}
{{ if (eq .District nil) }} {{ if (eq .District nil) }}
{{ template "header-rmo" . }} {{ template "rmo/component/header-rmo.html" . }}
{{ else }} {{ else }}
{{ template "header-district" .District }} {{ template "rmo/component/header-district.html" .District }}
{{ end }} {{ end }}
<!-- Main Content --> <!-- Main Content -->
<main class="py-5"> <main class="py-5">

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

@ -0,0 +1,503 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
body {
overflow-x: hidden;
}
.map-container {
height: 250px;
background-color: #e9ecef;
border-radius: 4px;
}
.calendar-container {
background-color: #f8f9fa;
border-radius: 4px;
}
.calendar-day {
width: 2.5rem;
height: 2.5rem;
line-height: 2.5rem;
text-align: center;
}
.calendar-day.has-events {
position: relative;
}
.calendar-day.has-events::after {
content: "";
position: absolute;
bottom: 0.2rem;
left: 50%;
transform: translateX(-50%);
height: 0.25rem;
border-radius: 0.125rem;
background-color: #0d6efd;
}
.calendar-day.has-events.light::after {
width: 0.25rem;
background-color: #0d6efd;
}
.calendar-day.has-events.medium::after {
width: 0.5rem;
background-color: #fd7e14;
}
.calendar-day.has-events.heavy::after {
width: 0.75rem;
background-color: #dc3545;
}
.trap-increase {
color: #dc3545;
}
.trap-decrease {
color: #28a745;
}
.status-active {
background-color: #d4edda;
}
.status-break {
background-color: #fff3cd;
}
.status-transit {
background-color: #cce5ff;
}
.tech-table tr td {
vertical-align: middle;
}
</style>
{{ end }}
{{ define "content" }}
<!-- Header -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="#">
<img
src="https://via.placeholder.com/40"
alt="{{ .DistrictName }} Logo"
class="me-2"
/>
<span>{{ .DistrictName }} Mosquito Abatement District</span>
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
>
<span class="navbar-toggler-icon"></span>
</button>
<!--
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link active" href="#"><i class="bi bi-house-door"></i> Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="bi bi-people"></i> Residents</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="bi bi-clipboard-data"></i> Reports</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="bi bi-gear"></i> Settings</a>
</li>
</ul>
</div>-->
</div>
</nav>
<div class="container-fluid py-3">
<div class="row mb-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body py-2">
<form action="/phone-call/search-results" method="GET">
<div class="input-group">
<input
type="text"
class="form-control"
placeholder="Search by ID (e.g., easy twin jazz cats), address, GPS, name or phone..."
aria-label="Search"
/>
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i> Search
</button>
</div>
<small class="form-text text-muted"
>Quick search for request ID, address, coordinates, contact
name, or phone number</small
>
</form>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Left Column -->
<div class="col-md-8 mb-3">
<div class="card shadow-sm h-100">
<div
class="card-header bg-success text-white d-flex justify-content-between align-items-center py-2"
>
<h5 class="mb-0">Mosquito Activity & Relief</h5>
<a href="/mock/admin/service-request" class="btn btn-light btn-sm"
><i class="bi bi-plus-circle"></i> New Service Request</a
>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-12">
<label class="form-label mb-1">Location of Concern</label>
<div
class="map-container d-flex justify-content-center align-items-center"
>
<div class="text-center text-muted">
<i class="bi bi-map fs-1"></i>
<p>Interactive map will be loaded here</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-2">
<h6 class="mb-1">Recent Trap Counts</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Trap Location</th>
<th>Current</th>
<th>Week Δ</th>
<th>Month Δ</th>
<th>YoY</th>
</tr>
</thead>
<tbody>
<tr>
<td>Elmwood Park</td>
<td>47</td>
<td class="trap-decrease">-12%</td>
<td class="trap-decrease">-23%</td>
<td class="trap-increase">+5%</td>
</tr>
<tr>
<td>Riverside Dr</td>
<td>32</td>
<td class="trap-increase">+8%</td>
<td class="trap-decrease">-5%</td>
<td class="trap-decrease">-10%</td>
</tr>
<tr>
<td>Oakdale Creek</td>
<td>53</td>
<td class="trap-increase">+15%</td>
<td class="trap-increase">+22%</td>
<td class="trap-increase">+17%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h6 class="mb-1">Nearby Service Requests</h6>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Status</th>
<th>Type</th>
<th>Distance</th>
</tr>
</thead>
<tbody>
<tr>
<td>10/15/23</td>
<td><span class="badge bg-success">Completed</span></td>
<td>Green Pool</td>
<td>0.2 mi</td>
</tr>
<tr>
<td>10/18/23</td>
<td>
<span class="badge bg-warning text-dark"
>Scheduled</span
>
</td>
<td>Mosquito Nuisance</td>
<td>0.3 mi</td>
</tr>
<tr>
<td>10/19/23</td>
<td>
<span class="badge bg-info text-dark">Accepted</span>
</td>
<td>Previous Source</td>
<td>0.5 mi</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column - Calendar -->
<div class="col-md-4 mb-3">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white py-2">
<h5 class="mb-0">Calendar</h5>
</div>
<div class="card-body p-2">
<div class="calendar-container p-2 mb-2">
<div
class="d-flex justify-content-between align-items-center mb-2"
>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-chevron-left"></i>
</button>
<h6 class="mb-0">October 2023</h6>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<table class="table table-sm mb-0">
<thead>
<tr class="text-center">
<th>Su</th>
<th>Mo</th>
<th>Tu</th>
<th>We</th>
<th>Th</th>
<th>Fr</th>
<th>Sa</th>
</tr>
</thead>
<tbody>
<tr class="text-center">
<td class="calendar-day text-muted">1</td>
<td class="calendar-day text-muted has-events light">2</td>
<td class="calendar-day text-muted has-events medium">3</td>
<td class="calendar-day text-muted">4</td>
<td class="calendar-day text-muted has-events light">5</td>
<td class="calendar-day text-muted">6</td>
<td class="calendar-day text-muted">7</td>
</tr>
<tr class="text-center">
<td class="calendar-day text-muted">8</td>
<td class="calendar-day text-muted has-events medium">9</td>
<td class="calendar-day text-muted has-events light">10</td>
<td class="calendar-day text-muted has-events heavy">11</td>
<td class="calendar-day text-muted has-events medium">
12
</td>
<td class="calendar-day text-muted has-events light">13</td>
<td class="calendar-day text-muted">14</td>
</tr>
<tr class="text-center">
<td class="calendar-day text-muted">15</td>
<td class="calendar-day text-muted has-events medium">
16
</td>
<td class="calendar-day text-muted has-events light">17</td>
<td class="calendar-day text-muted has-events medium">
18
</td>
<td class="calendar-day text-muted has-events heavy">19</td>
<td class="calendar-day text-muted has-events medium">
20
</td>
<td class="calendar-day text-muted">21</td>
</tr>
<tr class="text-center">
<td class="calendar-day">22</td>
<td
class="calendar-day bg-primary text-white rounded has-events heavy"
>
23
</td>
<td class="calendar-day has-events medium">24</td>
<td class="calendar-day has-events light">25</td>
<td class="calendar-day has-events medium">26</td>
<td class="calendar-day has-events heavy">27</td>
<td class="calendar-day">28</td>
</tr>
<tr class="text-center">
<td class="calendar-day">29</td>
<td class="calendar-day has-events medium">30</td>
<td class="calendar-day has-events light">31</td>
<td class="calendar-day text-muted">1</td>
<td class="calendar-day text-muted has-events light">2</td>
<td class="calendar-day text-muted">3</td>
<td class="calendar-day text-muted">4</td>
</tr>
</tbody>
</table>
<div class="d-flex justify-content-center mt-1 small">
<span class="me-3"
><span
class="d-inline-block bg-primary rounded-circle"
style="width:8px;height:8px"
></span>
Light</span
>
<span class="me-3"
><span
class="d-inline-block bg-warning rounded-circle"
style="width:8px;height:8px"
></span>
Medium</span
>
<span
><span
class="d-inline-block bg-danger rounded-circle"
style="width:8px;height:8px"
></span>
Heavy</span
>
</div>
</div>
<h6 class="mb-1">Today's Schedule - October 23, 2023</h6>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th>Time</th>
<th>Address</th>
<th>Type</th>
<th>Technician</th>
</tr>
</thead>
<tbody>
<tr>
<td>8:00 AM</td>
<td>123 Maple St</td>
<td>Nuisance</td>
<td>S. Johnson</td>
</tr>
<tr>
<td>9:30 AM</td>
<td>456 Oak Ave</td>
<td>Green Pool</td>
<td>M. Williams</td>
</tr>
<tr class="table-active">
<td>11:00 AM</td>
<td>789 Pine Ln</td>
<td>Prev Source</td>
<td>L. Rodriguez</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Technician Roster - Table View -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-info text-white py-2">
<h5 class="mb-0">Today's Technician Roster</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-sm tech-table mb-0">
<thead>
<tr>
<th>Technician</th>
<th>Scheduled</th>
<th>Completed</th>
<th>Phone</th>
<th>Status</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://via.placeholder.com/32"
class="rounded-circle me-2"
alt="Photo"
/>
<span>Sarah Johnson</span>
</div>
</td>
<td>8</td>
<td>5</td>
<td>(555) 234-5678</td>
<td><span class="badge bg-success">Servicing</span></td>
<td>123 Maple St, Zone 3</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://via.placeholder.com/32"
class="rounded-circle me-2"
alt="Photo"
/>
<span>Mark Williams</span>
</div>
</td>
<td>7</td>
<td>3</td>
<td>(555) 345-6789</td>
<td>
<span class="badge bg-warning text-dark">On Break</span>
</td>
<td>Office - Lunchroom</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://via.placeholder.com/32"
class="rounded-circle me-2"
alt="Photo"
/>
<span>Lisa Rodriguez</span>
</div>
</td>
<td>9</td>
<td>6</td>
<td>(555) 456-7890</td>
<td><span class="badge bg-primary">In Transit</span></td>
<td>En route to 789 Pine Ln</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://via.placeholder.com/32"
class="rounded-circle me-2"
alt="Photo"
/>
<span>Carlos Martinez</span>
</div>
</td>
<td>6</td>
<td>4</td>
<td>(555) 567-8901</td>
<td><span class="badge bg-success">Servicing</span></td>
<td>202 Birch Dr, Zone 2</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ template "title" . }} - Nidus Sync</title>
<!-- Bootstrap CSS -->
<link href="/static/css/bootstrap.css" rel="stylesheet" />
<!-- Fontawesome Icons -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
/>
<!-- Bootstrap Icons -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
/>
<!-- favicon -->
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
{{ block "extraheader" . }}{{ end }}
</head>
<body>
{{ template "sync/component/icons.html" }}
<div class="d-flex">
{{ if .User }}
{{ template "sync/component/sidebar.html" .User }}
{{ end }}
</div>
{{ template "content" . }}
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
<script>
document
.getElementById("sidebarToggle")
.addEventListener("click", function () {
document.getElementById("sidebar").classList.toggle("collapsed");
document.getElementById("content").classList.toggle("expanded");
});
</script>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ template "title" . }} - Nidus Sync</title>
<!-- Bootstrap CSS -->
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet" />
<!-- Bootstrap Icons -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"
/>
<!-- Fontawesome Icons -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
/>
<!-- favicon -->
<link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" />
{{ block "extraheader" . }}{{ end }}
</head>
<body>
{{ template "content" . }}
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,257 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ template "map" .MapData }}
<style>
.address-container {
display: flex;
flex-direction: column;
justify-content: center;
}
.section-header {
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 mb-5">
<!-- Location Header Section -->
<div class="row mb-4">
<div class="col-12">
<h1>Location Data View</h1>
</div>
</div>
<!-- Map and Address Section - Side by Side -->
<div class="row mb-4">
<div class="col-md-8">
<div class="map-container">
<div id="map"></div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body address-container">
<h5>Approximate Address:</h5>
<p class="lead" id="location-address">
123 Main St, Anytown, ST 12345
</p>
<hr class="location-divider" />
<h5>Cell Coordinates (Hexagon):</h5>
<div class="table-responsive">
<table class="coordinates-table">
<tbody>
{{ range $i, $cb := .CellBoundary }}
<tr>
<td><strong>Vertex {{ $i }}:</strong></td>
<td>{{ $cb|latLngDisplay }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<hr class="location-divider" />
<p>{{ .CellBoundary|GISStatement }}</p>
</div>
</div>
</div>
</div>
<!-- Two-Column Layout for Tables -->
<div class="row">
<!-- Left Column -->
<div class="col-md-6">
<!-- Breeding Sources Section -->
<h2 class="section-header">Mosquito Breeding Sources</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Source Type</th>
<th>Last Inspected</th>
<th>Last Treated</th>
</tr>
</thead>
<tbody>
{{ range .BreedingSources }}
<tr>
<td>
<a href="/source/{{ .ID }}">{{ .ID|uuidShort }}</a>
</td>
<td>{{ .Type }}</td>
<td>{{ .LastInspected|timeSincePtr }}</td>
<td>{{ .LastTreated|timeSincePtr }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!--
<nav aria-label="Breeding sources pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">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>
<!-- Inspections Section -->
<h2 class="section-header">Inspections History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>LocationID</th>
<th>Location</th>
<th>Date</th>
<th>Action</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{{ range .Inspections }}
<tr>
<td>
<a href="/source/{{ .LocationID }}"
>{{ .LocationID|uuidShort }}</a
>
</td>
<td>{{ .Location }}</td>
<td>{{ .Date|timeSincePtr }}</td>
<td>{{ .Action }}</td>
<td>{{ .Notes }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!--
<nav aria-label="Inspections pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">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>
<!-- Right Column -->
<div class="col-md-6">
<h2 class="section-header">Traps</h2>
{{ if gt (len .Traps) 0 }}
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Active</th>
<th>Comments</th>
</tr>
</thead>
<tbody>
{{ range .Traps }}
<tr>
<td>
<a href="/trap/{{ .GlobalID }}"
>{{ .GlobalID|uuidShort }}</a
>
</td>
<td>{{ .Active }}</td>
<td>{{ .Comments }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
{{ else }}
<p>No traps</p>
{{ end }}
<!-- Treatments Section -->
<h2 class="section-header">Treatment History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Location</th>
<th>Treatment Date</th>
<th>Insecticide Used</th>
<th>Technician Notes</th>
</tr>
</thead>
<tbody>
{{ range .Treatments }}
<tr>
<td>
<a href="/source/{{ .LocationID }}"
>{{ .LocationID|uuidShort }}</a
>
</td>
<td>{{ .Date|timeSincePtr }}</td>
<td>{{ .Product }}</td>
<td>{{ .Notes }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<!--
<nav aria-label="Treatments pagination">
<ul class="pagination justify-content-center">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1">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>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,149 @@
{{ define "sync/component/header.html" }}
<header class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div class="container">
<!-- Logo -->
<a class="navbar-brand" href="/">
<img
src="/static/img/nidus-logo-no-lettering-64.png"
style="height: 48px; display: flex; align-items: center; justify-content: center; "
/>
</a>
<!-- Toggle Button for Mobile -->
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<!-- Nav Items -->
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">Dashboard</a>
</li>
</ul>
<!-- User Info, Notifications & Logout -->
<div class="d-flex align-items-center">
<!-- Notification Bell with Badge -->
<div class="dropdown me-3">
<a
class="position-relative text-decoration-none"
href="#"
role="button"
id="notificationsDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="bi bi-bell-fill fs-5"></i>
{{ if gt (len .Notifications) 0 }}
<span
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"
>
{{ if gt (len .Notifications) 99 }}
99+
{{ else }}
{{ len .Notifications }}
{{ end }}
<span class="visually-hidden">unread notifications</span>
</span>
{{ end }}
</a>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="notificationsDropdown"
style="width: 300px; max-height: 400px; overflow-y: auto;"
>
<li><h6 class="dropdown-header">Notifications</h6></li>
{{ if gt (len .Notifications) 0 }}
{{ range .Notifications }}
<li>
<a class="dropdown-item py-2" href="{{ .Link }}">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div
class="{{ if eq .Type "alert" }}
bg-danger
{{ else }}
bg-primary
{{ end }} rounded-circle p-1"
>
<i
class="bi bi-{{ if eq .Type "alert" }}
exclamation
{{ else }}
info
{{ end }}-circle text-white small"
></i>
</div>
</div>
<div class="flex-grow-1 ms-2">
<p class="mb-0 small">{{ .Message }}</p>
<span class="text-muted x-small"
>{{ .Time | timeSincePtr }}</span
>
</div>
</div>
</a>
</li>
{{ end }}
<li><hr class="dropdown-divider" /></li>
<li>
<a
class="dropdown-item text-center small"
href="notifications.html"
>View all</a
>
</li>
{{ else }}
<li>
<p class="dropdown-item text-center py-3 text-muted">
No new notifications
</p>
</li>
{{ end }}
</ul>
</div>
<div class="dropdown">
<a
class="text-decoration-none dropdown-toggle d-flex align-items-center"
href="#"
role="button"
id="userDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<div
class="avatar me-2 bg-primary rounded-circle d-flex align-items-center justify-content-center"
style="width: 36px; height: 36px;"
>
<span class="text-white">{{ .Initials }}</span>
</div>
<span class="me-2">{{ .DisplayName }}</span>
</a>
<ul
class="dropdown-menu dropdown-menu-end"
aria-labelledby="userDropdown"
>
<li><a class="dropdown-item" href="/settings">Settings</a></li>
<li><hr class="dropdown-divider" /></li>
<li>
<a class="dropdown-item text-danger" href="/signout"
>Sign out</a
>
</li>
</ul>
</div>
</div>
</div>
</div>
</header>
{{ end }}

View file

@ -0,0 +1,109 @@
{{ define "sync/component/icons.html" }}
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="bootstrap" viewBox="0 0 118 94">
<title>Bootstrap</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M24.509 0c-6.733 0-11.715 5.893-11.492 12.284.214 6.14-.064 14.092-2.066 20.577C8.943 39.365 5.547 43.485 0 44.014v5.972c5.547.529 8.943 4.649 10.951 11.153 2.002 6.485 2.28 14.437 2.066 20.577C12.794 88.106 17.776 94 24.51 94H93.5c6.733 0 11.714-5.893 11.491-12.284-.214-6.14.064-14.092 2.066-20.577 2.009-6.504 5.396-10.624 10.943-11.153v-5.972c-5.547-.529-8.934-4.649-10.943-11.153-2.002-6.484-2.28-14.437-2.066-20.577C105.214 5.894 100.233 0 93.5 0H24.508zM80 57.863C80 66.663 73.436 72 62.543 72H44a2 2 0 01-2-2V24a2 2 0 012-2h18.437c9.083 0 15.044 4.92 15.044 12.474 0 5.302-4.01 10.049-9.119 10.88v.277C75.317 46.394 80 51.21 80 57.863zM60.521 28.34H49.948v14.934h8.905c6.884 0 10.68-2.772 10.68-7.727 0-4.643-3.264-7.207-9.012-7.207zM49.948 49.2v16.458H60.91c7.167 0 10.964-2.876 10.964-8.281 0-5.406-3.903-8.178-11.425-8.178H49.948z"
></path>
</symbol>
<symbol id="home" viewBox="0 0 16 16">
<path
d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"
/>
</symbol>
<symbol id="speedometer2" viewBox="0 0 16 16">
<path
d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z"
/>
<path
fill-rule="evenodd"
d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z"
/>
</symbol>
<symbol id="table" viewBox="0 0 16 16">
<path
d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"
/>
</symbol>
<symbol id="people-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
<path
fill-rule="evenodd"
d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1z"
/>
</symbol>
<symbol id="grid" viewBox="0 0 16 16">
<path
d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"
/>
</symbol>
<symbol id="collection" viewBox="0 0 16 16">
<path
d="M2.5 3.5a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-11zm2-2a.5.5 0 0 1 0-1h7a.5.5 0 0 1 0 1h-7zM0 13a1.5 1.5 0 0 0 1.5 1.5h13A1.5 1.5 0 0 0 16 13V6a1.5 1.5 0 0 0-1.5-1.5h-13A1.5 1.5 0 0 0 0 6v7zm1.5.5A.5.5 0 0 1 1 13V6a.5.5 0 0 1 .5-.5h13a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-13z"
/>
</symbol>
<symbol id="calendar3" viewBox="0 0 16 16">
<path
d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zM1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857V3.857z"
/>
<path
d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"
/>
</symbol>
<symbol id="chat-quote-fill" viewBox="0 0 16 16">
<path
d="M16 8c0 3.866-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.584.296-1.925.864-4.181 1.234-.2.032-.352-.176-.273-.362.354-.836.674-1.95.77-2.966C.744 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7zM7.194 6.766a1.688 1.688 0 0 0-.227-.272 1.467 1.467 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 5.734 6C4.776 6 4 6.746 4 7.667c0 .92.776 1.666 1.734 1.666.343 0 .662-.095.931-.26-.137.389-.39.804-.81 1.22a.405.405 0 0 0 .011.59c.173.16.447.155.614-.01 1.334-1.329 1.37-2.758.941-3.706a2.461 2.461 0 0 0-.227-.4zM11 9.073c-.136.389-.39.804-.81 1.22a.405.405 0 0 0 .012.59c.172.16.446.155.613-.01 1.334-1.329 1.37-2.758.942-3.706a2.466 2.466 0 0 0-.228-.4 1.686 1.686 0 0 0-.227-.273 1.466 1.466 0 0 0-.469-.324l-.008-.004A1.785 1.785 0 0 0 10.07 6c-.957 0-1.734.746-1.734 1.667 0 .92.777 1.666 1.734 1.666.343 0 .662-.095.931-.26z"
/>
</symbol>
<symbol id="cpu-fill" viewBox="0 0 16 16">
<path
d="M6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"
/>
<path
d="M5.5.5a.5.5 0 0 0-1 0V2A2.5 2.5 0 0 0 2 4.5H.5a.5.5 0 0 0 0 1H2v1H.5a.5.5 0 0 0 0 1H2v1H.5a.5.5 0 0 0 0 1H2v1H.5a.5.5 0 0 0 0 1H2A2.5 2.5 0 0 0 4.5 14v1.5a.5.5 0 0 0 1 0V14h1v1.5a.5.5 0 0 0 1 0V14h1v1.5a.5.5 0 0 0 1 0V14h1v1.5a.5.5 0 0 0 1 0V14a2.5 2.5 0 0 0 2.5-2.5h1.5a.5.5 0 0 0 0-1H14v-1h1.5a.5.5 0 0 0 0-1H14v-1h1.5a.5.5 0 0 0 0-1H14v-1h1.5a.5.5 0 0 0 0-1H14A2.5 2.5 0 0 0 11.5 2V.5a.5.5 0 0 0-1 0V2h-1V.5a.5.5 0 0 0-1 0V2h-1V.5a.5.5 0 0 0-1 0V2h-1V.5zm1 4.5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3A1.5 1.5 0 0 1 6.5 5z"
/>
</symbol>
<symbol id="gear-fill" viewBox="0 0 16 16">
<path
d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"
/>
</symbol>
<symbol id="speedometer" viewBox="0 0 16 16">
<path
d="M8 2a.5.5 0 0 1 .5.5V4a.5.5 0 0 1-1 0V2.5A.5.5 0 0 1 8 2zM3.732 3.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 8a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 8zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 7.31A.91.91 0 1 0 8.85 8.569l3.434-4.297a.389.389 0 0 0-.029-.518z"
/>
<path
fill-rule="evenodd"
d="M6.664 15.889A8 8 0 1 1 9.336.11a8 8 0 0 1-2.672 15.78zm-4.665-4.283A11.945 11.945 0 0 1 8 10c2.186 0 4.236.585 6.001 1.606a7 7 0 1 0-12.002 0z"
/>
</symbol>
<symbol id="toggles2" viewBox="0 0 16 16">
<path
d="M9.465 10H12a2 2 0 1 1 0 4H9.465c.34-.588.535-1.271.535-2 0-.729-.195-1.412-.535-2z"
/>
<path
d="M6 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 1a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm.535-10a3.975 3.975 0 0 1-.409-1H4a1 1 0 0 1 0-2h2.126c.091-.355.23-.69.41-1H4a2 2 0 1 0 0 4h2.535z"
/>
<path d="M14 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0z" />
</symbol>
<symbol id="tools" viewBox="0 0 16 16">
<path
d="M1 0L0 1l2.2 3.081a1 1 0 0 0 .815.419h.07a1 1 0 0 1 .708.293l2.675 2.675-2.617 2.654A3.003 3.003 0 0 0 0 13a3 3 0 1 0 5.878-.851l2.654-2.617.968.968-.305.914a1 1 0 0 0 .242 1.023l3.356 3.356a1 1 0 0 0 1.414 0l1.586-1.586a1 1 0 0 0 0-1.414l-3.356-3.356a1 1 0 0 0-1.023-.242L10.5 9.5l-.96-.96 2.68-2.643A3.005 3.005 0 0 0 16 3c0-.269-.035-.53-.102-.777l-2.14 2.141L12 4l-.364-1.757L13.777.102a3 3 0 0 0-3.675 3.68L7.462 6.46 4.793 3.793a1 1 0 0 1-.293-.707v-.071a1 1 0 0 0-.419-.814L1 0zm9.646 10.646a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708zM3 11l.471.242.529.026.287.445.445.287.026.529L5 13l-.242.471-.026.529-.445.287-.287.445-.529.026L3 15l-.471-.242L2 14.732l-.287-.445L1.268 14l-.026-.529L1 13l.242-.471.026-.529.445-.287.287-.445.529-.026L3 11z"
/>
</symbol>
<symbol id="chevron-right" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
/>
</symbol>
<symbol id="geo-fill" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
d="M4 4a4 4 0 1 1 4.5 3.969V13.5a.5.5 0 0 1-1 0V7.97A4 4 0 0 1 4 3.999zm2.493 8.574a.5.5 0 0 1-.411.575c-.712.118-1.28.295-1.655.493a1.319 1.319 0 0 0-.37.265.301.301 0 0 0-.057.09V14l.002.008a.147.147 0 0 0 .016.033.617.617 0 0 0 .145.15c.165.13.435.27.813.395.751.25 1.82.414 3.024.414s2.273-.163 3.024-.414c.378-.126.648-.265.813-.395a.619.619 0 0 0 .146-.15.148.148 0 0 0 .015-.033L12 14v-.004a.301.301 0 0 0-.057-.09 1.318 1.318 0 0 0-.37-.264c-.376-.198-.943-.375-1.655-.493a.5.5 0 1 1 .164-.986c.77.127 1.452.328 1.957.594C12.5 13 13 13.4 13 14c0 .426-.26.752-.544.977-.29.228-.68.413-1.116.558-.878.293-2.059.465-3.34.465-1.281 0-2.462-.172-3.34-.465-.436-.145-.826-.33-1.116-.558C3.26 14.752 3 14.426 3 14c0-.599.5-1 .961-1.243.505-.266 1.187-.467 1.957-.594a.5.5 0 0 1 .575.411z"
/>
</symbol>
</svg>
{{ end }}

View file

@ -1,6 +1,9 @@
{{define "map"}} {{ define "sync/component/map.html" }}
<script src='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js'></script> <script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' /> <link
href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css"
rel="stylesheet"
/>
<script> <script>
const geojson = JSON.parse({{.GeoJSON}}) const geojson = JSON.parse({{.GeoJSON}})
function addMarkers(map, markers) { function addMarkers(map, markers) {
@ -59,26 +62,26 @@ function onLoad() {
} }
window.addEventListener("load", onLoad); window.addEventListener("load", onLoad);
</script> </script>
<style> <style>
.map-container { .map-container {
background-color: #e9ecef; background-color: #e9ecef;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px; height: 500px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
} }
#map { #map {
height: 500px; height: 500px;
width:100%; width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
} }
#map img { #map img {
max-width: none; max-width: none;
min-width: 0px; min-width: 0px;
height: auto; height: auto;
} }
</style> </style>
{{end}} {{ end }}

View file

@ -0,0 +1,57 @@
{{ define "sync/component/sidebar.html" }}
<div id="sidebar">
<div class="sidebar-header">
<div class="logo-container">
<img class="logo" src="/static/img/nidus-logo-256-transparent.png" />
</div>
</div>
<button id="sidebarToggle" class="btn btn-sm p-0">
<i id="sidebarToggleIcon" class="bi bi-chevron-left"></i>
</button>
<ul class="sidebar-menu">
<li>
<a href="/" data-toggle="tooltip" data-placement="right" title="Home">
<div class="menu-icon"><i class="bi bi-house"></i></div>
<span class="menu-text ms-2">Home</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-graph-up"></i></div>
<span class="menu-text ms-2">Analytics</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-file-earmark"></i></div>
<span class="menu-text ms-2">Reports</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-people"></i></div>
<span class="menu-text ms-2">Users</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-chat-dots"></i></div>
<span class="menu-text ms-2">Messages</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-gear"></i></div>
<span class="menu-text ms-2">Settings</span>
</a>
</li>
<li>
<a href="#">
<div class="menu-icon"><i class="bi bi-question-circle"></i></div>
<span class="menu-text ms-2">Help</span>
</a>
</li>
</ul>
</div>
{{ end }}

View file

@ -0,0 +1,246 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script>
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet"
/>
<script src="/static/js/map-aggregate.js"></script>
<style>
body {
background-color: #f8f9fa;
}
.dashboard-container {
padding: 20px 0;
}
.stats-card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
height: 100%;
}
.stats-card:hover {
transform: translateY(-5px);
}
.section-title {
margin: 30px 0 20px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.last-refreshed {
color: #6c757d;
}
.logo-placeholder {
width: 100px;
height: 40px;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.metric-icon {
font-size: 2rem;
margin-bottom: 10px;
display: inline-block;
width: 50px;
height: 50px;
line-height: 50px;
text-align: center;
border-radius: 50%;
}
.metric-value {
font-size: 2rem;
font-weight: bold;
}
.syncing {
color: #28a745;
animation: fa-spin 2s linear infinite;
}
</style>
<script>
function onLoad() {
const map = document.querySelector("map-aggregate");
map.addEventListener("cell-click", (event) => {
window.location.href = "/cell/" + event.detail.cell;
});
}
window.addEventListener("load", onLoad);
</script>
{{ end }}
{{ define "content" }}
<div class="container dashboard-container">
<!-- Dashboard Header -->
<div class="row mb-4">
<div class="col-md-6">
<h1>{{ .User.Organization.Name }} Dashboard</h1>
<p class="text-muted">
Overview of mosquito control activities in your district
</p>
</div>
<div
class="col-md-6 text-md-end d-flex align-items-center justify-content-md-end"
>
{{ if .IsSyncOngoing }}
<p class="last-refreshed mb-0">
<i class="fas fa-sync-alt me-2 syncing"></i>Syncing now...
</p>
{{ else }}
<p class="last-refreshed mb-0">
<i class="fas fa-sync-alt me-2"></i>Last updated:
<span id="last-refreshed-time">{{ .LastSync | timeSincePtr }}</span>
<button class="btn btn-sm btn-outline-primary ms-3">
Refresh Data
</button>
</p>
{{ end }}
</div>
</div>
<!-- Key Metrics -->
<div class="row g-4">
<!-- Last Refreshed -->
<div class="col-md-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="metric-icon bg-success bg-opacity-10 text-white">
<i class="fas fa-clock"></i>
</div>
<h5 class="card-title">Last Data Refresh</h5>
<p class="metric-value">{{ .LastSync | timeSincePtr }}</p>
<!-- <p class="card-text text-muted">Last sync: 12:45 PM</p> -->
</div>
</div>
</div>
<!-- Service Requests -->
<div class="col-md-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="metric-icon bg-warning bg-opacity-10 text-white">
<i class="fas fa-ticket-alt"></i>
</div>
<h5 class="card-title">Service Requests</h5>
{{ if .IsSyncOngoing }}
<p class="metric-value">
{{ .CountServiceRequests | bigNumber }}...?
</p>
{{ else }}
<p class="metric-value">
{{ .CountServiceRequests | bigNumber }}
</p>
{{ end }}
<!--<p class="card-text text-muted">
<span class="text-success">
<i class="fas fa-arrow-up"></i> 12%
</span> since last week
</p>-->
</div>
</div>
</div>
<!-- Mosquito Sources -->
<div class="col-md-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="metric-icon bg-danger bg-opacity-10 text-white">
<i class="fas fa-bug"></i>
</div>
<h5 class="card-title">Mosquito Sources</h5>
{{ if .IsSyncOngoing }}
<p class="metric-value">
{{ .CountMosquitoSources | bigNumber }}..?
</p>
{{ else }}
<p class="metric-value">
{{ .CountMosquitoSources | bigNumber }}
</p>
{{ end }}
<!-- <p class="card-text text-muted">
<span class="text-danger">
<i class="fas fa-arrow-up"></i> 8%
</span> since last month
</p> -->
</div>
</div>
</div>
<!-- Inspections -->
<div class="col-md-3">
<div class="card stats-card h-100">
<div class="card-body text-center">
<div class="metric-icon bg-info bg-opacity-10 text-white">
<i class="fas fa-clipboard-check"></i>
</div>
<h5 class="card-title">Traps</h5>
{{ if .IsSyncOngoing }}
<p class="metric-value">{{ .CountTraps | bigNumber }}...?</p>
{{ else }}
<p class="metric-value">{{ .CountTraps | bigNumber }}</p>
{{ end }}
<!-- <p class="card-text text-muted">
<span class="text-success">
<i class="fas fa-arrow-up"></i> 15%
</span> since last week
</p> -->
</div>
</div>
</div>
</div>
<!-- Map Section -->
<h3 class="section-title">Mosquito Activity Heatmap</h3>
<div class="row">
<div class="col-12">
<map-aggregate
api-key="{{ .MapData.MapboxToken }}"
latitude="36.3"
longitude="-119.2"
organization-id="{{ .User.Organization.ID }}"
tegola="{{ .Config.URLTegola }}"
zoom="9"
/>
</div>
</div>
<!-- Recent Activity Section -->
<h3 class="section-title">Recent Activity</h3>
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Location</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{ range $i, $sr := .RecentRequests }}
<tr>
<td>{{ $sr.Date | timeSincePtr }}</td>
<td>Service Request</td>
<td>{{ $sr.Location }}</td>
<td><span class="badge bg-success">Completed</span></td>
<td>
<a href="#" class="btn btn-sm btn-outline-primary"
>View</a
>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,235 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
<style>
.results-container {
max-width: 1000px;
margin: 0 auto;
}
.error-code {
font-family: monospace;
padding: 0.2rem 0.4rem;
background-color: rgba(220, 53, 69, 0.1);
border-radius: 0.2rem;
color: #dc3545;
}
.error-table {
font-size: 0.9rem;
}
.error-suggestions {
color: #495057;
}
.table-header-error {
width: 40%;
}
.table-header-suggestion {
width: 40%;
}
.table-header-line {
width: 20%;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 results-container mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Upload Failed: pools-data-2023.csv</h2>
<span class="badge bg-danger rounded-pill">
<i class="bi bi-x-circle me-1"></i> Validation Errors
</span>
</div>
<div class="alert alert-danger" role="alert">
<div class="d-flex align-items-center">
<div class="me-3">
<i
class="bi bi-exclamation-triangle-fill"
style="font-size: 2rem;"
></i>
</div>
<div>
<h4 class="alert-heading">CSV Upload Failed</h4>
<p class="mb-0">
Your file contains several errors that must be fixed before it can
be processed. Details are provided below.
</p>
</div>
</div>
</div>
<div class="card mb-4">
<div
class="card-header bg-light d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">Error Summary</h5>
<a href="#" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download me-1"></i> Download Error Log
</a>
</div>
<div class="card-body">
<p>
We found <strong>12 errors</strong> in your CSV file. The most common
issues are:
</p>
<ul>
<li>
<strong>Missing required column:</strong> The "Latitude" column is
not present in your file
</li>
<li>
<strong>Invalid data format:</strong> 8 GPS coordinates contain
non-numeric values
</li>
<li>
<strong>Empty required fields:</strong> 3 records are missing Plat
ID values
</li>
</ul>
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>Tip:</strong> Make sure your column names exactly match the
required format. Column names are case-sensitive.
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Detailed Error Report</h5>
</div>
<div class="table-responsive">
<table class="table table-striped error-table mb-0">
<thead class="table-light">
<tr>
<th class="table-header-line">Line Number</th>
<th class="table-header-error">Error</th>
<th class="table-header-suggestion">Suggestion</th>
</tr>
</thead>
<tbody>
<tr>
<td class="fw-bold text-center">1</td>
<td>
<span class="error-code">MISSING_COLUMN</span><br />
Required column "Latitude" is missing from the header row
</td>
<td class="error-suggestions">
Add a "Latitude" column to your CSV file. Make sure the spelling
and capitalization match exactly.
</td>
</tr>
<tr>
<td class="fw-bold text-center">5</td>
<td>
<span class="error-code">INVALID_DATA_FORMAT</span><br />
GPS coordinate "37.45N" is not a valid decimal number
</td>
<td class="error-suggestions">
Change "37.45N" to a decimal format (e.g., "37.45"). Remove any
non-numeric characters except for the decimal point.
</td>
</tr>
<tr>
<td class="fw-bold text-center">8</td>
<td>
<span class="error-code">EMPTY_REQUIRED_FIELD</span><br />
Plat ID is empty or missing
</td>
<td class="error-suggestions">
Add a Plat ID value for this record. Each pool must have a
unique identifier.
</td>
</tr>
<tr>
<td class="fw-bold text-center">12</td>
<td>
<span class="error-code">INVALID_DATA_FORMAT</span><br />
GPS coordinate "unknown" is not a valid decimal number
</td>
<td class="error-suggestions">
Replace "unknown" with the actual longitude value in decimal
format (e.g., "-122.4194").
</td>
</tr>
<tr>
<td class="fw-bold text-center">17</td>
<td>
<span class="error-code">INVALID_DATA_FORMAT</span><br />
GPS coordinate "N/A" is not a valid decimal number
</td>
<td class="error-suggestions">
Replace "N/A" with the actual latitude value in decimal format
(e.g., "37.7749").
</td>
</tr>
<tr>
<td class="fw-bold text-center">21</td>
<td>
<span class="error-code">EMPTY_REQUIRED_FIELD</span><br />
Plat ID is empty or missing
</td>
<td class="error-suggestions">
Add a Plat ID value for this record. Each pool must have a
unique identifier.
</td>
</tr>
</tbody>
</table>
</div>
<div class="card-footer bg-white">
<nav aria-label="Error pagination">
<ul class="pagination justify-content-center m-0">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true"
>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="#">Next</a>
</li>
</ul>
</nav>
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Next Steps</h5>
</div>
<div class="card-body">
<ol>
<li>
Download the error log for a complete list of issues (optional)
</li>
<li>Fix the errors in your CSV file</li>
<li>Re-upload the corrected file using the button below</li>
</ol>
<div class="alert alert-warning" role="alert">
<i class="bi bi-lightbulb me-2"></i>
<strong>Recommendation:</strong> Review our
<a href="#" class="alert-link">CSV formatting guide</a> to ensure your
file meets all requirements.
</div>
<div class="mt-4 text-center">
<button class="btn btn-primary btn-lg" id="uploadNewBtn">
<i class="bi bi-arrow-repeat me-2"></i> Upload Corrected File
</button>
</div>
</div>
</div>
<div class="mt-4 text-center">
<a href="#" class="text-muted">
<i class="bi bi-question-circle me-1"></i> Need help? Contact support
</a>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,243 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
<style>
.results-container {
max-width: 1200px;
margin: 0 auto;
}
.summary-card {
transition: transform 0.2s;
}
.summary-card:hover {
transform: translateY(-5px);
}
.warning-row {
background-color: rgba(255, 193, 7, 0.15) !important;
}
.status-badge {
font-size: 0.85rem;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 results-container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Upload Results: pools-data-2023.csv</h2>
<span class="badge bg-success rounded-pill">
<i class="bi bi-check-circle me-1"></i> File Parsed Successfully
</span>
</div>
<div class="row mb-4">
<div class="col-md-4">
<div class="card summary-card h-100 border-primary">
<div class="card-body text-center">
<h1 class="display-4 text-primary">45</h1>
<h5>Existing Pools</h5>
<p class="text-muted">Matches found in previous records</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card summary-card h-100 border-success">
<div class="card-body text-center">
<h1 class="display-4 text-success">23</h1>
<h5>New Pools</h5>
<p class="text-muted">Not found in existing records</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card summary-card h-100 border-warning">
<div class="card-body text-center">
<h1 class="display-4 text-warning">4</h1>
<h5>Outside District</h5>
<p class="text-muted">Potential geocoding errors</p>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div
class="card-header bg-light d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">Data Preview</h5>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showIssuesOnly" />
<label class="form-check-label" for="showIssuesOnly"
>Show issues only</label
>
</div>
</div>
<div class="card-body">
<div class="alert alert-warning" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Warning:</strong> 4 entries appear to be outside district
boundaries. These are highlighted in yellow below.
</div>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="table-light">
<tr>
<th>Plat ID</th>
<th>Latitude</th>
<th>Longitude</th>
<th>Street Address</th>
<th>Status</th>
<th>In District</th>
</tr>
</thead>
<tbody>
<tr>
<td>P12345</td>
<td>37.7749</td>
<td>-122.4194</td>
<td>123 Main St, Anytown, CA</td>
<td>
<span class="badge bg-primary status-badge">Existing</span>
</td>
<td>
<i class="bi bi-check-circle-fill text-success"></i> Yes
</td>
</tr>
<tr>
<td>P23456</td>
<td>37.3352</td>
<td>-121.8811</td>
<td>456 Oak Ave, Someville, CA</td>
<td>
<span class="badge bg-primary status-badge">Existing</span>
</td>
<td>
<i class="bi bi-check-circle-fill text-success"></i> Yes
</td>
</tr>
<tr class="warning-row">
<td>P34567</td>
<td>38.5816</td>
<td>-121.4944</td>
<td>789 Pine Rd, Outtown, CA</td>
<td><span class="badge bg-success status-badge">New</span></td>
<td>
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
<strong>No</strong> - Outside northern boundary
</td>
</tr>
<tr>
<td>P45678</td>
<td>37.4419</td>
<td>-122.1430</td>
<td>101 Elm St, Cityville, CA</td>
<td><span class="badge bg-success status-badge">New</span></td>
<td>
<i class="bi bi-check-circle-fill text-success"></i> Yes
</td>
</tr>
<tr>
<td>P56789</td>
<td>37.3541</td>
<td>-121.9552</td>
<td>202 Maple Dr, Townburg, CA</td>
<td>
<span class="badge bg-primary status-badge">Existing</span>
</td>
<td>
<i class="bi bi-check-circle-fill text-success"></i> Yes
</td>
</tr>
<tr class="warning-row">
<td>P67890</td>
<td>35.3733</td>
<td>-119.0187</td>
<td>303 Cedar Ln, Farville, CA</td>
<td><span class="badge bg-success status-badge">New</span></td>
<td>
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
<strong>No</strong> - Outside southern boundary
</td>
</tr>
<tr>
<td>P78901</td>
<td>37.8044</td>
<td>-122.2712</td>
<td>404 Birch Ave, Metroburg, CA</td>
<td>
<span class="badge bg-primary status-badge">Existing</span>
</td>
<td>
<i class="bi bi-check-circle-fill text-success"></i> Yes
</td>
</tr>
<tr class="warning-row">
<td>P89012</td>
<td>37.4032</td>
<td>-123.9612</td>
<td>505 Walnut St, Edgetown, CA</td>
<td><span class="badge bg-success status-badge">New</span></td>
<td>
<i class="bi bi-exclamation-triangle-fill text-warning"></i>
<strong>No</strong> - Outside western boundary
</td>
</tr>
</tbody>
</table>
</div>
<nav aria-label="Table navigation">
<ul class="pagination justify-content-center mt-3">
<li class="page-item disabled">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true"
>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 class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">Notes & Recommendations</h5>
</div>
<div class="card-body">
<div class="mb-3">
<p><strong>Issues detected:</strong></p>
<ul>
<li>
4 pools appear to be outside district boundaries (possible
geocoding errors)
</li>
<li>All required fields are present and properly formatted</li>
</ul>
</div>
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle me-2"></i>
<strong>Note:</strong> You may proceed with this upload or edit your
CSV file to fix the issues identified.
</div>
</div>
</div>
<div class="d-flex justify-content-between mt-4 mb-5">
<a href="#" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Upload Edited File
</a>
<button class="btn btn-primary" id="confirmUploadBtn">
<i class="bi bi-check2 me-1"></i> Confirm and Submit Data
</button>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,131 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
<style>
.upload-container {
max-width: 800px;
margin: 0 auto;
}
.schema-table {
font-size: 0.9rem;
}
.upload-area {
border: 2px dashed #dee2e6;
padding: 2rem;
text-align: center;
margin: 1.5rem 0;
border-radius: 5px;
background-color: #f8f9fa;
}
.required-field::after {
content: "*";
color: red;
margin-left: 3px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 upload-container">
<h2 class="mb-4">Upload Pool Data</h2>
<div class="card mb-4">
<div class="card-header bg-light">
<h5 class="mb-0">CSV Upload Requirements</h5>
</div>
<div class="card-body">
<p>
Your CSV file must contain the following columns in any order. Please
ensure your data matches the required format.
</p>
<table class="table table-bordered schema-table">
<thead class="table-light">
<tr>
<th>Field</th>
<th>Description</th>
<th>Format</th>
<th>Required</th>
</tr>
</thead>
<tbody>
<tr>
<td class="required-field">Latitude</td>
<td>GPS latitude coordinate</td>
<td>Decimal (e.g., 37.7749)</td>
<td>Yes</td>
</tr>
<tr>
<td class="required-field">Longitude</td>
<td>GPS longitude coordinate</td>
<td>Decimal (e.g., -122.4194)</td>
<td>Yes</td>
</tr>
<tr>
<td class="required-field">Plat ID</td>
<td>Unique identifier for the property</td>
<td>Alphanumeric (e.g., P12345)</td>
<td>Yes</td>
</tr>
<tr>
<td>Street Address</td>
<td>Nearest street address to the pool</td>
<td>Text (e.g., 123 Main St)</td>
<td>No</td>
</tr>
</tbody>
</table>
<div class="alert alert-info small">
<i class="bi bi-info-circle"></i> Need a template?
<a href="#" class="alert-link">Download sample CSV file</a>
</div>
</div>
</div>
<div class="card">
<div class="card-header bg-light">
<h5 class="mb-0">Upload Data</h5>
</div>
<div class="card-body">
<div class="upload-area">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-cloud-arrow-up text-primary mb-3"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708l2-2z"
/>
<path
d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"
/>
</svg>
<h5>Select your CSV file</h5>
<p class="text-muted">Drag and drop a file here or click to browse</p>
<input type="file" class="form-control" id="csvFile" accept=".csv" />
</div>
<div class="d-grid gap-2">
<a href="/mock/data-entry/good">
<button class="btn btn-primary" type="button" id="uploadButton">
Upload and Continue
</button>
</a>
<a href="/mock/data-entry/bad"> See what happens on a bad upload </a>
</div>
</div>
</div>
<div class="text-muted text-center mt-4">
<small
>Need assistance? Contact
<a href="mailto:support@example.com">support@example.com</a></small
>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,330 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
<script>
document.addEventListener("DOMContentLoaded", function () {
const checkboxes = document.querySelectorAll(".route-select");
const switchBtn = document.getElementById("switchRoutesBtn");
// Enable/disable switch button based on selection
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
const checkedBoxes = document.querySelectorAll(
".route-select:checked",
);
switchBtn.disabled = checkedBoxes.length !== 2;
});
});
// For demonstration purposes
switchBtn.addEventListener("click", function () {
const selectedRoutes = Array.from(
document.querySelectorAll(".route-select:checked"),
).map((cb) => cb.value);
alert(`Switching routes ${selectedRoutes[0]} and ${selectedRoutes[1]}`);
// In a real application, this would trigger the route switching logic
});
});
</script>
<style>
.main-container {
max-width: 1200px;
margin: 0 auto;
}
.map-container {
height: 400px;
background-color: #e9ecef;
border: 1px solid #dee2e6;
border-radius: 5px;
position: relative;
}
.route-badge {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
border-radius: 50%;
}
.tech-photo {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 50%;
}
.action-button {
padding: 10px 20px;
}
.completion-alert {
border-left: 5px solid #0d6efd;
}
</style>
{{ end }}
{{ define "content" }}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1>Route Calculation Results</h1>
<a href="{{ .URLs.Dispatch }}" class="btn btn-outline-primary">
<i class="bi bi-pencil-square me-1"></i> Edit Parameters
</a>
</div>
<!-- Map View -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Route Map</h2>
</div>
<div class="card-body p-2">
<div
class="map-container d-flex align-items-center justify-content-center"
>
<div class="text-center">
<i class="bi bi-map fs-1 text-muted"></i>
<h3 class="text-muted">Interactive Map View</h3>
<p class="text-muted">
Routes are color-coded by technician assignment
</p>
</div>
</div>
</div>
</div>
<!-- Completion Projection -->
<div class="alert alert-info mb-4 completion-alert">
<div class="d-flex align-items-center">
<i class="bi bi-calendar-check fs-4 me-3"></i>
<div>
<h4 class="h5 mb-1">Coverage Projection</h4>
<p class="mb-0">
If every day were like today, all pools would be complete on
<strong>October 27, 2023</strong>
</p>
</div>
</div>
</div>
<!-- Route Summary Table -->
<div class="card mb-4">
<div
class="card-header bg-primary text-white d-flex justify-content-between align-items-center"
>
<h2 class="h4 mb-0">Route Summary</h2>
<button class="btn btn-sm btn-light" id="switchRoutesBtn" disabled>
<i class="bi bi-arrow-left-right me-1"></i> Switch Selected Routes
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th width="40">Select</th>
<th width="50">Route</th>
<th>Technician</th>
<th>Cold Call Pools</th>
<th>Drone Inspections</th>
<th>Service Calls</th>
<th>Warrants</th>
<th>Est. Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="form-check">
<input
class="form-check-input route-select"
type="checkbox"
value="A"
id="routeA"
/>
</div>
</td>
<td>
<div class="route-badge bg-primary text-white">A</div>
</td>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/men/32.jpg"
alt="John Davis"
class="tech-photo me-2"
/>
<span>John Davis</span>
</div>
</td>
<td>12</td>
<td>0</td>
<td>5</td>
<td>2</td>
<td>6h 15m</td>
<td>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="form-check">
<input
class="form-check-input route-select"
type="checkbox"
value="B"
id="routeB"
/>
</div>
</td>
<td>
<div class="route-badge bg-success text-white">B</div>
</td>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/women/65.jpg"
alt="Sarah Johnson"
class="tech-photo me-2"
/>
<span>Sarah Johnson</span>
</div>
</td>
<td>8</td>
<td>3</td>
<td>4</td>
<td>1</td>
<td>7h 30m</td>
<td>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="form-check">
<input
class="form-check-input route-select"
type="checkbox"
value="C"
id="routeC"
/>
</div>
</td>
<td>
<div class="route-badge bg-danger text-white">C</div>
</td>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/men/44.jpg"
alt="Michael Chen"
class="tech-photo me-2"
/>
<span>Michael Chen</span>
</div>
</td>
<td>10</td>
<td>4</td>
<td>3</td>
<td>0</td>
<td>7h 45m</td>
<td>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
<tr>
<td>
<div class="form-check">
<input
class="form-check-input route-select"
type="checkbox"
value="D"
id="routeD"
/>
</div>
</td>
<td>
<div class="route-badge bg-warning text-dark">D</div>
</td>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/women/22.jpg"
alt="Jessica Martinez"
class="tech-photo me-2"
/>
<span>Jessica Martinez</span>
</div>
</td>
<td>14</td>
<td>2</td>
<td>6</td>
<td>3</td>
<td>8h 00m</td>
<td>
<button class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between">
<a
href="{{ .URLs.Dispatch }}"
class="btn btn-outline-secondary action-button"
>
<i class="bi bi-arrow-left me-2"></i>Back to Parameters
</a>
<a href="{{ .URLs.Root }}" class="btn btn-success action-button">
<i class="bi bi-check-circle me-2"></i>Approve & Dispatch
</a>
</div>
<!-- Modal for Route Details -->
<div
class="modal fade"
id="routeDetailsModal"
tabindex="-1"
aria-labelledby="routeDetailsModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="routeDetailsModalLabel">
Route A Details
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<!-- Route details would go here -->
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,308 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
<style>
.tech-photo {
width: 50px;
height: 50px;
object-fit: cover;
border-radius: 50%;
}
.main-container {
max-width: 1200px;
margin: 0 auto;
}
.action-button {
padding: 15px 30px;
font-size: 1.2rem;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container-fluid p-4 main-container">
<h1 class="mb-4">Technician Routing & Dispatch</h1>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Technician Roster</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Technician</th>
<th>Working Hours</th>
<th>Truck Assignment</th>
<th>Warrant Service</th>
<th>Drone Certified</th>
<th>Routing Options</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/men/32.jpg"
alt="John Davis"
class="tech-photo me-3"
/>
<div>
<div class="fw-bold">John Davis</div>
<div class="small text-muted">ID: T-1001</div>
</div>
</div>
</td>
<td>7:00 AM - 3:30 PM</td>
<td>Truck #103</td>
<td><span class="badge bg-success">Yes</span></td>
<td><span class="badge bg-danger">No</span></td>
<td>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="warrant-partner1"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Require second person for warrant service"
/>
<label class="form-check-label" for="warrant-partner1"
>Warrant Partner</label
>
</div>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="resident-history1"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Route to previously visited residents"
/>
<label class="form-check-label" for="resident-history1"
>Prior Interactions</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="include-route1"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Include in route calculations"
/>
<label class="form-check-label" for="include-route1"
>Include in Routes</label
>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/women/65.jpg"
alt="Sarah Johnson"
class="tech-photo me-3"
/>
<div>
<div class="fw-bold">Sarah Johnson</div>
<div class="small text-muted">ID: T-1042</div>
</div>
</div>
</td>
<td>8:00 AM - 4:30 PM</td>
<td>Truck #118</td>
<td><span class="badge bg-success">Yes</span></td>
<td><span class="badge bg-success">Yes</span></td>
<td>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="warrant-partner2"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Require second person for warrant service"
/>
<label class="form-check-label" for="warrant-partner2"
>Warrant Partner</label
>
</div>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="resident-history2"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Route to previously visited residents"
/>
<label class="form-check-label" for="resident-history2"
>Prior Interactions</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="include-route2"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Include in route calculations"
/>
<label class="form-check-label" for="include-route2"
>Include in Routes</label
>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/men/44.jpg"
alt="Michael Chen"
class="tech-photo me-3"
/>
<div>
<div class="fw-bold">Michael Chen</div>
<div class="small text-muted">ID: T-1019</div>
</div>
</div>
</td>
<td>6:30 AM - 3:00 PM</td>
<td>Truck #107</td>
<td><span class="badge bg-danger">No</span></td>
<td><span class="badge bg-success">Yes</span></td>
<td>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="warrant-partner3"
disabled
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Require second person for warrant service"
/>
<label class="form-check-label" for="warrant-partner3"
>Warrant Partner</label
>
</div>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="resident-history3"
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Route to previously visited residents"
/>
<label class="form-check-label" for="resident-history3"
>Prior Interactions</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="include-route3"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Include in route calculations"
/>
<label class="form-check-label" for="include-route3"
>Include in Routes</label
>
</div>
</td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<img
src="https://randomuser.me/api/portraits/women/22.jpg"
alt="Jessica Martinez"
class="tech-photo me-3"
/>
<div>
<div class="fw-bold">Jessica Martinez</div>
<div class="small text-muted">ID: T-1055</div>
</div>
</div>
</td>
<td>7:30 AM - 4:00 PM</td>
<td>Truck #112</td>
<td><span class="badge bg-success">Yes</span></td>
<td><span class="badge bg-success">Yes</span></td>
<td>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="warrant-partner4"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Require second person for warrant service"
/>
<label class="form-check-label" for="warrant-partner4"
>Warrant Partner</label
>
</div>
<div class="form-check mb-2">
<input
class="form-check-input"
type="checkbox"
id="resident-history4"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Route to previously visited residents"
/>
<label class="form-check-label" for="resident-history4"
>Prior Interactions</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="include-route4"
checked
data-bs-toggle="tooltip"
data-bs-placement="left"
title="Include in route calculations"
/>
<label class="form-check-label" for="include-route4"
>Include in Routes</label
>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="d-grid gap-2 col-md-6 mx-auto">
<a
href="{{ .URLs.DispatchResults }}"
class="btn btn-primary btn-lg action-button"
type="button"
>
<i class="bi bi-calculator me-2"></i>Calculate Routes
</a>
</div>
</div>
{{ end }}

View file

@ -1,14 +1,17 @@
{{template "base.html" .}} {{ template "sync/base.html" . }}
{{define "title"}}Dash{{end}} {{ define "title" }}Dash{{ end }}
{{define "extraheader"}} {{ define "extraheader" }}
<script src='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js'></script> <script src="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.js"></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css' rel='stylesheet' /> <link
<script src="/static/js/address-display.js"></script> href="https://api.mapbox.com/mapbox-gl-js/v3.17.0-beta.1/mapbox-gl.css"
<script src="/static/js/address-suggestion.js"></script> rel="stylesheet"
<script src="/static/js/geocode.js"></script> />
<script src="/static/js/location.js"></script> <script src="/static/js/address-display.js"></script>
<script src="/static/js/map.js"></script> <script src="/static/js/address-suggestion.js"></script>
<script src="/static/js/geocode.js"></script>
<script src="/static/js/location.js"></script>
<script src="/static/js/map.js"></script>
<script> <script>
const MAPBOX_ACCESS_TOKEN = '{{.MapboxToken}}'; const MAPBOX_ACCESS_TOKEN = '{{.MapboxToken}}';
function setDistrictColors(map) { function setDistrictColors(map) {
@ -123,7 +126,7 @@ function onLoad() {
}); });
console.log("Map post-load done."); console.log("Map post-load done.");
}); });
map.addControl(new mapboxgl.NavigationControl()); map.addControl(new mapboxgl.NavigationControl());
console.log("Map init done."); console.log("Map init done.");
@ -153,132 +156,132 @@ function onLoad() {
} }
document.addEventListener("DOMContentLoaded", onLoad); document.addEventListener("DOMContentLoaded", onLoad);
</script> </script>
<style> <style>
body { body {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
.detail-label { .detail-label {
font-size: 0.8rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
color: #6c757d; color: #6c757d;
margin-bottom: 2px; margin-bottom: 2px;
font-weight: 600; font-weight: 600;
} }
.dashboard-container { .dashboard-container {
padding: 20px 0; padding: 20px 0;
} }
.stats-card { .stats-card {
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition: transform 0.2s; transition: transform 0.2s;
height: 100%; height: 100%;
} }
.stats-card:hover { .stats-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
} }
.map-container { .map-container {
background-color: #e9ecef; background-color: #e9ecef;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
height: 500px; height: 500px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 20px; margin-top: 20px;
} }
#map { #map {
height: 500px; height: 500px;
width:100%; width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
} }
#map img { #map img {
max-width: none; max-width: none;
min-width: 0px; min-width: 0px;
height: auto; height: auto;
} }
.section-title { .section-title {
margin: 30px 0 20px; margin: 30px 0 20px;
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid #dee2e6; border-bottom: 1px solid #dee2e6;
} }
.last-refreshed { .last-refreshed {
color: #6c757d; color: #6c757d;
} }
.logo-placeholder { .logo-placeholder {
width: 100px; width: 100px;
height: 40px; height: 40px;
background-color: #e9ecef; background-color: #e9ecef;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 4px; border-radius: 4px;
} }
.metric-icon { .metric-icon {
font-size: 2rem; font-size: 2rem;
margin-bottom: 10px; margin-bottom: 10px;
display: inline-block; display: inline-block;
width: 50px; width: 50px;
height: 50px; height: 50px;
line-height: 50px; line-height: 50px;
text-align: center; text-align: center;
border-radius: 50%; border-radius: 50%;
} }
.metric-value { .metric-value {
font-size: 2rem; font-size: 2rem;
font-weight: bold; font-weight: bold;
} }
.syncing { .syncing {
color: #28a745; color: #28a745;
animation: fa-spin 2s linear infinite; animation: fa-spin 2s linear infinite;
} }
</style> </style>
{{end}} {{ end }}
{{define "content"}} {{ define "content" }}
<div class="container my-4"> <div class="container my-4">
<div class="card-body p-0"> <div class="card-body p-0">
<div class="mb-3 position-relative"> <div class="mb-3 position-relative">
<address-input <address-input
placeholder="Start typing an address (min 3 characters)" placeholder="Start typing an address (min 3 characters)"
api-key="{{ .MapboxToken }}"> api-key="{{ .MapboxToken }}"
</address-input> >
</address-input>
</div>
<div class="mt-3"></div>
</div> </div>
<div class="mt-3"> <div class="card-body p-0">
</div> <div class="map-container" id="map-container">
</div> <div id="map"></div>
<div class="card-body p-0">
<div class="map-container" id="map-container">
<div id="map"></div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="card-body p-0">
<address-display></address-display>
</div> </div>
</div> </div>
<div class="col-6"> <div class="row">
<div class="district-display" class="mt-4 "> <div class="col-6">
<h5 class="mb-3">District Details</h5> <div class="card-body p-0">
<div class="location-card p-3 mb-3"> <address-display></address-display>
<div class="district-detail"> </div>
<div class="detail-label">Agency</div> </div>
<div class="detail-value" id="district-agency">-</div> <div class="col-6">
</div> <div class="district-display" class="mt-4 ">
<div class="district-detail"> <h5 class="mb-3">District Details</h5>
<div class="detail-label">Manager</div> <div class="location-card p-3 mb-3">
<div class="agency detail-value" id="district-manager">-</div> <div class="district-detail">
</div> <div class="detail-label">Agency</div>
<div class="district-detail"> <div class="detail-value" id="district-agency">-</div>
<div class="detail-label">Phone</div> </div>
<div class="agency detail-value" id="district-phone">-</div> <div class="district-detail">
</div> <div class="detail-label">Manager</div>
<div class="district-detail"> <div class="agency detail-value" id="district-manager">-</div>
<div class="detail-label">Website</div> </div>
<div class="agency detail-value" id="district-website">-</div> <div class="district-detail">
<div class="detail-label">Phone</div>
<div class="agency detail-value" id="district-phone">-</div>
</div>
<div class="district-detail">
<div class="detail-label">Website</div>
<div class="agency detail-value" id="district-website">-</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> {{ end }}
{{end}}

View file

@ -0,0 +1,7 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
{{ end }}

View file

@ -0,0 +1,7 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
{{ end }}

View file

@ -0,0 +1,77 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Layout Test{{ end }}
{{ define "extraheader" }}
<style>
.bd-example {
padding: 1.5rem;
margin-right: 0;
margin-left: 0;
border-width: 1px;
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
}
</style>
{{ end }}
{{ define "content" }}
<!-- Main Content -->
<div id="content">
<div class="container-fluid">
<div class="row">
<div class="bd-example">
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-secondary">Secondary</button>
<button type="button" class="btn btn-success">Success</button>
<button type="button" class="btn btn-danger">Danger</button>
<button type="button" class="btn btn-warning">Warning</button>
<button type="button" class="btn btn-info">Info</button>
<button type="button" class="btn btn-light">Light</button>
<button type="button" class="btn btn-dark">Dark</button>
<button type="button" class="btn btn-link">Link</button>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Card 1</h5>
<p class="card-text">Some example content for the first card.</p>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">Card 2</h5>
<p class="card-text">Some example content for the second card.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="p-3 mb-3 bg-primary text-white">Primary</div>
<div class="p-3 mb-3 bg-primary-100 text-white">Primary-100</div>
<div class="p-3 mb-3 bg-primary-200 text-white">Primary-200</div>
<div class="p-3 mb-3 bg-primary-300 text-white">Primary-300</div>
</div>
<div class="col-md-4">
<div class="p-3 mb-3 bg-secondary text-white">Secondary</div>
</div>
<div class="col-md-4">
<div class="p-3 mb-3 bg-success text-white">Success</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="p-3 mb-3 bg-danger text-white">Danger</div>
</div>
<div class="col-md-4">
<div class="p-3 mb-3 bg-warning text-white">Warning</div>
</div>
<div class="col-md-4">
<div class="p-3 mb-3 bg-info text-white">Info</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,66 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Data Entry{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
<div class="container">
<h1>Mock Listing</h1>
<table class="table">
<thead>
<tr>
<th scope="col" style="min-width: 20vw">Link</th>
<th scope="col">Name</th>
<th scope="col">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/mock/admin">/mock/admin</a></td>
<td>Admin</td>
<td>
Used by office admins to handle phone calls and other public-facing
responsibilities
</td>
</tr>
<tr>
<td><a href="/mock/data-entry">/mock/flyover-data-entry</a></td>
<td>Flyover Data Entry</td>
<td>
Used to upload CSV files with information about problematic pools
</td>
</tr>
<tr>
<td><a href="/mock/dispatch">/mock/dispatch</a></td>
<td>Dispatch</td>
<td>Used each day to calculate the working routes for technicians</td>
</tr>
<tr>
<td><a href="/mock/report">/mock/report</a></td>
<td>Reporting Overview</td>
<td>
Shows examples of text message contents, printable QR codes, and
email bodies for sending to the public to help them self-report
</td>
</tr>
<tr>
<td><a href="/mock/report/abc-123">/mock/report/abc-123</a></td>
<td>Self-Report</td>
<td>A page for members of the public to report a green pool.</td>
</tr>
<tr>
<td><a href="/mock/service-request">/mock/service-request</a></td>
<td>Service Request</td>
<td>
A page for members of the public to make a direct service request
</td>
</tr>
<tr>
<td><a href="/mock/setting">/mock/setting</a></td>
<td>Settings</td>
<td>A page for management to control the behavior of Nidus</td>
</tr>
</tbody>
</table>
</div>
{{ end }}

View file

@ -0,0 +1,115 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.connect-container {
max-width: 800px;
margin: 0 auto;
}
.connect-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 40px;
background-color: #fff;
}
.connect-header {
margin-bottom: 25px;
text-align: center;
}
.steps-container {
margin: 30px 0;
}
.step {
margin-bottom: 20px;
padding: 15px;
border-left: 3px solid #0d6efd;
background-color: #f8f9fa;
}
.connect-btn {
margin-top: 30px;
}
</style>
{{ end }}
{{ define "content" }}
<div
class="container min-vh-100 d-flex align-items-center justify-content-center py-5"
>
<div class="connect-container">
<div class="connect-box">
<div class="connect-header">
<h1>Connect Your ArcGIS Account</h1>
<p class="text-muted">Link your data to get started</p>
</div>
<div class="connect-content">
<p>
To provide you with the best experience, we need to connect to your
ArcGIS account. This allows us to securely access and visualize your
spatial data within our platform.
</p>
<div class="steps-container">
<h4>What to expect:</h4>
<div class="step">
<h5>1. Secure Authentication</h5>
<p>
When you click the "Connect to ArcGIS" button below, you'll be
redirected to the official ArcGIS login page. This connection is
secure and uses OAuth 2.0 protocol.
</p>
</div>
<div class="step">
<h5>2. Grant Permissions</h5>
<p>
After logging in with your ArcGIS credentials, you'll be asked
to approve permissions for our application to access your data.
We only request access to what's needed for the platform to
function.
</p>
</div>
<div class="step">
<h5>3. Return to Platform</h5>
<p>
Once authentication is complete, you'll be automatically
redirected back to our platform where your data will be
available to work with.
</p>
</div>
</div>
<div class="alert alert-info">
<strong>Note:</strong> You'll need an active ArcGIS Online account
or ArcGIS Enterprise account to proceed. If you don't have one, you
can
<a href="https://www.arcgis.com/home/signin.html" target="_blank"
>create an ArcGIS account here</a
>.
</div>
<p>By connecting your ArcGIS account, you'll be able to:</p>
<ul>
<li>Access and visualize your spatial data</li>
<li>Perform advanced analysis using our integrated tools</li>
<li>Share results with team members securely</li>
<li>Keep your data synchronized across platforms</li>
</ul>
<div class="text-center connect-btn">
<a href="/arcgis/oauth/begin" class="btn btn-primary btn-lg">
Connect to ArcGIS
</a>
<p class="mt-2 text-muted">
<small
>You can disconnect your account at any time in settings</small
>
</p>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,561 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Privacy Policy{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
<div class="container">
<h1>Privacy Policy</h1>
<p>Last updated: January 20, 2026</p>
<p>
This Privacy Policy describes Our policies and procedures on the
collection, use and disclosure of Your information when You use the
Service and tells You about Your privacy rights and how the law protects
You.
</p>
<p>
We use Your Personal Data to provide and improve the Service. By using the
Service, You agree to the collection and use of information in accordance
with this Privacy Policy.
</p>
<h2>Interpretation and Definitions</h2>
<h3>Interpretation</h3>
<p>
The words whose initial letters are capitalized have meanings defined
under the following conditions. The following definitions shall have the
same meaning regardless of whether they appear in singular or in plural.
</p>
<h3>Definitions</h3>
<p>For the purposes of this Privacy Policy:</p>
<ul>
<li>
<p>
<strong>Account</strong> means a unique account created for You to
access our Service or parts of our Service.
</p>
</li>
<li>
<p>
<strong>Affiliate</strong> means an entity that controls, is
controlled by, or is under common control with a party, where
&quot;control&quot; means ownership of 50% or more of the shares,
equity interest or other securities entitled to vote for election of
directors or other managing authority.
</p>
</li>
<li>
<p>
<strong>Company</strong> (referred to as either &quot;the
Company&quot;, &quot;We&quot;, &quot;Us&quot; or &quot;Our&quot; in
this Privacy Policy) refers to {{ .Company }},
{{ .Address }}
</p>
</li>
<li>
<p>
<strong>Cookies</strong> are small files that are placed on Your
computer, mobile device or any other device by a website, containing
the details of Your browsing history on that website among its many
uses.
</p>
</li>
<li>
<p><strong>Country</strong> refers to: Arizona, United States</p>
</li>
<li>
<p>
<strong>Device</strong> means any device that can access the Service
such as a computer, a cell phone or a digital tablet.
</p>
</li>
<li>
<p>
<strong>Personal Data</strong> (or &quot;Personal Information&quot;)
is any information that relates to an identified or identifiable
individual.
</p>
<p>
We use &quot;Personal Data&quot; and &quot;Personal Information&quot;
interchangeably unless a law uses a specific term.
</p>
</li>
<li>
<p><strong>Service</strong> refers to the Website.</p>
</li>
<li>
<p>
<strong>Service Provider</strong> means any natural or legal person
who processes the data on behalf of the Company. It refers to
third-party companies or individuals employed by the Company to
facilitate the Service, to provide the Service on behalf of the
Company, to perform services related to the Service or to assist the
Company in analyzing how the Service is used.
</p>
</li>
<li>
<p>
<strong>Usage Data</strong> refers to data collected automatically,
either generated by the use of the Service or from the Service
infrastructure itself (for example, the duration of a page visit).
</p>
</li>
<li>
<p>
<strong>Website</strong> refers to {{ .Site }}, accessible from
<a
href="{{ .URLSync }}"
rel="external nofollow noopener"
target="_blank"
>{{ .URLSync }}</a
>.
</p>
</li>
<li>
<p>
<strong>You</strong> means the individual accessing or using the
Service, or the company, or other legal entity on behalf of which such
individual is accessing or using the Service, as applicable.
</p>
</li>
</ul>
<h2>Collecting and Using Your Personal Data</h2>
<h3>Types of Data Collected</h3>
<h4>Personal Data</h4>
<p>
While using Our Service, We may ask You to provide Us with certain
personally identifiable information that can be used to contact or
identify You. Personally identifiable information may include, but is not
limited to:
</p>
<ul>
<li>Email address</li>
<li>First name and last name</li>
<li>Phone number</li>
<li>Address, State, Province, ZIP/Postal code, City</li>
</ul>
<h4>Usage Data</h4>
<p>Usage Data is collected automatically when using the Service.</p>
<p>
Usage Data may include information such as Your Device's Internet Protocol
address (e.g. IP address), browser type, browser version, the pages of our
Service that You visit, the time and date of Your visit, the time spent on
those pages, unique device identifiers and other diagnostic data.
</p>
<p>
When You access the Service by or through a mobile device, We may collect
certain information automatically, including, but not limited to, the type
of mobile device You use, Your mobile device's unique ID, the IP address
of Your mobile device, Your mobile operating system, the type of mobile
Internet browser You use, unique device identifiers and other diagnostic
data.
</p>
<p>
We may also collect information that Your browser sends whenever You visit
Our Service or when You access the Service by or through a mobile device.
</p>
<h4>Tracking Technologies and Cookies</h4>
<p>
We use Cookies and similar tracking technologies to track the activity on
Our Service and store certain information. Tracking technologies We use
include beacons, tags, and scripts to collect and track information and to
improve and analyze Our Service. The technologies We use may include:
</p>
<ul>
<li>
<strong>Cookies or Browser Cookies.</strong> A cookie is a small file
placed on Your Device. You can instruct Your browser to refuse all
Cookies or to indicate when a Cookie is being sent. However, if You do
not accept Cookies, You may not be able to use some parts of our
Service.
</li>
</ul>
<p>
Cookies can be &quot;Persistent&quot; or &quot;Session&quot; Cookies.
Persistent Cookies remain on Your personal computer or mobile device when
You go offline, while Session Cookies are deleted as soon as You close
Your web browser.
</p>
<p>
Where required by law, we use non-essential cookies (such as analytics,
advertising, and remarketing cookies) only with Your consent. You can
withdraw or change Your consent at any time using Our cookie preferences
tool (if available) or through Your browser/device settings. Withdrawing
consent does not affect the lawfulness of processing based on consent
before its withdrawal.
</p>
<p>
We use both Session and Persistent Cookies for the purposes set out below:
</p>
<ul>
<li>
<p><strong>Necessary / Essential Cookies</strong></p>
<p>Type: Session Cookies</p>
<p>Administered by: Us</p>
<p>
Purpose: These Cookies are essential to provide You with services
available through the Website and to enable You to use some of its
features. They help to authenticate users and prevent fraudulent use
of user accounts. Without these Cookies, the services that You have
asked for cannot be provided, and We only use these Cookies to provide
You with those services.
</p>
</li>
<li>
<p><strong>Functionality Cookies</strong></p>
<p>Type: Persistent Cookies</p>
<p>Administered by: Us</p>
<p>
Purpose: These Cookies allow Us to remember choices You make when You
use the Website, such as remembering your login details or language
preference. The purpose of these Cookies is to provide You with a more
personal experience and to avoid You having to re-enter your
preferences every time You use the Website.
</p>
</li>
</ul>
<p>
For more information about the cookies we use and your choices regarding
cookies, please visit our Cookies Policy or the Cookies section of Our
Privacy Policy.
</p>
<h3>Use of Your Personal Data</h3>
<p>The Company may use Personal Data for the following purposes:</p>
<ul>
<li>
<p>
<strong>To provide and maintain our Service</strong>, including to
monitor the usage of our Service.
</p>
</li>
<li>
<p>
<strong>To manage Your Account:</strong> to manage Your registration
as a user of the Service. The Personal Data You provide can give You
access to different functionalities of the Service that are available
to You as a registered user.
</p>
</li>
<li>
<p>
<strong>For the performance of a contract:</strong> the development,
compliance and undertaking of the purchase contract for the products,
items or services You have purchased or of any other contract with Us
through the Service.
</p>
</li>
<li>
<p>
<strong>To contact You:</strong> To contact You by email, telephone
calls, SMS, or other equivalent forms of electronic communication,
such as a mobile application's push notifications regarding updates or
informative communications related to the functionalities, products or
contracted services, including the security updates, when necessary or
reasonable for their implementation.
</p>
</li>
<li>
<p>
<strong>To provide You</strong> with news, special offers, and general
information about other goods, services and events which We offer that
are similar to those that you have already purchased or inquired about
unless You have opted not to receive such information.
</p>
</li>
<li>
<p>
<strong>To manage Your requests:</strong> To attend and manage Your
requests to Us.
</p>
</li>
<li>
<p>
<strong>For business transfers:</strong> We may use Your Personal Data
to evaluate or conduct a merger, divestiture, restructuring,
reorganization, dissolution, or other sale or transfer of some or all
of Our assets, whether as a going concern or as part of bankruptcy,
liquidation, or similar proceeding, in which Personal Data held by Us
about our Service users is among the assets transferred.
</p>
</li>
<li>
<p>
<strong>For other purposes</strong>: We may use Your information for
other purposes, such as data analysis, identifying usage trends,
determining the effectiveness of our promotional campaigns and to
evaluate and improve our Service, products, services, marketing and
your experience.
</p>
</li>
</ul>
<p>We may share Your Personal Data in the following situations:</p>
<ul>
<li>
<strong>With Service Providers:</strong> We may share Your Personal Data
with Service Providers to monitor and analyze the use of our Service, to
contact You.
</li>
<li>
<strong>For business transfers:</strong> We may share or transfer Your
Personal Data in connection with, or during negotiations of, any merger,
sale of Company assets, financing, or acquisition of all or a portion of
Our business to another company.
</li>
<li>
<strong>With Affiliates:</strong> We may share Your Personal Data with
Our affiliates, in which case we will require those affiliates to honor
this Privacy Policy. Affiliates include Our parent company and any other
subsidiaries, joint venture partners or other companies that We control
or that are under common control with Us.
</li>
<li>
<strong>With business partners:</strong> We may share Your Personal Data
with Our business partners to offer You certain products, services or
promotions.
</li>
<li>
<strong>With other users:</strong> If Our Service offers public areas,
when You share Personal Data or otherwise interact in the public areas
with other users, such information may be viewed by all users and may be
publicly distributed outside.
</li>
<li>
<strong>With Your consent</strong>: We may disclose Your Personal Data
for any other purpose with Your consent.
</li>
</ul>
<h3>Retention of Your Personal Data</h3>
<p>
The Company will retain Your Personal Data only for as long as is
necessary for the purposes set out in this Privacy Policy. We will retain
and use Your Personal Data to the extent necessary to comply with our
legal obligations (for example, if We are required to retain Your data to
comply with applicable laws), resolve disputes, and enforce our legal
agreements and policies.
</p>
<p>
Where possible, We apply shorter retention periods and/or reduce
identifiability by deleting, aggregating, or anonymizing data. Unless
otherwise stated, the retention periods below are maximum periods
(&quot;up to&quot;) and We may delete or anonymize data sooner when it is
no longer needed for the relevant purpose. We apply different retention
periods to different categories of Personal Data based on the purpose of
processing and legal obligations:
</p>
<ul>
<li>
<p>Account Information</p>
<ul>
<li>
User Accounts: retained for the duration of your account
relationship plus up to 24 months after account closure to handle
any post-termination issues or resolve disputes.
</li>
</ul>
</li>
<li>
<p>Customer Support Data</p>
<ul>
<li>
Support tickets and correspondence: up to 24 months from the date of
ticket closure to resolve follow-up inquiries, track service
quality, and defend against potential legal claims
</li>
<li>
Chat transcripts: up to 24 months for quality assurance and staff
training purposes.
</li>
</ul>
</li>
<li>
<p>Usage Data</p>
<ul>
<li>
<p>
Website analytics data (cookies, IP addresses, device
identifiers): up to 24 months from the date of collection, which
allows us to analyze trends while respecting privacy principles.
</p>
</li>
<li>
<p>
Server logs (IP addresses, access times): up to 24 months for
security monitoring and troubleshooting purposes.
</p>
</li>
</ul>
</li>
</ul>
<p>
Usage Data is retained in accordance with the retention periods described
above, and may be retained longer only where necessary for security, fraud
prevention, or legal compliance.
</p>
<p>
We may retain Personal Data beyond the periods stated above for different
reasons:
</p>
<ul>
<li>
Legal obligation: We are required by law to retain specific data (e.g.,
financial records for tax authorities).
</li>
<li>
Legal claims: Data is necessary to establish, exercise, or defend legal
claims.
</li>
<li>Your explicit request: You ask Us to retain specific information.</li>
<li>
Technical limitations: Data exists in backup systems that are scheduled
for routine deletion.
</li>
</ul>
<p>
You may request information about how long We will retain Your Personal
Data by contacting Us.
</p>
<p>
When retention periods expire, We securely delete or anonymize Personal
Data according to the following procedures:
</p>
<ul>
<li>
Deletion: Personal Data is removed from Our systems and no longer
actively processed.
</li>
<li>
Backup retention: Residual copies may remain in encrypted backups for a
limited period consistent with our backup retention schedule and are not
restored except where necessary for security, disaster recovery, or
legal compliance.
</li>
<li>
Anonymization: In some cases, We convert Personal Data into anonymous
statistical data that cannot be linked back to You. This anonymized data
may be retained indefinitely for research and analytics.
</li>
</ul>
<h3>Transfer of Your Personal Data</h3>
<p>
Your information, including Personal Data, is processed at the Company's
operating offices and in any other places where the parties involved in
the processing are located. It means that this information may be
transferred to — and maintained on — computers located outside of Your
state, province, country or other governmental jurisdiction where the data
protection laws may differ from those from Your jurisdiction.
</p>
<p>
Where required by applicable law, We will ensure that international
transfers of Your Personal Data are subject to appropriate safeguards and
supplementary measures where appropriate. The Company will take all steps
reasonably necessary to ensure that Your data is treated securely and in
accordance with this Privacy Policy and no transfer of Your Personal Data
will take place to an organization or a country unless there are adequate
controls in place including the security of Your data and other personal
information.
</p>
<h3>Delete Your Personal Data</h3>
<p>
You have the right to delete or request that We assist in deleting the
Personal Data that We have collected about You.
</p>
<p>
Our Service may give You the ability to delete certain information about
You from within the Service.
</p>
<p>
You may update, amend, or delete Your information at any time by signing
in to Your Account, if you have one, and visiting the account settings
section that allows you to manage Your personal information. You may also
contact Us to request access to, correct, or delete any Personal Data that
You have provided to Us.
</p>
<p>
Please note, however, that We may need to retain certain information when
we have a legal obligation or lawful basis to do so.
</p>
<h3>Disclosure of Your Personal Data</h3>
<h4>Business Transactions</h4>
<p>
If the Company is involved in a merger, acquisition or asset sale, Your
Personal Data may be transferred. We will provide notice before Your
Personal Data is transferred and becomes subject to a different Privacy
Policy.
</p>
<h4>Law enforcement</h4>
<p>
Under certain circumstances, the Company may be required to disclose Your
Personal Data if required to do so by law or in response to valid requests
by public authorities (e.g. a court or a government agency).
</p>
<h4>Other legal requirements</h4>
<p>
The Company may disclose Your Personal Data in the good faith belief that
such action is necessary to:
</p>
<ul>
<li>Comply with a legal obligation</li>
<li>Protect and defend the rights or property of the Company</li>
<li>
Prevent or investigate possible wrongdoing in connection with the
Service
</li>
<li>Protect the personal safety of Users of the Service or the public</li>
<li>Protect against legal liability</li>
</ul>
<h3>Security of Your Personal Data</h3>
<p>
The security of Your Personal Data is important to Us, but remember that
no method of transmission over the Internet, or method of electronic
storage is 100% secure. While We strive to use commercially reasonable
means to protect Your Personal Data, We cannot guarantee its absolute
security.
</p>
<h2>Children's Privacy</h2>
<p>
Our Service does not address anyone under the age of 16. We do not
knowingly collect personally identifiable information from anyone under
the age of 16. If You are a parent or guardian and You are aware that Your
child has provided Us with Personal Data, please contact Us. If We become
aware that We have collected Personal Data from anyone under the age of 16
without verification of parental consent, We take steps to remove that
information from Our servers.
</p>
<p>
If We need to rely on consent as a legal basis for processing Your
information and Your country requires consent from a parent, We may
require Your parent's consent before We collect and use that information.
</p>
<h2>Links to Other Websites</h2>
<p>
Our Service may contain links to other websites that are not operated by
Us. If You click on a third party link, You will be directed to that third
party's site. We strongly advise You to review the Privacy Policy of every
site You visit.
</p>
<p>
We have no control over and assume no responsibility for the content,
privacy policies or practices of any third party sites or services.
</p>
<h2>Changes to this Privacy Policy</h2>
<p>
We may update Our Privacy Policy from time to time. We will notify You of
any changes by posting the new Privacy Policy on this page.
</p>
<p>
We will let You know via email and/or a prominent notice on Our Service,
prior to the change becoming effective and update the &quot;Last
updated&quot; date at the top of this Privacy Policy.
</p>
<p>
You are advised to review this Privacy Policy periodically for any
changes. Changes to this Privacy Policy are effective when they are posted
on this page.
</p>
<h2>Contact Us</h2>
<p>
If you have any questions about this Privacy Policy, You can contact us:
</p>
<ul>
<li>By email: privacy@gleipnir.technology</li>
</ul>
</div>
{{ end }}

View file

@ -0,0 +1,250 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.success-icon {
font-size: 70px;
color: #198754;
margin-bottom: 20px;
display: block;
text-align: center;
}
.confirmation-title {
text-align: center;
margin-bottom: 30px;
}
.appointment-summary {
background-color: #e8f4f8;
border-left: 4px solid #0d6efd;
padding: 20px;
border-radius: 8px;
margin-bottom: 25px;
}
.appointment-details {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 15px;
}
.appointment-detail {
flex: 1;
min-width: 150px;
padding: 10px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.detail-label {
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
font-weight: 600;
margin-bottom: 5px;
}
.detail-value {
font-weight: 600;
}
.contact-info {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-top: 25px;
margin-bottom: 25px;
}
.contact-method {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.contact-method i {
font-size: 1.2rem;
margin-right: 10px;
color: #0d6efd;
width: 25px;
text-align: center;
}
.tracking-link {
background-color: #e9f7ef;
border: 1px solid #d1e7dd;
border-left: 4px solid #198754;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
text-decoration: none;
color: inherit;
transition: all 0.2s;
}
.tracking-link:hover {
background-color: #d1e7dd;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.confirmation-number {
background-color: #f8f9fa;
padding: 10px 15px;
border-radius: 6px;
font-family: monospace;
font-size: 1.2rem;
font-weight: 600;
letter-spacing: 1px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Main Content -->
<div class="content-card">
<span class="success-icon">
<i class="bi bi-check-circle-fill"></i>
</span>
<div class="confirmation-title">
<h1 class="h3">Thank You for Your Submission!</h1>
<p class="text-muted">
Your green pool report has been successfully submitted.
</p>
</div>
<!-- Appointment Summary -->
<div class="appointment-summary">
<h5><i class="bi bi-calendar-check me-2"></i>Appointment Confirmed</h5>
<p>Our inspector will visit your property at the scheduled time:</p>
<div class="appointment-details">
<div class="appointment-detail">
<div class="detail-label">Date</div>
<div class="detail-value">Thursday, June 22, 2023</div>
</div>
<div class="appointment-detail">
<div class="detail-label">Time</div>
<div class="detail-value">10:00 AM</div>
</div>
<div class="appointment-detail">
<div class="detail-label">Confirmation #</div>
<div class="detail-value">GP-23685</div>
</div>
</div>
</div>
<!-- What's Next Section -->
<div>
<h5 class="mb-3">What Happens Next?</h5>
<ul class="mb-4">
<li>
A confirmation email has been sent to the email address you
provided.
</li>
<li>
You'll receive a reminder notification 24 hours before your
scheduled appointment.
</li>
<li>
Our team will review your report and contact you by the next
business day if any additional information is needed.
</li>
<li>
During the scheduled visit, our inspector will assess the pool
condition and discuss treatment options if necessary.
</li>
</ul>
<p>
You can use the link below to track your report status and view the
photos you've submitted.
</p>
</div>
<!-- Tracking Link -->
<a href="#" class="tracking-link">
<div>
<strong>Track Your Report Status</strong>
<p class="mb-0 text-muted">View photos and check for updates</p>
</div>
<i class="bi bi-arrow-right"></i>
</a>
<!-- Contact Information -->
<div class="contact-info">
<h5 class="mb-3">Questions or Concerns?</h5>
<p>
If you have any questions about your report or need to change your
appointment, please contact us:
</p>
<div class="contact-method">
<i class="bi bi-telephone-fill"></i>
<div>
<strong>(555) 123-4567</strong>
<div class="small text-muted">Monday-Friday, 8:00 AM - 5:00 PM</div>
</div>
</div>
<div class="contact-method">
<i class="bi bi-envelope-fill"></i>
<div>
<strong>greenpool@vectorcontrol.county.gov</strong>
</div>
</div>
<p class="mt-3 mb-0 small">
Please include your confirmation number (GP-23685) in all
correspondence.
</p>
</div>
<!-- Print Option -->
<div class="d-flex justify-content-center mt-4">
<button
type="button"
class="btn btn-outline-secondary"
onclick="window.print()"
>
<i class="bi bi-printer me-2"></i> Print Confirmation
</button>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
Thank you for helping keep our community safe from mosquito-borne
diseases.
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,295 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.progress-container {
margin: 30px 0 20px;
}
.progress {
height: 8px;
}
.upload-area {
border: 2px dashed #dee2e6;
border-radius: 10px;
padding: 30px;
text-align: center;
margin-bottom: 25px;
background-color: #f8f9fa;
transition: all 0.3s ease;
}
.upload-area:hover {
border-color: #0d6efd;
background-color: #f0f7ff;
}
.upload-icon {
font-size: 48px;
color: #6c757d;
margin-bottom: 15px;
}
.photo-example {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.example-item {
flex: 1;
min-width: 200px;
text-align: center;
}
.example-img {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 10px;
border: 2px solid #dee2e6;
}
.good-example {
border-color: #198754;
}
.poor-example {
border-color: #dc3545;
}
.upload-options {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
}
.upload-options button {
flex: 1;
min-width: 150px;
}
.uploaded-photos {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 20px;
}
.uploaded-photo {
position: relative;
height: 150px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #dee2e6;
}
.uploaded-photo img {
width: 100%;
height: 100%;
object-fit: cover;
}
.remove-photo {
position: absolute;
top: 5px;
right: 5px;
background-color: rgba(255, 255, 255, 0.8);
color: #dc3545;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.tips-section {
background-color: #e8f4f8;
border-left: 4px solid #0d6efd;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">Upload Photos</span>
<span class="text-muted small">3 of 4</span>
</div>
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 75%"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<!-- Main Content -->
<div class="content-card">
<h1 class="h4 mb-3">Upload Current Pool Photos</h1>
<p>
Please provide current photos of your pool to help us assess its
condition.
</p>
<!-- Photo Tips Section -->
<div class="tips-section">
<h5><i class="bi bi-lightbulb me-2"></i>Photo Tips</h5>
<ul class="mb-0">
<li>
<strong>Take photos from as high an angle as possible</strong>
(second story window, deck, etc.)
</li>
<li>Try to capture the <strong>entire pool</strong> in your photo</li>
<li>Ensure photos are <strong>clear and well-lit</strong></li>
<li>
You can add <strong>multiple photos</strong> from different angles
</li>
</ul>
</div>
<!-- Photo Examples -->
<h5 class="mb-3">Photo Examples:</h5>
<div class="photo-example">
<div class="example-item">
<img
src="https://placehold.co/400x300/198754/ffffff?text=Good+Example"
alt="Good photo example"
class="example-img good-example"
/>
<div class="text-success">
<i class="bi bi-check-circle me-1"></i>Good: High angle, full view
</div>
</div>
<div class="example-item">
<img
src="https://placehold.co/400x300/dc3545/ffffff?text=Poor+Example"
alt="Poor photo example"
class="example-img poor-example"
/>
<div class="text-danger">
<i class="bi bi-x-circle me-1"></i>Poor: Ground level, partial view
</div>
</div>
</div>
<!-- Upload Area -->
<div class="upload-area">
<div class="upload-icon">
<i class="bi bi-camera"></i>
</div>
<h5>Add Pool Photos</h5>
<p class="text-muted">Take a new photo or upload from your device</p>
<div class="upload-options">
<button type="button" class="btn btn-primary">
<i class="bi bi-camera-fill me-2"></i> Take Photo
</button>
<button type="button" class="btn btn-outline-primary">
<i class="bi bi-image me-2"></i> Upload from Device
</button>
</div>
<input
type="file"
id="photo-upload"
accept="image/*"
multiple
style="display: none;"
/>
</div>
<!-- Already Uploaded Photos Section -->
<div>
<h5 class="mb-3">Uploaded Photos (2)</h5>
<div class="uploaded-photos">
<div class="uploaded-photo">
<img
src="https://placehold.co/300x300/76b5c5/ffffff?text=Pool+Photo+1"
alt="Uploaded pool photo"
/>
<button
type="button"
class="remove-photo"
aria-label="Remove photo"
>
<i class="bi bi-x"></i>
</button>
</div>
<div class="uploaded-photo">
<img
src="https://placehold.co/300x300/3e8e7e/ffffff?text=Pool+Photo+2"
alt="Uploaded pool photo"
/>
<button
type="button"
class="remove-photo"
aria-label="Remove photo"
>
<i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
<!-- Information -->
<div class="alert alert-secondary mt-4">
<small
>You can add up to 5 photos to provide a complete view of your pool
area. We recommend taking photos from multiple angles.</small
>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between mt-4">
<a href="{{ .URLs.ReportEvidence }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-2"></i> Back
</a>
<a href="{{ .URLs.ReportSchedule }}" class="btn btn-primary">
Next Step <i class="bi bi-arrow-right ms-2"></i>
</a>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
If you need assistance, please contact Vector Control at (555) 123-4567
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,151 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.map-container {
height: 300px;
background-color: #e9ecef;
border-radius: 10px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.map-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-image: url("https://placehold.co/600x300/e9ecef/adb5bd?text=Map+View");
background-size: cover;
background-position: center;
}
.map-pin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%);
color: #dc3545;
font-size: 30px;
}
.address-container {
background-color: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
border-left: 4px solid #0d6efd;
}
.action-buttons {
display: flex;
gap: 10px;
}
.progress-container {
margin: 30px 0 20px;
}
.progress {
height: 8px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">Location</span>
<span class="text-muted small">1 of 4</span>
</div>
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 25%"
aria-valuenow="25"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<!-- Main Content -->
<div class="content-card">
<h1 class="h4 mb-4">Confirm Property Location</h1>
<!-- Map View -->
<div class="map-container">
<div class="map-placeholder"></div>
<div class="map-pin">
<i class="bi bi-geo-alt-fill"></i>
</div>
</div>
<!-- Address Information -->
<div class="address-container">
<div class="mb-2"><strong>Detected Address:</strong></div>
<h5>123 Maple Street, Riverside, CA 92501</h5>
</div>
<div class="mb-4">
<p>Is this the correct location of the property in question?</p>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a
href="{{ .URLs.ReportEvidence }}"
class="btn btn-success flex-grow-1"
>
<i class="bi bi-check-circle me-2"></i> Correct
</a>
<a
href="{{ .URLs.ReportUpdate }}"
class="btn btn-outline-primary flex-grow-1"
>
<i class="bi bi-geo-alt me-2"></i> Update Location
</a>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
If you need assistance, please contact Vector Control at (555) 123-4567
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,281 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.photo-gallery {
display: flex;
gap: 10px;
margin-bottom: 20px;
overflow-x: auto;
padding-bottom: 10px;
}
.photo-item {
min-width: 200px;
height: 150px;
border-radius: 8px;
overflow: hidden;
border: 2px solid #dee2e6;
}
.photo-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.data-section {
margin-bottom: 25px;
}
.section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 10px;
color: #0d6efd;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 8px;
}
.table-container {
overflow-x: auto;
margin-bottom: 15px;
}
.alert-info {
background-color: #e8f4f8;
border-color: #b8daff;
}
.progress-container {
margin: 30px 0 20px;
}
.progress {
height: 8px;
}
.trap-high {
color: #dc3545;
font-weight: bold;
}
.trap-medium {
color: #fd7e14;
font-weight: bold;
}
.trap-low {
color: #198754;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">Evidence</span>
<span class="text-muted small">2 of 4</span>
</div>
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 50%"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<!-- Main Content -->
<div class="content-card">
<h1 class="h4 mb-4">Evidence of Potential Breeding Site</h1>
<!-- Aerial Photos -->
<div class="data-section">
<div class="section-title">
<i class="bi bi-camera-fill"></i> Aerial Surveillance Photos
</div>
<p class="small text-muted mb-2">
These photos were taken during routine aerial surveillance of the
area.
</p>
<div class="photo-gallery">
<div class="photo-item">
<img
src="https://placehold.co/200x150/76b5c5/ffffff?text=Aerial+Photo+1"
alt="Aerial photo of property"
/>
</div>
<div class="photo-item">
<img
src="https://placehold.co/200x150/3e8e7e/ffffff?text=Aerial+Photo+2"
alt="Aerial photo of property"
/>
</div>
<div class="photo-item">
<img
src="https://placehold.co/200x150/7accc8/ffffff?text=Aerial+Photo+3"
alt="Aerial photo of property"
/>
</div>
</div>
</div>
<!-- Historical Inspections -->
<div class="data-section">
<div class="section-title">
<i class="bi bi-clipboard-check-fill"></i> Historical Inspection Data
</div>
<div class="table-container">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Date</th>
<th>Inspector</th>
<th>Findings</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mar 15, 2023</td>
<td>J. Martinez</td>
<td>Pool water stagnant, green</td>
<td>Treatment applied, owner notified</td>
</tr>
<tr>
<td>Nov 02, 2022</td>
<td>L. Johnson</td>
<td>Pool water clear, maintained</td>
<td>No action needed</td>
</tr>
<tr>
<td>Aug 18, 2022</td>
<td>S. Williams</td>
<td>Minor algae formation</td>
<td>Owner provided maintenance resources</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Mosquito Trap Data -->
<div class="data-section">
<div class="section-title">
<i class="bi bi-bug-fill"></i> Mosquito Trap Count Data
</div>
<div class="table-container">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Date Collected</th>
<th>Count</th>
<th>Distance</th>
<th>Level</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jun 12, 2023</td>
<td>42</td>
<td>0.3 miles</td>
<td class="trap-high">High</td>
</tr>
<tr>
<td>Jun 05, 2023</td>
<td>36</td>
<td>0.3 miles</td>
<td class="trap-high">High</td>
</tr>
<tr>
<td>May 29, 2023</td>
<td>28</td>
<td>0.3 miles</td>
<td class="trap-medium">Medium</td>
</tr>
<tr>
<td>May 22, 2023</td>
<td>15</td>
<td>0.3 miles</td>
<td class="trap-low">Low</td>
</tr>
<tr>
<td>May 15, 2023</td>
<td>12</td>
<td>0.3 miles</td>
<td class="trap-low">Low</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Public Health Information -->
<div class="alert alert-info">
<h5><i class="bi bi-info-circle me-2"></i>Why This Matters</h5>
<p>
The data above shows mosquito activity in your area. Recent trap
counts indicate elevated mosquito populations, which increases the
risk of mosquito-borne diseases like West Nile virus.
</p>
<p>
Unmaintained swimming pools can produce thousands of mosquitoes each
week. By addressing potential breeding sites, you're helping protect
your family and neighbors from these health risks.
</p>
<p class="mb-0">
<strong>We need your help</strong> to ensure we maintain public health
by keeping mosquito counts low in your neighborhood. Your cooperation
makes a significant difference in community safety.
</p>
</div>
<!-- Action Button -->
<div class="d-grid gap-2 mt-4">
<a href="{{ .URLs.ReportContribute }}" class="btn btn-primary btn-lg">
<i class="bi bi-arrow-right-circle me-2"></i> Next Step: Upload
Current Photos
</a>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
If you need assistance, please contact Vector Control at (555) 123-4567
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,344 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.progress-container {
margin: 30px 0 20px;
}
.progress {
height: 8px;
}
.calendar-section {
margin-bottom: 30px;
}
.dates-container {
display: flex;
overflow-x: auto;
gap: 10px;
padding-bottom: 10px;
margin-bottom: 20px;
}
.date-card {
min-width: 110px;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.date-card:hover {
border-color: #0d6efd;
background-color: #f8f9fa;
}
.date-card.selected {
border-color: #0d6efd;
background-color: #e6f2ff;
box-shadow: 0 0 0 1px #0d6efd;
}
.date-card .month {
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
font-weight: 600;
}
.date-card .day {
font-size: 1.5rem;
font-weight: 700;
line-height: 1.2;
}
.date-card .weekday {
font-size: 0.8rem;
color: #6c757d;
}
.time-slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.time-slot {
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 10px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.time-slot:hover {
border-color: #0d6efd;
background-color: #f8f9fa;
}
.time-slot.selected {
border-color: #0d6efd;
background-color: #e6f2ff;
box-shadow: 0 0 0 1px #0d6efd;
}
.time-slot.unavailable {
background-color: #f8f9fa;
color: #adb5bd;
cursor: not-allowed;
text-decoration: line-through;
}
.form-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
}
.section-title {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 10px;
color: #0d6efd;
}
.selected-date-time {
background-color: #e8f4f8;
border-left: 4px solid #0d6efd;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Progress Bar -->
<div class="progress-container">
<div class="d-flex justify-content-between mb-1">
<span class="text-muted small">Schedule Follow-up</span>
<span class="text-muted small">4 of 4</span>
</div>
<div class="progress">
<div
class="progress-bar"
role="progressbar"
style="width: 100%"
aria-valuenow="100"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<!-- Main Content -->
<div class="content-card">
<h1 class="h4 mb-3">Schedule a Follow-up Inspection</h1>
<p>
Please select a convenient date and time for our inspector to visit your
property.
</p>
<!-- Calendar Section -->
<div class="calendar-section">
<div class="section-title">
<i class="bi bi-calendar-date"></i>
<h5 class="mb-0">Select Date</h5>
</div>
<!-- Date Selection -->
<div class="dates-container">
<div class="date-card">
<div class="month">Jun</div>
<div class="day">20</div>
<div class="weekday">Tue</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">21</div>
<div class="weekday">Wed</div>
</div>
<div class="date-card selected">
<div class="month">Jun</div>
<div class="day">22</div>
<div class="weekday">Thu</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">23</div>
<div class="weekday">Fri</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">26</div>
<div class="weekday">Mon</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">27</div>
<div class="weekday">Tue</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">28</div>
<div class="weekday">Wed</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">29</div>
<div class="weekday">Thu</div>
</div>
<div class="date-card">
<div class="month">Jun</div>
<div class="day">30</div>
<div class="weekday">Fri</div>
</div>
<div class="date-card">
<div class="month">Jul</div>
<div class="day">03</div>
<div class="weekday">Mon</div>
</div>
</div>
<!-- Time Slot Selection -->
<div class="section-title">
<i class="bi bi-clock"></i>
<h5 class="mb-0">Select Time</h5>
</div>
<div class="time-slots">
<div class="time-slot unavailable">8:00 AM</div>
<div class="time-slot unavailable">9:00 AM</div>
<div class="time-slot selected">10:00 AM</div>
<div class="time-slot">11:00 AM</div>
<div class="time-slot">1:00 PM</div>
<div class="time-slot">2:00 PM</div>
<div class="time-slot">3:00 PM</div>
<div class="time-slot">4:00 PM</div>
</div>
<!-- Selected Date & Time Summary -->
<div class="selected-date-time">
<div class="d-flex justify-content-between align-items-center">
<strong>Selected Appointment:</strong>
<span>Thursday, June 22, 2023 at 10:00 AM</span>
</div>
</div>
</div>
<!-- Contact Information Form -->
<div class="form-section">
<div class="section-title">
<i class="bi bi-person-lines-fill"></i>
<h5 class="mb-0">Contact Information</h5>
</div>
<form>
<div class="row g-3">
<div class="col-md-6">
<label for="fullName" class="form-label">Full Name</label>
<input type="text" class="form-control" id="fullName" required />
</div>
<div class="col-md-6">
<label for="phone" class="form-label">Phone Number</label>
<input
type="tel"
class="form-control"
id="phone"
placeholder="(555) 555-5555"
required
/>
</div>
<div class="col-12">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" required />
</div>
<div class="col-12">
<label for="notes" class="form-label"
>Additional Notes (Optional)</label
>
<textarea
class="form-control"
id="notes"
rows="3"
placeholder="Any special instructions or information for the inspector"
></textarea>
</div>
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="reminders"
checked
/>
<label class="form-check-label" for="reminders">
Send me text reminders about my appointment
</label>
</div>
</div>
</div>
<!-- Information -->
<div class="alert alert-secondary mt-4">
<small
>Our inspector will need access to view your pool area. If you
won't be home during the appointment, please provide access
instructions in the notes.</small
>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between mt-4">
<a
href="{{ .URLs.ReportContribute }}"
class="btn btn-outline-secondary"
>
<i class="bi bi-arrow-left me-2"></i> Back
</a>
<a href="{{ .URLs.ReportConfirmation }}" class="btn btn-success">
<i class="bi bi-calendar-check me-2"></i> Confirm Appointment
</a>
</div>
</form>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
If you need assistance, please contact Vector Control at (555) 123-4567
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,225 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
body {
background-color: #f8f9fa;
}
.page-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.content-card {
background-color: white;
border-radius: 15px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 25px;
margin-bottom: 20px;
}
.logo-area {
text-align: center;
margin-bottom: 20px;
}
.logo-placeholder {
height: 50px;
max-width: 200px;
margin: 0 auto;
}
.map-container {
height: 350px;
background-color: #e9ecef;
border-radius: 10px;
margin-bottom: 25px;
position: relative;
overflow: hidden;
border: 1px solid #dee2e6;
}
.map-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-image: url("https://placehold.co/800x350/e9ecef/adb5bd?text=Interactive+Map");
background-size: cover;
background-position: center;
}
.map-pin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%);
color: #dc3545;
font-size: 30px;
filter: drop-shadow(0px 2px 2px rgba(0, 0, 0, 0.3));
}
.map-instructions {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(255, 255, 255, 0.9);
padding: 10px 15px;
border-radius: 20px;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
max-width: 90%;
text-align: center;
}
.or-divider {
display: flex;
align-items: center;
margin: 25px 0;
color: #6c757d;
}
.or-divider::before,
.or-divider::after {
content: "";
flex: 1;
border-bottom: 1px solid #dee2e6;
}
.or-divider::before {
margin-right: 15px;
}
.or-divider::after {
margin-left: 15px;
}
.info-box {
background-color: #e8f4f8;
border-left: 4px solid #0d6efd;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.info-box p:last-child {
margin-bottom: 0;
}
.form-section {
margin-bottom: 25px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="page-container">
<!-- Logo -->
<div class="logo-area">
<img
src="https://placehold.co/200x50/e9ecef/adb5bd?text=County+Vector+Control"
alt="County Vector Control"
class="logo-placeholder"
/>
</div>
<!-- Main Content -->
<div class="content-card">
<h1 class="h4 mb-4">Update Property Location</h1>
<!-- Information Box -->
<div class="info-box mb-4">
<h5>
<i class="bi bi-info-circle me-2"></i>Two Ways to Update Location
</h5>
<p>
You can update the property location by either clicking on the map or
entering an address below. Both methods will automatically update each
other.
</p>
</div>
<!-- Map Section -->
<div class="form-section">
<h5 class="mb-3">Option 1: Select Location on Map</h5>
<div class="map-container">
<div class="map-placeholder"></div>
<div class="map-pin">
<i class="bi bi-geo-alt-fill"></i>
</div>
<div class="map-instructions">
<i class="bi bi-hand-index me-2"></i> Click or tap anywhere on the
map to set the location
</div>
</div>
</div>
<!-- Divider -->
<div class="or-divider">OR</div>
<!-- Address Form -->
<div class="form-section">
<h5 class="mb-3">Option 2: Enter Address</h5>
<form>
<div class="mb-3">
<label for="streetAddress" class="form-label">Street Address</label>
<input
type="text"
class="form-control"
id="streetAddress"
placeholder="123 Main Street"
/>
</div>
<div class="row g-3">
<div class="col-md-6">
<label for="city" class="form-label">City</label>
<input
type="text"
class="form-control"
id="city"
placeholder="Riverside"
/>
</div>
<div class="col-md-3">
<label for="state" class="form-label">State</label>
<select class="form-select" id="state">
<option selected>CA</option>
<option>AZ</option>
<option>NV</option>
<option>OR</option>
<!-- Add more states as needed -->
</select>
</div>
<div class="col-md-3">
<label for="zipCode" class="form-label">ZIP Code</label>
<input
type="text"
class="form-control"
id="zipCode"
placeholder="92501"
/>
</div>
</div>
</form>
</div>
<!-- Current Coordinates Display -->
<div class="mb-4 p-3 bg-light rounded small text-muted">
<div class="d-flex justify-content-between">
<span>Current Coordinates:</span>
<span>33.9806° N, 117.3755° W</span>
</div>
</div>
<!-- Action Buttons -->
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary" id="cancelBtn">
Nevermind
</button>
<button type="button" class="btn btn-primary" id="saveBtn">
<i class="bi bi-check-circle me-2"></i> Save Updates
</button>
</div>
</div>
<!-- Help Text -->
<div class="text-center text-muted small">
<p>
If you need assistance, please contact Vector Control at (555) 123-4567
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,266 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
.entry-container {
max-width: 1000px;
margin: 0 auto;
}
.entry-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
padding: 40px;
background-color: #fff;
margin-bottom: 30px;
}
.entry-header {
margin-bottom: 25px;
text-align: center;
}
.logo-area {
text-align: center;
margin-bottom: 30px;
}
.logo-placeholder {
width: 120px;
height: 60px;
background-color: #e9ecef;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.method-section {
margin-bottom: 40px;
padding: 25px;
border-radius: 10px;
background-color: #f8f9fa;
}
.method-title {
margin-bottom: 20px;
display: flex;
align-items: center;
}
.method-title i {
font-size: 1.5rem;
margin-right: 10px;
}
.sms-mockup {
max-width: 300px;
background-color: #dcf8c6;
border-radius: 15px;
padding: 15px;
position: relative;
margin: 20px auto;
}
.qr-code-placeholder {
width: 200px;
height: 200px;
background-color: #e9ecef;
margin: 20px auto;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #ced4da;
}
.email-mockup {
max-width: 550px;
background-color: white;
border: 1px solid #ddd;
border-radius: 10px;
padding: 20px;
margin: 20px auto;
}
.email-header {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 15px;
}
.door-hanger {
max-width: 300px;
background-color: #fff;
border: 2px solid #dc3545;
border-radius: 10px;
padding: 15px;
position: relative;
margin: 20px auto;
text-align: center;
}
.door-hanger:before {
content: "";
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 25px;
background-color: #fff;
border: 2px solid #dc3545;
border-bottom: none;
border-radius: 20px 20px 0 0;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-5">
<div class="entry-container">
<div class="entry-box">
<div class="entry-header">
<h1>Green Pool Reporting</h1>
<p class="text-muted">Entry Points Diagnostic Page</p>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
This page demonstrates the various ways customers can access the Green
Pool Reporting system.
</div>
<!-- SMS Entry Point -->
<div class="method-section">
<div class="method-title">
<i class="bi bi-chat-dots-fill text-primary"></i>
<h3>Text Message Entry Point</h3>
</div>
<p>
Customers will receive the following text message with a link to
begin the reporting process:
</p>
<div class="sms-mockup">
<strong>Vector Control:</strong> We noticed a potential green pool
at your property. Please tap the link to report status or schedule
inspection:
<a href="{{ .URLs.ReportDetail }}">{{ .URLs.ReportDetail }}</a>
</div>
<div class="mt-3">
<p><strong>SMS Details:</strong></p>
<ul>
<li>Sent via automated system after aerial detection</li>
<li>Contains unique tracking link for each property</li>
<li>Customers tap link to open mobile browser</li>
</ul>
</div>
</div>
<!-- Door Hanger Entry Point -->
<div class="method-section">
<div class="method-title">
<i class="bi bi-qr-code text-success"></i>
<h3>Door Hanger QR Code Entry Point</h3>
</div>
<p>
Inspectors will leave door hangers with a QR code for properties
where no one is home:
</p>
<div class="door-hanger">
<h5>IMPORTANT NOTICE</h5>
<p>We visited regarding a potential mosquito breeding site.</p>
<img src="/qr-code/report/t78fd3" width="256" height="256" />
<p>
<strong>Scan this code</strong> with your phone camera to report
your pool status or schedule an inspection.
</p>
<p class="small text-muted">Or visit: {{ .URLs.ReportDetail }}</p>
</div>
<div class="mt-3">
<p><strong>Door Hanger Details:</strong></p>
<ul>
<li>Physical notices left on the door handle</li>
<li>QR code contains property-specific link</li>
<li>Fallback URL provided for manual entry</li>
</ul>
</div>
</div>
<!-- Email Entry Point -->
<div class="method-section">
<div class="method-title">
<i class="bi bi-envelope-fill text-danger"></i>
<h3>Email Notification Entry Point</h3>
</div>
<p>
Property owners will receive this email as a follow-up to other
communication attempts:
</p>
<div class="email-mockup">
<div class="email-header">
<strong>From:</strong> Green Pool Response Team
&lt;noreply@vectorcontrol.county.gov&gt;<br />
<strong>To:</strong> Property Owner
&lt;resident@example.com&gt;<br />
<strong>Subject:</strong> Action Required: Green Pool Detected at
Your Property
</div>
<div class="email-body">
<p>Dear Property Owner,</p>
<p>
Our recent surveillance has identified a potential unmaintained
swimming pool at your property located at
<strong>123 Main Street</strong>. Untreated pools can become
mosquito breeding grounds and pose public health risks,
including the spread of West Nile virus and other diseases.
</p>
<div class="text-center my-4">
<a href="/report/t78fd3" class="btn btn-primary"
>Report Pool Status or Schedule Inspection</a
>
</div>
<p>
Please click the button above or visit
<a href="{{ .URLs.ReportDetail }}">{{ .URLs.ReportDetail }}</a>
to complete a brief questionnaire about your pool status. This
will help us determine if an inspection is needed or if you've
already addressed the issue.
</p>
<p>Thank you for helping keep our community safe and healthy.</p>
<p>
Sincerely,<br />
Vector Control Department<br />
County Health Services
</p>
</div>
</div>
<div class="mt-3">
<p><strong>Email Details:</strong></p>
<ul>
<li>
Sent as follow-up or for property owners with registered email
addresses
</li>
<li>
Contains clear call-to-action button and alternative text link
</li>
<li>Explains reason for contact and next steps</li>
</ul>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<a href="/" class="btn btn-outline-primary">Back to Dashboard</a>
<a href="{{ .URLs.ReportDetail }}" class="btn btn-success"
>Test Reporting Flow</a
>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,387 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.district-logo {
max-height: 80px;
width: auto;
}
.map-container {
height: 300px;
background-color: #e9ecef;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
border: 1px solid #dee2e6;
}
.progress-tracker {
display: flex;
justify-content: space-between;
margin: 20px 0;
position: relative;
}
.progress-tracker::before {
content: "";
position: absolute;
top: 25px;
left: 0;
width: 100%;
height: 2px;
background-color: #dee2e6;
z-index: 1;
}
.progress-step {
position: relative;
z-index: 2;
text-align: center;
width: 60px;
}
.progress-icon {
height: 50px;
width: 50px;
background-color: #fff;
border-radius: 50%;
border: 2px solid #dee2e6;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.progress-icon.active {
border-color: #0d6efd;
background-color: #0d6efd;
color: white;
}
.progress-icon.completed {
border-color: #198754;
background-color: #198754;
color: white;
}
.progress-label {
font-size: 0.8rem;
font-weight: 500;
}
.type-badge {
display: flex;
align-items: center;
font-size: 1.1rem;
}
.type-badge i {
margin-right: 8px;
}
.coordinates {
font-size: 0.85rem;
color: #6c757d;
}
.note-item {
border-left: 3px solid #0d6efd;
padding: 10px 15px;
background-color: rgba(13, 110, 253, 0.05);
margin-bottom: 10px;
}
.note-date {
font-size: 0.8rem;
color: #6c757d;
margin-bottom: 2px;
}
</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 -->
<div class="row mb-4">
<div class="col-md-8">
<h2>Report #MMD-2023-12345</h2>
</div>
<div class="col-md-4 text-md-end">
<span class="badge bg-primary type-badge">
<i class="bi bi-water"></i> Green Pool
</span>
</div>
</div>
<!-- Progress Tracker -->
<div class="progress-tracker mb-5">
<div class="progress-step">
<div class="progress-icon completed">
<i class="bi bi-check-lg"></i>
</div>
<div class="progress-label">Submitted</div>
</div>
<div class="progress-step">
<div class="progress-icon completed">
<i class="bi bi-check-lg"></i>
</div>
<div class="progress-label">Accepted</div>
</div>
<div class="progress-step">
<div class="progress-icon active">
<i class="bi bi-clock"></i>
</div>
<div class="progress-label">Scheduled</div>
</div>
<div class="progress-step">
<div class="progress-icon">
<i class="bi bi-flag"></i>
</div>
<div class="progress-label">Complete</div>
</div>
</div>
<div class="row">
<!-- Map and Location Information -->
<div class="col-lg-5 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Location</h5>
</div>
<div class="card-body">
<!-- Map Placeholder -->
<div class="map-container mb-3">
<div class="text-center">
<i class="bi bi-map fs-1 text-secondary"></i>
<p class="mb-0">Map of Report Location</p>
</div>
</div>
<!-- Address Information -->
<div>
<h6>Address</h6>
<p class="mb-2">123 Mosquito Ave, Lakeside, CA 92040</p>
<p class="coordinates mb-0">
<i class="bi bi-geo-alt"></i> 32.8573° N, 116.9222° W
</p>
</div>
</div>
</div>
</div>
<!-- Report Details -->
<div class="col-lg-7 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Report Details</h5>
</div>
<div class="card-body">
<div class="row mb-4">
<div class="col-md-6">
<h6>Report Source</h6>
<p><i class="bi bi-phone"></i> Phone Call</p>
</div>
<div class="col-md-6">
<h6>Report Date</h6>
<p>October 15, 2023 at 2:45 PM</p>
</div>
</div>
<div class="mb-4">
<h6>Description</h6>
<p>
I noticed my neighbor's backyard pool has turned green and
there's nobody living in the house currently. I'm concerned it
might be breeding mosquitoes as I've noticed more of them in
my yard recently. The house seems to be vacant for about 3
months now.
</p>
</div>
<div class="row mb-4">
<div class="col-md-6">
<h6>Pool Status</h6>
<p>
<span class="badge bg-warning text-dark"
><i class="bi bi-exclamation-triangle"></i>
Stagnant/Green</span
>
</p>
</div>
<div class="col-md-6">
<h6>Scheduled Appointment</h6>
<p>October 20, 2023, 9:00 AM - 11:00 AM</p>
</div>
</div>
<div class="mb-4">
<h6>Contact Information</h6>
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>Reported By:</strong> John Smith</p>
<p class="mb-1"><strong>Phone:</strong> (555) 123-4567</p>
</div>
<div class="col-md-6">
<p class="mb-1">
<strong>Email:</strong> john.smith@example.com
</p>
<p class="mb-1">
<strong>Preferred Contact:</strong> Phone
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Technician Notes -->
<div class="row">
<div class="col-12 mb-4">
<div class="card">
<div
class="card-header d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">Notes & Updates</h5>
<span class="badge bg-info">3 Notes</span>
</div>
<div class="card-body">
<div class="note-item">
<div class="note-date">
Added by System on Oct 15, 2023, 2:45 PM
</div>
<div class="note-content">
Report created via phone call to district office.
</div>
</div>
<div class="note-item">
<div class="note-date">
Added by Sarah Johnson (Office Staff) on Oct 16, 2023, 9:30 AM
</div>
<div class="note-content">
Verified location information. Property appears to be vacant
according to county records. Left voicemail with property
management company listed in county database.
</div>
</div>
<div class="note-item">
<div class="note-date">
Added by Mike Davis (Technician) on Oct 18, 2023, 11:15 AM
</div>
<div class="note-content">
Scheduled inspection for Oct 20. Will need access to backyard.
Contacted reporter to confirm they'll be available to provide
access information on day of service.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Next Steps -->
<div class="row">
<div class="col-12 mb-4">
<div class="card bg-light">
<div class="card-body">
<h5><i class="bi bi-arrow-right-circle"></i> Next Steps</h5>
<p>
Technician scheduled to inspect the property on October 20,
2023, between 9:00 AM - 11:00 AM. If access to the property is
not possible, treatment may be conducted from outside the
property or additional follow-up may be required.
</p>
<p class="mb-0">
<strong>Note:</strong> You will receive a notification when the
status of this report changes.
</p>
</div>
</div>
</div>
</div>
<!-- Update Form -->
<div class="row mt-3">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Add Information</h5>
</div>
<div class="card-body">
<p>
Do you have additional information about this report? Add it
below to update the technician.
</p>
<form>
<div class="mb-3">
<label for="updateMessage" class="form-label">Message</label>
<textarea
class="form-control"
id="updateMessage"
rows="3"
placeholder="Enter additional information here..."
></textarea>
</div>
<div class="mb-3">
<label for="contactPhone" class="form-label"
>Phone Number (optional)</label
>
<input
type="tel"
class="form-control"
id="contactPhone"
placeholder="(555) 123-4567"
/>
<div class="form-text">
Provide your phone number if you'd like to be contacted
about this update.
</div>
</div>
<button type="submit" class="btn btn-primary">
Submit Update
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Back Button -->
<div class="row mt-4">
<div class="col-12">
<a href="/check-status" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Status Check
</a>
</div>
</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

@ -0,0 +1,423 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.district-logo {
max-height: 80px;
width: auto;
}
.map-container {
height: 400px;
background-color: #e9ecef;
border-radius: 5px;
border: 1px solid #dee2e6;
position: relative;
}
.map-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
}
.map-overlay {
position: absolute;
bottom: 20px;
right: 20px;
background-color: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
max-width: 250px;
}
.map-controls {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: column;
gap: 5px;
}
.map-control-btn {
width: 40px;
height: 40px;
background-color: white;
border: 1px solid #dee2e6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.report-type-icon {
font-size: 1.2rem;
}
.days-ago {
font-size: 0.85rem;
color: #6c757d;
}
.status-badge {
font-size: 0.85rem;
font-weight: normal;
padding: 6px 10px;
}
.report-row:hover {
background-color: rgba(13, 110, 253, 0.05);
cursor: pointer;
}
.instruction-card {
border-left: 4px solid #0d6efd;
}
</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">
<!-- Page Title -->
<div class="row mb-4">
<div class="col-12">
<h2>Lookup Reports by Location</h2>
<p class="lead">Find reports and mosquito activity in your area</p>
</div>
</div>
<!-- Instructions Card -->
<div class="row mb-4">
<div class="col-12">
<div class="card instruction-card">
<div class="card-body">
<h5>
<i class="bi bi-info-circle me-2"></i>How to use this tool
</h5>
<p class="mb-0">
You can either <strong>enter an address</strong> in the search
box or <strong>navigate the map</strong> by dragging and zooming
to find reports in your area. The table below will update
automatically to show reports within the current map view.
</p>
</div>
</div>
</div>
</div>
<!-- Search and Map Section -->
<div class="row mb-4">
<div class="col-md-4 mb-3 mb-md-0">
<!-- Address Search -->
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Search by Address</h5>
</div>
<div class="card-body">
<form id="addressSearchForm">
<div class="mb-3">
<label for="addressInput" class="form-label"
>Enter an address or location</label
>
<div class="input-group">
<input
type="text"
class="form-control"
id="addressInput"
placeholder="123 Main St, City, State"
/>
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i>
</button>
</div>
</div>
<div class="mb-3">
<label class="form-label">Search radius</label>
<select class="form-select" id="searchRadius">
<option value="0.5">0.5 miles</option>
<option value="1" selected>1 mile</option>
<option value="2">2 miles</option>
<option value="5">5 miles</option>
</select>
</div>
</form>
<div class="alert alert-info mt-4">
<small
><i class="bi bi-eye me-1"></i> Currently showing reports
within <strong>1 mile</strong> of map center</small
>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<!-- Map Area -->
<div class="map-container">
<div class="map-placeholder">
<i class="bi bi-map fs-1 text-secondary mb-2"></i>
<p class="mb-1 fw-bold">Interactive Map Area</p>
<p class="text-muted small mb-0">
The map will display here and allow you to navigate the area
</p>
</div>
<!-- Simulated map controls -->
<div class="map-controls">
<button class="map-control-btn">
<i class="bi bi-plus"></i>
</button>
<button class="map-control-btn">
<i class="bi bi-dash"></i>
</button>
<button class="map-control-btn">
<i class="bi bi-house"></i>
</button>
</div>
<!-- Map overlay showing current view -->
<div class="map-overlay">
<div class="small fw-bold mb-1">Current View</div>
<div class="small">Lakeside, CA</div>
<div class="small text-muted">32.857° N, 116.922° W</div>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div
class="card-header d-flex justify-content-between align-items-center"
>
<h5 class="mb-0">Reports in This Area</h5>
<span class="badge bg-secondary"
>Showing 12 of 37 total reports</span
>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Type</th>
<th>Location</th>
<th>Submitted</th>
<th>Status</th>
<th>Report ID</th>
</tr>
</thead>
<tbody>
<!-- Report Row 1 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-primary me-1">
<i class="bi bi-water"></i>
</span>
Green Pool
</td>
<td>123 Mosquito Ave</td>
<td>
Oct 15, 2023
<div class="days-ago">5 days ago</div>
</td>
<td>
<span class="badge bg-primary status-badge"
>Scheduled</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12345"
>MMD-2023-12345</a
>
</td>
</tr>
<!-- Report Row 2 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-danger me-1">
<i class="bi bi-bug"></i>
</span>
Mosquito Nuisance
</td>
<td>456 Lake Dr</td>
<td>
Oct 12, 2023
<div class="days-ago">8 days ago</div>
</td>
<td>
<span class="badge bg-success status-badge"
>Complete</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12341"
>MMD-2023-12341</a
>
</td>
</tr>
<!-- Report Row 3 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-success me-1">
<i class="bi bi-droplet"></i>
</span>
Fish Request
</td>
<td>789 Creek Rd</td>
<td>
Oct 18, 2023
<div class="days-ago">2 days ago</div>
</td>
<td>
<span class="badge bg-warning text-dark status-badge"
>Acknowledged</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12350"
>MMD-2023-12350</a
>
</td>
</tr>
<!-- Report Row 4 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-danger me-1">
<i class="bi bi-bug"></i>
</span>
Mosquito Nuisance
</td>
<td>101 Pond Ln</td>
<td>
Sep 25, 2023
<div class="days-ago">25 days ago</div>
</td>
<td>
<span class="badge bg-success status-badge"
>Complete</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12289"
>MMD-2023-12289</a
>
</td>
</tr>
<!-- Report Row 5 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-primary me-1">
<i class="bi bi-water"></i>
</span>
Green Pool
</td>
<td>202 Highland Ave</td>
<td>
Oct 19, 2023
<div class="days-ago">1 day ago</div>
</td>
<td>
<span class="badge bg-secondary status-badge"
>Submitted</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12356"
>MMD-2023-12356</a
>
</td>
</tr>
<!-- Report Row 6 -->
<tr class="report-row">
<td>
<span class="report-type-icon text-primary me-1">
<i class="bi bi-water"></i>
</span>
Green Pool
</td>
<td>303 Marsh Way</td>
<td>
Aug 15, 2023
<div class="days-ago">2 months ago</div>
</td>
<td>
<span class="badge bg-success status-badge"
>Complete</span
>
</td>
<td>
<a href="/report-details?id=MMD-2023-12056"
>MMD-2023-12056</a
>
</td>
</tr>
<!-- More rows would go here -->
</tbody>
</table>
</div>
</div>
<div class="card-footer text-center">
<p class="mb-0 small text-muted">
Showing reports submitted within the last 6 months. The table
displays up to 20 reports at a time.
</p>
</div>
</div>
</div>
</div>
<!-- Back Button -->
<div class="row mt-4">
<div class="col-12">
<a href="/check-status" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Status Check
</a>
</div>
</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

@ -0,0 +1,738 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<script>
// Handle inspection type selection
function selectInspectionType(type) {
// Remove selected class from both cards
document
.getElementById("propertyInspection")
.classList.remove("selected");
document
.getElementById("neighborhoodInspection")
.classList.remove("selected");
// Add selected class to chosen card
if (type === "property") {
document.getElementById("propertyInspection").classList.add("selected");
document.getElementById("inspectionTypeProperty").checked = true;
document.getElementById("schedulingSection").style.display = "block";
} else {
document
.getElementById("neighborhoodInspection")
.classList.add("selected");
document.getElementById("inspectionTypeNeighborhood").checked = true;
document.getElementById("schedulingSection").style.display = "none";
}
}
// Check for source identification
document.addEventListener("DOMContentLoaded", function () {
const sourceCheckboxes = [
document.getElementById("sourceStagnantWater"),
document.getElementById("sourceContainers"),
document.getElementById("sourceGutters"),
];
const sourceAlert = document.getElementById("sourceFoundAlert");
sourceCheckboxes.forEach((checkbox) => {
checkbox.addEventListener("change", function () {
// If any source is checked, show the alert
if (sourceCheckboxes.some((cb) => cb.checked)) {
sourceAlert.style.display = "block";
} else {
sourceAlert.style.display = "none";
}
});
});
});
</script>
<style>
.district-logo {
max-height: 80px;
width: auto;
}
.form-section {
margin-bottom: 2.5rem;
padding-bottom: 2rem;
border-bottom: 1px solid #dee2e6;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 1rem;
padding-bottom: 0;
}
.section-heading {
margin-bottom: 1.5rem;
display: flex;
align-items: center;
}
.section-heading i {
margin-right: 10px;
font-size: 1.5rem;
color: #0d6efd;
}
.optional-label {
font-size: 0.875rem;
color: #6c757d;
font-weight: normal;
margin-left: 8px;
}
.submit-container {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-top: 2rem;
}
.source-card {
height: 100%;
transition: transform 0.3s;
}
.source-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.source-icon {
font-size: 2rem;
margin-bottom: 1rem;
color: #0d6efd;
}
.time-of-day-btn {
width: 100%;
margin-bottom: 10px;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 0;
}
.time-of-day-icon {
font-size: 1.5rem;
margin-bottom: 8px;
}
.time-label {
font-size: 0.9rem;
}
.severity-item {
text-align: center;
padding: 10px;
}
.severity-scale {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.btn-check:checked + .btn.time-of-day-btn {
background-color: #0d6efd;
color: white;
}
.inspection-type-card {
cursor: pointer;
border: 1px solid #dee2e6;
padding: 20px;
border-radius: 5px;
height: 100%;
transition: all 0.3s;
}
.inspection-type-card.selected {
border-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.05);
}
.inspection-type-card:hover {
border-color: #0d6efd;
}
.card-highlight {
border-left: 4px solid #0d6efd;
background-color: #f8f9fa;
}
</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">
<!-- Page Title -->
<div class="row mb-4">
<div class="col-12">
<h2>Report Mosquito Nuisance</h2>
<p class="lead">Help us identify mosquito activity in your area</p>
</div>
</div>
<!-- Info Alert -->
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info" role="alert">
<h5 class="alert-heading">
<i class="bi bi-info-circle me-2"></i>About Mosquito Control
</h5>
<p class="mb-0">
While we don't spray for adult mosquitoes based on individual
requests, your reports help us identify and eliminate breeding
sources. Adult mosquito control is based on trap counts and
disease testing. Your detailed information helps us prioritize our
work and locate potential breeding sites.
</p>
</div>
</div>
</div>
<!-- Report Form -->
<form id="mosquitoNuisanceForm">
<!-- Mosquito Activity Section -->
<div class="form-section">
<div class="section-heading">
<i class="bi bi-bug"></i>
<h3>Mosquito Activity Information</h3>
<span class="optional-label">optional</span>
</div>
<p class="mb-4">
The time when mosquitoes are active can help us identify the species
and likely breeding sources.
</p>
<!-- Time of Day -->
<div class="row mb-4">
<div class="col-12">
<label class="form-label"
>When do you typically notice mosquitoes? (Select all that
apply)</label
>
<div class="row">
<div class="col-6 col-md-3">
<input
type="checkbox"
class="btn-check"
id="earlyMorning"
autocomplete="off"
/>
<label
class="btn btn-outline-primary time-of-day-btn"
for="earlyMorning"
>
<span class="time-of-day-icon"
><i class="bi bi-sunrise"></i
></span>
<span class="time-label">Early Morning</span>
<small class="text-muted">5am-8am</small>
</label>
</div>
<div class="col-6 col-md-3">
<input
type="checkbox"
class="btn-check"
id="daytime"
autocomplete="off"
/>
<label
class="btn btn-outline-primary time-of-day-btn"
for="daytime"
>
<span class="time-of-day-icon"
><i class="bi bi-sun"></i
></span>
<span class="time-label">Daytime</span>
<small class="text-muted">8am-5pm</small>
</label>
</div>
<div class="col-6 col-md-3">
<input
type="checkbox"
class="btn-check"
id="evening"
autocomplete="off"
/>
<label
class="btn btn-outline-primary time-of-day-btn"
for="evening"
>
<span class="time-of-day-icon"
><i class="bi bi-sunset"></i
></span>
<span class="time-label">Evening</span>
<small class="text-muted">5pm-9pm</small>
</label>
</div>
<div class="col-6 col-md-3">
<input
type="checkbox"
class="btn-check"
id="night"
autocomplete="off"
/>
<label
class="btn btn-outline-primary time-of-day-btn"
for="night"
>
<span class="time-of-day-icon"
><i class="bi bi-moon-stars"></i
></span>
<span class="time-label">Night</span>
<small class="text-muted">9pm-5am</small>
</label>
</div>
</div>
</div>
</div>
<!-- Duration -->
<div class="row mb-4">
<div class="col-md-6">
<label for="duration" class="form-label"
>How long have you been experiencing this mosquito
problem?</label
>
<select class="form-select" id="duration">
<option value="just-noticed">Just noticed recently</option>
<option value="few-days">A few days</option>
<option value="1-2-weeks">1-2 weeks</option>
<option value="2-4-weeks">2-4 weeks</option>
<option value="1-3-months">1-3 months</option>
<option value="seasonal">All season (recurring issue)</option>
</select>
</div>
<!-- Severity -->
<div class="col-md-6">
<label for="severityRange" class="form-label"
>How would you rate the severity of the mosquito problem?</label
>
<input
type="range"
class="form-range"
min="1"
max="5"
id="severityRange"
oninput="document.getElementById('severityValue').innerText = this.value"
/>
<div class="severity-scale">
<div class="severity-item">
<div>Minor</div>
<small>Occasional mosquito</small>
</div>
<div class="severity-item">
<div>Moderate</div>
<small>Regular presence</small>
</div>
<div class="severity-item">
<div>Severe</div>
<small>Many mosquitoes</small>
</div>
</div>
<div class="text-center">
Current selection: <span id="severityValue">3</span>/5
</div>
</div>
</div>
<!-- Location -->
<div class="row">
<div class="col-md-12">
<label for="activityLocation" class="form-label"
>Where on your property do you notice the most mosquito
activity?</label
>
<select class="form-select" id="activityLocation">
<option value="">Please select</option>
<option value="front-yard">Front yard</option>
<option value="backyard">Back yard</option>
<option value="patio">Patio/deck area</option>
<option value="garden">Garden</option>
<option value="pool-area">Pool area</option>
<option value="throughout">Throughout the property</option>
<option value="indoors">Indoors</option>
<option value="other">Other area</option>
</select>
</div>
</div>
</div>
<!-- Potential Sources Section -->
<div class="form-section">
<div class="section-heading">
<i class="bi bi-search"></i>
<h3>Potential Mosquito Sources</h3>
<span class="optional-label">optional</span>
</div>
<p class="mb-3">
Have you noticed any of these common mosquito breeding sources in
your area?
</p>
<div class="card card-highlight mb-4">
<div class="card-body">
<h5 class="card-title">Did you know?</h5>
<p class="card-text">
Mosquitoes can breed in as little as a bottle cap of water!
Eliminating standing water is the most effective way to reduce
mosquito populations.
</p>
</div>
</div>
<div class="row g-4 mb-4">
<!-- Source 1 -->
<div class="col-md-4">
<div class="card source-card">
<div class="card-body text-center">
<div class="source-icon">
<i class="bi bi-water"></i>
</div>
<h5 class="card-title">Stagnant Water</h5>
<p class="card-text">
Green pools, ponds, fountains, or birdbaths that aren't
maintained
</p>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="sourceStagnantWater"
/>
<label class="form-check-label" for="sourceStagnantWater">
I've noticed this
</label>
</div>
</div>
</div>
</div>
<!-- Source 2 -->
<div class="col-md-4">
<div class="card source-card">
<div class="card-body text-center">
<div class="source-icon">
<i class="bi bi-droplet"></i>
</div>
<h5 class="card-title">Containers</h5>
<p class="card-text">
Buckets, planters, toys, tires, or any items that collect
rainwater
</p>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="sourceContainers"
/>
<label class="form-check-label" for="sourceContainers">
I've noticed this
</label>
</div>
</div>
</div>
</div>
<!-- Source 3 -->
<div class="col-md-4">
<div class="card source-card">
<div class="card-body text-center">
<div class="source-icon">
<i class="bi bi-house"></i>
</div>
<h5 class="card-title">Roof & Gutters</h5>
<p class="card-text">
Clogged gutters, flat roofs, or AC units that collect water
</p>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="sourceGutters"
/>
<label class="form-check-label" for="sourceGutters">
I've noticed this
</label>
</div>
</div>
</div>
</div>
</div>
<div
class="alert alert-warning mb-4"
id="sourceFoundAlert"
style="display: none;"
>
<h5 class="alert-heading">
<i class="bi bi-exclamation-triangle me-2"></i>Potential Breeding
Source Found!
</h5>
<p>
It looks like you may have identified a mosquito breeding source.
If you'd like to report a specific source (like a green pool),
please use our
<a href="/report-green-pool" class="alert-link"
>Report a Green Pool</a
>
form for faster service.
</p>
</div>
<div class="row">
<div class="col-md-12">
<label for="otherSources" class="form-label"
>Have you noticed any other potential mosquito breeding
sources?</label
>
<textarea
class="form-control"
id="otherSources"
rows="2"
placeholder="Describe any other potential breeding sites you've noticed..."
></textarea>
</div>
</div>
</div>
<!-- Inspection Request Section -->
<div class="form-section">
<div class="section-heading">
<i class="bi bi-clipboard-check"></i>
<h3>Inspection Request</h3>
</div>
<p class="mb-4">
Would you like our technicians to inspect for potential mosquito
sources?
</p>
<div class="row g-4 mb-4">
<div class="col-md-6">
<div
class="inspection-type-card"
onclick="selectInspectionType('property')"
id="propertyInspection"
>
<h5>
<i class="bi bi-house-door me-2"></i>Property Inspection
</h5>
<p>
Request a technician to inspect your property for mosquito
sources. We'll contact you to schedule a convenient time.
</p>
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="inspectionType"
id="inspectionTypeProperty"
value="property"
/>
<label class="form-check-label" for="inspectionTypeProperty">
<strong>Schedule a property inspection</strong>
</label>
</div>
</div>
</div>
<div class="col-md-6">
<div
class="inspection-type-card"
onclick="selectInspectionType('neighborhood')"
id="neighborhoodInspection"
>
<h5><i class="bi bi-map me-2"></i>Neighborhood Inspection</h5>
<p>
Request a general inspection of your neighborhood. We'll
survey the area for potential mosquito breeding sources.
</p>
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="inspectionType"
id="inspectionTypeNeighborhood"
value="neighborhood"
/>
<label
class="form-check-label"
for="inspectionTypeNeighborhood"
>
<strong>Request neighborhood inspection</strong>
</label>
</div>
</div>
</div>
</div>
<!-- Property Inspection Scheduling (hidden by default) -->
<div id="schedulingSection" style="display: none;">
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Schedule Property Inspection</h5>
<p class="card-text">
Please indicate your availability for a technician visit.
</p>
<div class="row mb-3">
<div class="col-md-6">
<label for="preferredDateRange" class="form-label"
>Preferred Date Range</label
>
<select class="form-select" id="preferredDateRange">
<option value="">Select preferred dates</option>
<option value="next-week">Next week</option>
<option value="in-two-weeks">In two weeks</option>
<option value="any-time">Any time</option>
</select>
</div>
<div class="col-md-6">
<label for="preferredTime" class="form-label"
>Preferred Time of Day</label
>
<select class="form-select" id="preferredTime">
<option value="">Select preferred time</option>
<option value="morning">Morning (8am-12pm)</option>
<option value="afternoon">Afternoon (12pm-4pm)</option>
<option value="any-time">Any time</option>
</select>
</div>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requestCall"
/>
<label class="form-check-label" for="requestCall">
Please call me to schedule a specific appointment time
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Location & Contact Section -->
<div class="form-section">
<div class="section-heading">
<i class="bi bi-geo-alt"></i>
<h3>Location & Contact Information</h3>
</div>
<div class="row mb-4">
<div class="col-md-12">
<label for="address" class="form-label">Your Address</label>
<input
type="text"
class="form-control"
id="address"
placeholder="Enter your street address"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="name" class="form-label">Your Name</label>
<input type="text" class="form-control" id="name" />
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" id="phone" />
</div>
<div class="col-md-12 mb-3">
<label for="email" class="form-label">Email Address</label>
<input type="email" class="form-control" id="email" />
<div class="form-text">
We'll use this to send you a confirmation and follow-up
information.
</div>
</div>
</div>
</div>
<!-- Additional Information Section -->
<div class="form-section">
<div class="section-heading">
<i class="bi bi-card-text"></i>
<h3>Additional Information</h3>
<span class="optional-label">optional</span>
</div>
<div class="row">
<div class="col-md-12">
<label for="additionalInfo" class="form-label"
>Is there anything else you'd like us to know?</label
>
<textarea
class="form-control"
id="additionalInfo"
rows="4"
placeholder="Additional details about the mosquito issue..."
></textarea>
</div>
</div>
</div>
<!-- Submit Section -->
<div class="submit-container">
<div class="row align-items-center">
<div class="col-md-8">
<p class="mb-0">
<strong>Thank you for reporting this mosquito issue.</strong>
</p>
<p class="mb-0 small text-muted">
After submission, you'll receive a confirmation with a report ID
and further information.
</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<button type="submit" class="btn btn-primary btn-lg">
Submit Report
</button>
</div>
</div>
</div>
</form>
<!-- Back Button -->
<div class="row mt-4">
<div class="col-12">
<a href="/" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Home
</a>
</div>
</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

@ -1,82 +1,83 @@
{{template "base.html" .}} {{ template "sync/base.html" . }}
{{define "title"}}Dash{{end}} {{ define "title" }}Dash{{ end }}
{{define "extraheader"}} {{ define "extraheader" }}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
const photoUpload = document.getElementById('photoUpload'); const photoUpload = document.getElementById("photoUpload");
const imagePreview = document.getElementById('imagePreview'); const imagePreview = document.getElementById("imagePreview");
const dropArea = document.getElementById('dropArea'); const dropArea = document.getElementById("dropArea");
// Handle file selection // Handle file selection
photoUpload.addEventListener('change', handleFileSelect); photoUpload.addEventListener("change", handleFileSelect);
// Handle drag and drop // Handle drag and drop
dropArea.addEventListener('dragover', function(e) { dropArea.addEventListener("dragover", function (e) {
e.preventDefault(); e.preventDefault();
dropArea.style.backgroundColor = '#e9ecef'; dropArea.style.backgroundColor = "#e9ecef";
}); });
dropArea.addEventListener('dragleave', function() { dropArea.addEventListener("dragleave", function () {
dropArea.style.backgroundColor = '#f8f9fa'; dropArea.style.backgroundColor = "#f8f9fa";
}); });
dropArea.addEventListener('drop', function(e) { dropArea.addEventListener("drop", function (e) {
e.preventDefault(); e.preventDefault();
dropArea.style.backgroundColor = '#f8f9fa'; dropArea.style.backgroundColor = "#f8f9fa";
if (e.dataTransfer.files.length) { if (e.dataTransfer.files.length) {
handleFiles(e.dataTransfer.files); handleFiles(e.dataTransfer.files);
} }
}); });
function handleFileSelect(e) { function handleFileSelect(e) {
const files = e.target.files; const files = e.target.files;
handleFiles(files); handleFiles(files);
}
function handleFiles(files) {
const maxFiles = 5;
const currentFiles = imagePreview.querySelectorAll('.preview-item').length;
for (let i = 0; i < files.length && currentFiles + i < maxFiles; i++) {
const file = files[i];
if (!file.type.match('image.*')) {
continue;
} }
const reader = new FileReader(); function handleFiles(files) {
const maxFiles = 5;
reader.onload = (function(theFile) { const currentFiles =
return function(e) { imagePreview.querySelectorAll(".preview-item").length;
const previewItem = document.createElement('div');
previewItem.className = 'preview-item'; for (let i = 0; i < files.length && currentFiles + i < maxFiles; i++) {
const file = files[i];
const img = document.createElement('img');
img.src = e.target.result; if (!file.type.match("image.*")) {
continue;
const removeBtn = document.createElement('div'); }
removeBtn.className = 'preview-remove';
removeBtn.innerHTML = 'x'; const reader = new FileReader();
removeBtn.addEventListener('click', function() {
imagePreview.removeChild(previewItem); reader.onload = (function (theFile) {
}); return function (e) {
const previewItem = document.createElement("div");
previewItem.appendChild(img); previewItem.className = "preview-item";
previewItem.appendChild(removeBtn);
imagePreview.appendChild(previewItem); const img = document.createElement("img");
}; img.src = e.target.result;
})(file);
const removeBtn = document.createElement("div");
reader.readAsDataURL(file); removeBtn.className = "preview-remove";
} removeBtn.innerHTML = "x";
removeBtn.addEventListener("click", function () {
photoUpload.value = ''; imagePreview.removeChild(previewItem);
} });
});
</script> previewItem.appendChild(img);
<style> previewItem.appendChild(removeBtn);
imagePreview.appendChild(previewItem);
};
})(file);
reader.readAsDataURL(file);
}
photoUpload.value = "";
}
});
</script>
<style>
.district-logo { .district-logo {
max-height: 80px; max-height: 80px;
width: auto; width: auto;
@ -125,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() {
position: absolute; position: absolute;
top: 5px; top: 5px;
right: 5px; right: 5px;
background-color: rgba(0,0,0,0.5); background-color: rgba(0, 0, 0, 0.5);
color: white; color: white;
border-radius: 50%; border-radius: 50%;
width: 20px; width: 20px;
@ -168,9 +169,9 @@ document.addEventListener('DOMContentLoaded', function() {
border-radius: 5px; border-radius: 5px;
margin-top: 2rem; margin-top: 2rem;
} }
</style> </style>
{{end}} {{ end }}
{{define "content"}} {{ define "content" }}
<!-- Header --> <!-- Header -->
<header class="bg-light py-3"> <header class="bg-light py-3">
<div class="container"> <div class="container">
@ -179,7 +180,11 @@ document.addEventListener('DOMContentLoaded', function() {
<h1 class="district-name">{{ .DistrictName }}</h1> <h1 class="district-name">{{ .DistrictName }}</h1>
</div> </div>
<div class="col-md-6 text-md-end"> <div class="col-md-6 text-md-end">
<img src="placeholder-logo.png" alt="District Logo" class="district-logo"> <img
src="placeholder-logo.png"
alt="District Logo"
class="district-logo"
/>
</div> </div>
</div> </div>
</div> </div>
@ -192,7 +197,10 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<h2>Report a Green Pool or Mosquito Source</h2> <h2>Report a Green Pool or Mosquito Source</h2>
<p class="lead">Help us locate and treat potential mosquito breeding sources in your area</p> <p class="lead">
Help us locate and treat potential mosquito breeding sources in your
area
</p>
</div> </div>
</div> </div>
@ -200,8 +208,14 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
<h5 class="alert-heading"><i class="bi bi-info-circle me-2"></i>All fields are optional</h5> <h5 class="alert-heading">
<p class="mb-0">We appreciate any information you can provide. The more details you share, the better we can address the issue. Photos and location information are especially helpful.</p> <i class="bi bi-info-circle me-2"></i>All fields are optional
</h5>
<p class="mb-0">
We appreciate any information you can provide. The more details
you share, the better we can address the issue. Photos and
location information are especially helpful.
</p>
</div> </div>
</div> </div>
</div> </div>
@ -215,21 +229,36 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Photos</h3> <h3>Photos</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<p class="mb-3">Photos help us identify the severity of the issue and may contain location data that can help us find the source.</p> <p class="mb-3">
Photos help us identify the severity of the issue and may contain
location data that can help us find the source.
</p>
<div class="file-upload-container" id="dropArea"> <div class="file-upload-container" id="dropArea">
<input type="file" id="photoUpload" multiple accept="image/*" class="d-none"> <input
type="file"
id="photoUpload"
multiple
accept="image/*"
class="d-none"
/>
<div class="mb-2"> <div class="mb-2">
<i class="bi bi-cloud-arrow-up fs-1 text-primary"></i> <i class="bi bi-cloud-arrow-up fs-1 text-primary"></i>
</div> </div>
<p class="mb-1"><strong>Drag and drop photos here</strong></p> <p class="mb-1"><strong>Drag and drop photos here</strong></p>
<p class="mb-1">- or -</p> <p class="mb-1">- or -</p>
<button type="button" class="btn btn-primary mt-2" onclick="document.getElementById('photoUpload').click()"> <button
type="button"
class="btn btn-primary mt-2"
onclick="document.getElementById('photoUpload').click()"
>
Select Photos Select Photos
</button> </button>
<p class="small text-muted mt-2 mb-0">You can upload multiple photos (maximum 5)</p> <p class="small text-muted mt-2 mb-0">
You can upload multiple photos (maximum 5)
</p>
</div> </div>
<!-- Image Preview Area --> <!-- Image Preview Area -->
<div class="image-preview" id="imagePreview"> <div class="image-preview" id="imagePreview">
<!-- Preview items will be added here dynamically --> <!-- Preview items will be added here dynamically -->
@ -243,21 +272,36 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Location</h3> <h3>Location</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<p class="mb-3">Please provide the location of the potential mosquito breeding source. We may be able to extract this information from your photos if they contain location data.</p> <p class="mb-3">
Please provide the location of the potential mosquito breeding
source. We may be able to extract this information from your photos
if they contain location data.
</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-12"> <div class="col-md-12">
<label for="address" class="form-label">Address or Description of Location</label> <label for="address" class="form-label"
<input type="text" class="form-control" id="address" placeholder="123 Main St, City, State or nearby landmark description"> >Address or Description of Location</label
>
<input
type="text"
class="form-control"
id="address"
placeholder="123 Main St, City, State or nearby landmark description"
/>
</div> </div>
</div> </div>
<p class="small text-muted mb-2">You can also click on the map to mark the location precisely</p> <p class="small text-muted mb-2">
You can also click on the map to mark the location precisely
</p>
<div class="map-container"> <div class="map-container">
<div class="text-center"> <div class="text-center">
<i class="bi bi-map fs-1 text-secondary"></i> <i class="bi bi-map fs-1 text-secondary"></i>
<p class="mb-0">Interactive Map</p> <p class="mb-0">Interactive Map</p>
<p class="text-muted small">Click to set the location of the mosquito source</p> <p class="text-muted small">
Click to set the location of the mosquito source
</p>
</div> </div>
</div> </div>
</div> </div>
@ -269,10 +313,12 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Source Details</h3> <h3>Source Details</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label for="duration" class="form-label">How long has this source been present?</label> <label for="duration" class="form-label"
>How long has this source been present?</label
>
<select class="form-select" id="duration"> <select class="form-select" id="duration">
<option value="">I don't know</option> <option value="">I don't know</option>
<option value="less-than-week">Less than a week</option> <option value="less-than-week">Less than a week</option>
@ -282,23 +328,34 @@ document.addEventListener('DOMContentLoaded', function() {
<option value="more-than-3-months">More than 3 months</option> <option value="more-than-3-months">More than 3 months</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label d-block">Have you observed any of the following? <a href="#" data-bs-toggle="modal" data-bs-target="#larvaeInfoModal"><i class="bi bi-question-circle small ms-1"></i></a></label> <label class="form-label d-block"
>Have you observed any of the following?
<a
href="#"
data-bs-toggle="modal"
data-bs-target="#larvaeInfoModal"
><i class="bi bi-question-circle small ms-1"></i></a
></label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="larvae"> <input class="form-check-input" type="checkbox" id="larvae" />
<label class="form-check-label" for="larvae"> <label class="form-check-label" for="larvae">
Larvae (wigglers) in water Larvae (wigglers) in water
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="pupae"> <input class="form-check-input" type="checkbox" id="pupae" />
<label class="form-check-label" for="pupae"> <label class="form-check-label" for="pupae">
Pupae (tumblers) in water Pupae (tumblers) in water
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="adultMosquitoes"> <input
class="form-check-input"
type="checkbox"
id="adultMosquitoes"
/>
<label class="form-check-label" for="adultMosquitoes"> <label class="form-check-label" for="adultMosquitoes">
Adult mosquitoes near the source Adult mosquitoes near the source
</label> </label>
@ -314,43 +371,71 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Access Information</h3> <h3>Access Information</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<p class="mb-3">Please provide any details about how to access the mosquito source. This helps our technicians when they visit the site.</p> <p class="mb-3">
Please provide any details about how to access the mosquito source.
This helps our technicians when they visit the site.
</p>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-12"> <div class="col-md-12">
<label for="accessInfo" class="form-label">How can the source be accessed?</label> <label for="accessInfo" class="form-label"
<textarea class="form-control" id="accessInfo" rows="3" placeholder="Example: The pool is in the backyard, which can be accessed through a side gate on the right side of the house."></textarea> >How can the source be accessed?</label
>
<textarea
class="form-control"
id="accessInfo"
rows="3"
placeholder="Example: The pool is in the backyard, which can be accessed through a side gate on the right side of the house."
></textarea>
</div> </div>
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label d-block">Access obstacles (check all that apply):</label> <label class="form-label d-block"
>Access obstacles (check all that apply):</label
>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="gate"> <input class="form-check-input" type="checkbox" id="gate" />
<label class="form-check-label" for="gate">Gate</label> <label class="form-check-label" for="gate">Gate</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="fence"> <input
class="form-check-input"
type="checkbox"
id="fence"
/>
<label class="form-check-label" for="fence">Fence</label> <label class="form-check-label" for="fence">Fence</label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="lockedEntrance"> <input
<label class="form-check-label" for="lockedEntrance">Locked entrance</label> class="form-check-input"
type="checkbox"
id="lockedEntrance"
/>
<label class="form-check-label" for="lockedEntrance"
>Locked entrance</label
>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="dogs"> <input class="form-check-input" type="checkbox" id="dogs" />
<label class="form-check-label" for="dogs">Dogs/pets</label> <label class="form-check-label" for="dogs">Dogs/pets</label>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="otherObstacle"> <input
<label class="form-check-label" for="otherObstacle">Other obstacle</label> class="form-check-input"
type="checkbox"
id="otherObstacle"
/>
<label class="form-check-label" for="otherObstacle"
>Other obstacle</label
>
</div> </div>
</div> </div>
</div> </div>
@ -365,42 +450,47 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Contact Information</h3> <h3>Contact Information</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<!-- Property Owner Information --> <!-- Property Owner Information -->
<h5 class="mb-3">Property Owner Information (if known)</h5> <h5 class="mb-3">Property Owner Information (if known)</h5>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="ownerName" class="form-label">Owner Name</label> <label for="ownerName" class="form-label">Owner Name</label>
<input type="text" class="form-control" id="ownerName"> <input type="text" class="form-control" id="ownerName" />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="ownerPhone" class="form-label">Owner Phone</label> <label for="ownerPhone" class="form-label">Owner Phone</label>
<input type="tel" class="form-control" id="ownerPhone"> <input type="tel" class="form-control" id="ownerPhone" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label for="ownerEmail" class="form-label">Owner Email</label> <label for="ownerEmail" class="form-label">Owner Email</label>
<input type="email" class="form-control" id="ownerEmail"> <input type="email" class="form-control" id="ownerEmail" />
</div> </div>
</div> </div>
<!-- Your Contact Information --> <!-- Your Contact Information -->
<h5 class="mb-3">Your Contact Information (for updates)</h5> <h5 class="mb-3">Your Contact Information (for updates)</h5>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="reporterName" class="form-label">Your Name</label> <label for="reporterName" class="form-label">Your Name</label>
<input type="text" class="form-control" id="reporterName"> <input type="text" class="form-control" id="reporterName" />
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="reporterPhone" class="form-label">Your Phone</label> <label for="reporterPhone" class="form-label">Your Phone</label>
<input type="tel" class="form-control" id="reporterPhone"> <input type="tel" class="form-control" id="reporterPhone" />
</div> </div>
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3">
<label for="reporterEmail" class="form-label">Your Email</label> <label for="reporterEmail" class="form-label">Your Email</label>
<input type="email" class="form-control" id="reporterEmail"> <input type="email" class="form-control" id="reporterEmail" />
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="receiveUpdates" checked> <input
class="form-check-input"
type="checkbox"
id="receiveUpdates"
checked
/>
<label class="form-check-label" for="receiveUpdates"> <label class="form-check-label" for="receiveUpdates">
I would like to receive updates on this report I would like to receive updates on this report
</label> </label>
@ -416,12 +506,22 @@ document.addEventListener('DOMContentLoaded', function() {
<h3>Additional Information</h3> <h3>Additional Information</h3>
<span class="optional-label">optional</span> <span class="optional-label">optional</span>
</div> </div>
<p class="mb-3">Please provide any other information that might help us address this mosquito source.</p> <p class="mb-3">
Please provide any other information that might help us address this
mosquito source.
</p>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label for="additionalInfo" class="form-label">Additional Details</label> <label for="additionalInfo" class="form-label"
<textarea class="form-control" id="additionalInfo" rows="4" placeholder="Example: The house appears to be vacant. There is algae growth in the pool. I've noticed increased mosquito activity in the evenings."></textarea> >Additional Details</label
>
<textarea
class="form-control"
id="additionalInfo"
rows="4"
placeholder="Example: The house appears to be vacant. There is algae growth in the pool. I've noticed increased mosquito activity in the evenings."
></textarea>
</div> </div>
</div> </div>
</div> </div>
@ -430,8 +530,16 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="submit-container"> <div class="submit-container">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col-md-8"> <div class="col-md-8">
<p class="mb-0"><strong>Thank you for helping us keep our community safe from mosquito-borne illnesses.</strong></p> <p class="mb-0">
<p class="mb-0 small text-muted">After submission, you will receive a confirmation with a report ID for tracking purposes.</p> <strong
>Thank you for helping us keep our community safe from
mosquito-borne illnesses.</strong
>
</p>
<p class="mb-0 small text-muted">
After submission, you will receive a confirmation with a report
ID for tracking purposes.
</p>
</div> </div>
<div class="col-md-4 text-md-end mt-3 mt-md-0"> <div class="col-md-4 text-md-end mt-3 mt-md-0">
<button type="submit" class="btn btn-primary btn-lg"> <button type="submit" class="btn btn-primary btn-lg">
@ -454,12 +562,25 @@ document.addEventListener('DOMContentLoaded', function() {
</main> </main>
<!-- Larvae Info Modal --> <!-- Larvae Info Modal -->
<div class="modal fade" id="larvaeInfoModal" tabindex="-1" aria-labelledby="larvaeInfoModalLabel" aria-hidden="true"> <div
class="modal fade"
id="larvaeInfoModal"
tabindex="-1"
aria-labelledby="larvaeInfoModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="larvaeInfoModalLabel">How to Identify Mosquito Larvae and Pupae</h5> <h5 class="modal-title" id="larvaeInfoModalLabel">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> How to Identify Mosquito Larvae and Pupae
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="row mb-4"> <div class="row mb-4">
@ -486,7 +607,10 @@ document.addEventListener('DOMContentLoaded', function() {
</ul> </ul>
</div> </div>
</div> </div>
<p>When looking for mosquito larvae and pupae, check standing water sources like:</p> <p>
When looking for mosquito larvae and pupae, check standing water
sources like:
</p>
<ul> <ul>
<li>Swimming pools</li> <li>Swimming pools</li>
<li>Bird baths</li> <li>Bird baths</li>
@ -495,13 +619,24 @@ document.addEventListener('DOMContentLoaded', function() {
<li>Plant saucers</li> <li>Plant saucers</li>
<li>Rain gutters</li> <li>Rain gutters</li>
</ul> </ul>
<p>If you see small creatures moving in standing water, there's a good chance they're mosquito larvae or pupae.</p> <p>
If you see small creatures moving in standing water, there's a good
chance they're mosquito larvae or pupae.
</p>
<div class="text-center"> <div class="text-center">
<a href="#" class="btn btn-outline-primary">View Detailed Identification Guide</a> <a href="#" class="btn btn-outline-primary"
>View Detailed Identification Guide</a
>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
</div> </div>
</div> </div>
</div> </div>
@ -512,13 +647,16 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<p class="mb-0">&copy; 2023 {{ .DistrictName }} Mosquito Management District</p> <p class="mb-0">
&copy; 2023 {{ .DistrictName }} Mosquito Management District
</p>
</div> </div>
<div class="col-md-6 text-md-end"> <div class="col-md-6 text-md-end">
<p class="mb-0">Contact: (555) 123-4567 | info@mosquitodistrict.gov</p> <p class="mb-0">
Contact: (555) 123-4567 | info@mosquitodistrict.gov
</p>
</div> </div>
</div> </div>
</div> </div>
</footer> </footer>
{{ end }}
{{end}}

View file

@ -0,0 +1,150 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.district-logo {
max-height: 60px;
width: auto;
}
.success-icon {
width: 100px;
height: 100px;
background-color: #28a745;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 2rem;
color: white;
}
.confirmation-card {
max-width: 600px;
margin: 0 auto;
border-radius: 10px;
}
.home-button {
padding: 12px 30px;
border-radius: 30px;
font-weight: 600;
}
@media (max-width: 767px) {
.header-title {
font-size: 1.5rem;
}
.success-icon {
width: 80px;
height: 80px;
}
}
</style>
{{ end }}
{{ define "content" }}
<!-- Header -->
<header class="bg-light py-2 mb-4">
<div class="container">
<div class="row align-items-center">
<div class="col-9">
<h1 class="district-name header-title mb-0">[District Name]</h1>
</div>
<div class="col-3 text-end">
<img
src="placeholder-logo.png"
alt="District Logo"
class="district-logo"
/>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container mb-5 py-4">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card shadow confirmation-card">
<div class="card-body p-4 p-md-5 text-center">
<!-- Success Icon -->
<div class="success-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="50"
height="50"
fill="currentColor"
class="bi bi-check-lg"
viewBox="0 0 16 16"
>
<path
d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"
/>
</svg>
</div>
<h2 class="mb-4">Report Received!</h2>
<p class="lead mb-4">
Thank you for contributing to the health and well-being of our
community.
</p>
<div class="mb-4 p-3 bg-light rounded-3">
<p class="mb-0">
Your mosquito report has been submitted successfully and will be
reviewed by our team. Your effort helps us identify problem
areas and better manage mosquito populations throughout our
district.
</p>
</div>
<p class="text-muted mb-4">
Report ID:
<span class="fw-bold"
>#MM
<script>
document.write(Math.floor(Math.random() * 10000) + 10000);
</script></span
>
</p>
<a href="/" class="btn btn-primary home-button"> Return Home </a>
</div>
</div>
<!-- Additional Info Card -->
<div class="card mt-4 border-0 bg-light">
<div class="card-body">
<h5>What happens next?</h5>
<p class="mb-0">
Our team reviews all reports daily. Depending on the nature of
your report, we may deploy field technicians to assess the area or
add it to our scheduled mosquito control activities. For urgent
matters, we prioritize responses based on public health risk
factors.
</p>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container">
<div class="row">
<div class="col-md-6">
<p class="mb-0 small">
&copy; 2023 [District Name] Mosquito Management District
</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0 small">Contact: (555) 123-4567</p>
</div>
</div>
</div>
</footer>
{{ end }}

View file

@ -0,0 +1,233 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.district-logo {
max-height: 60px;
width: auto;
}
.photo-upload-area {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 20px;
text-align: center;
margin-bottom: 20px;
background-color: #f9f9f9;
}
.photo-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.photo-preview img {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.submit-btn {
padding: 15px 0;
font-size: 1.25rem;
border-radius: 8px;
}
.location-info {
background-color: #e9f5ff;
border-radius: 8px;
padding: 12px;
margin-bottom: 20px;
font-size: 0.9rem;
}
@media (max-width: 767px) {
.header-title {
font-size: 1.5rem;
}
}
</style>
{{ end }}
{{ define "content" }}
<!-- Header -->
<header class="bg-light py-2 mb-4">
<div class="container">
<div class="row align-items-center">
<div class="col-9">
<h1 class="district-name header-title mb-0">[District Name]</h1>
</div>
<div class="col-3 text-end">
<img
src="placeholder-logo.png"
alt="District Logo"
class="district-logo"
/>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="container mb-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-body p-4">
<h2 class="card-title text-center mb-4">Quick Mosquito Report</h2>
<!-- Form -->
<form action="/service-request-quick-confirmation" method="get">
<!-- Location Automatic Collection Note -->
<div class="location-info d-flex align-items-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-geo-alt me-2"
viewBox="0 0 16 16"
>
<path
d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A31.493 31.493 0 0 1 8 14.58a31.481 31.481 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94zM8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10z"
/>
<path
d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
/>
</svg>
<span
>Your location and current time will be automatically
collected with your report.</span
>
</div>
<!-- Photo Upload -->
<div class="mb-4">
<label for="photos" class="form-label fw-bold"
>Photos (Optional)</label
>
<div class="photo-upload-area">
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
class="bi bi-camera mb-2"
viewBox="0 0 16 16"
>
<path
d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1v6zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2z"
/>
<path
d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5zm0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0z"
/>
</svg>
<div>
<input
type="file"
id="photos"
class="d-none"
accept="image/*"
multiple
/>
<button
type="button"
class="btn btn-outline-primary mb-2"
onclick="document.getElementById('photos').click()"
>
Add Photos
</button>
</div>
<small class="d-block text-muted"
>Take pictures of the mosquito problem area</small
>
<!-- Photo Preview Area (would be populated by JavaScript) -->
<div class="photo-preview mt-3">
<!-- Example preview images (these would be dynamically added) -->
<img
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80' fill='%23dddddd'%3E%3Crect width='80' height='80' fill='%23eeeeee'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='12' fill='%23999999'%3EPreview%3C/text%3E%3C/svg%3E"
alt="Preview"
/>
</div>
</div>
</div>
<!-- Comments -->
<div class="mb-4">
<label for="comments" class="form-label fw-bold"
>Comments</label
>
<textarea
class="form-control"
id="comments"
rows="4"
placeholder="Describe the mosquito issue (e.g., standing water, high mosquito activity, time of day they're most active)"
></textarea>
</div>
<!-- Submit Button -->
<button
type="submit"
class="btn btn-success w-100 submit-btn mt-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
class="bi bi-send-fill me-2"
viewBox="0 0 16 16"
>
<path
d="M15.964.686a.5.5 0 0 0-.65-.65L.767 5.855H.766l-.452.18a.5.5 0 0 0-.082.887l.41.26.001.002 4.995 3.178 3.178 4.995.002.002.26.41a.5.5 0 0 0 .886-.083l6-15Zm-1.833 1.89L6.637 10.07l-.215-.338a.5.5 0 0 0-.154-.154l-.338-.215 7.494-7.494 1.178-.471-.47 1.178Z"
/>
</svg>
Submit Report
</button>
</form>
</div>
</div>
<!-- Back Link -->
<div class="text-center mt-3">
<a href="javascript:history.back()" class="text-decoration-none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-left me-1"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
Back to home
</a>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container">
<div class="row">
<div class="col-md-6">
<p class="mb-0 small">
&copy; 2023 [District Name] Mosquito Management District
</p>
</div>
<div class="col-md-6 text-md-end">
<p class="mb-0 small">Contact: (555) 123-4567</p>
</div>
</div>
</div>
</footer>
{{ end }}

View file

@ -0,0 +1,222 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.option-card {
transition: transform 0.3s;
height: 100%;
}
.option-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.district-logo {
max-height: 80px;
width: auto;
}
.divider {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.divider-line {
border-left: 1px solid #dee2e6;
height: 80%;
}
@media (max-width: 767.98px) {
.divider-line {
border-left: none;
border-top: 1px solid #dee2e6;
width: 80%;
height: auto;
margin: 2rem 0;
}
}
</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">{{ .DistrictName }}</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>
<!-- Page Title -->
<section class="py-4 bg-primary text-white">
<div class="container">
<h2 class="text-center mb-0">Check Status or Follow-up</h2>
</div>
</section>
<!-- Lookup Options -->
<section class="py-5">
<div class="container">
<div class="row">
<div class="col-12 mb-4">
<p class="lead text-center">
Choose one of the following options to check on mosquito activity
or follow up on a previous report.
</p>
</div>
</div>
<div class="row g-4">
<!-- Report ID Lookup -->
<div class="col-md-5">
<div class="card option-card h-100">
<div class="card-body p-4">
<div class="text-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-file-earmark-text"
viewBox="0 0 16 16"
>
<path
d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"
/>
<path
d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"
/>
</svg>
</div>
<h4 class="card-title text-center mb-4">
Look up by Report ID
</h4>
<p class="card-text">
If you have a report ID from a previous request, enter it
below to view the details and current status.
</p>
<form>
<div class="mb-3">
<label for="reportId" class="form-label">Report ID</label>
<input
type="text"
class="form-control"
id="reportId"
name="reportId"
placeholder="Enter your report ID"
required
/>
<div class="form-text">Example: MMD-2023-12345</div>
</div>
<div class="d-grid gap-2">
<a
href="/service-request/abc-123"
type="submit"
class="btn btn-primary"
>View Report Details</a
>
</div>
</form>
</div>
</div>
</div>
<!-- Divider for visual separation -->
<div class="col-md-2 divider">
<div class="divider-line"></div>
</div>
<!-- Location Lookup -->
<div class="col-md-5">
<div class="card option-card h-100">
<div class="card-body p-4">
<div class="text-center mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-geo-alt"
viewBox="0 0 16 16"
>
<path
d="M12.166 8.94c-.524 1.062-1.234 2.12-1.96 3.07A31.493 31.493 0 0 1 8 14.58a31.481 31.481 0 0 1-2.206-2.57c-.726-.95-1.436-2.008-1.96-3.07C3.304 7.867 3 6.862 3 6a5 5 0 0 1 10 0c0 .862-.305 1.867-.834 2.94zM8 16s6-5.686 6-10A6 6 0 0 0 2 6c0 4.314 6 10 6 10z"
/>
<path
d="M8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 1a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
/>
</svg>
</div>
<h4 class="card-title text-center mb-4">Look up by Location</h4>
<p class="card-text">
Don't have a report ID? You can check mosquito activity and
reports in your area by providing your location information.
</p>
<p class="card-text mb-4">
This option will guide you through selecting your location to
find relevant information about mosquito activity near you.
</p>
<div class="d-grid gap-2 mt-auto">
<a href="/service-request-location" class="btn btn-primary"
>Search by Location</a
>
</div>
</div>
</div>
</div>
</div>
<!-- Back button -->
<div class="row mt-5">
<div class="col-12 text-center">
<a href="/" class="btn btn-outline-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-left me-2"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"
/>
</svg>
Back to Home
</a>
</div>
</div>
</div>
</section>
</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 {{ .DistrictName }} 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

@ -0,0 +1,230 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.service-card {
transition: transform 0.3s;
height: 100%;
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.district-logo {
max-height: 80px;
width: auto;
}
.quick-report-mobile {
background-color: #ff9800;
}
.quick-report-desktop {
background-color: #ffefd5;
border-left: 4px solid #ff9800;
}
</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">{{ .DistrictName }}</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>
<!-- Introduction Section -->
<section class="py-5 bg-primary text-white">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<h2 class="text-center mb-4">
Welcome to Our Mosquito Management Services
</h2>
<p class="lead text-center">
We are dedicated to protecting public health and improving quality
of life by reducing mosquito populations and the diseases they can
carry. Our district provides comprehensive mosquito surveillance,
control, and education services to our community.
</p>
</div>
</div>
</div>
</section>
<!-- Quick Report for Mobile - Only visible on small screens -->
<section class="py-3 quick-report-mobile d-md-none">
<div class="container">
<div class="row">
<div class="col-12 text-center">
<h4 class="mb-2">On the go?</h4>
<a href="/mock/service-request-quick" class="btn btn-dark btn-lg"
>Make a Quick Report</a
>
<p class="mb-0 mt-2">
<small>Report mosquito issues in under 60 seconds</small>
</p>
</div>
</div>
</div>
</section>
<!-- Services Section -->
<section class="py-5">
<div class="container">
<h3 class="text-center mb-4">How Can We Help You Today?</h3>
<div class="row g-4">
<!-- Service Option 1 -->
<div class="col-md-4">
<div class="card service-card h-100">
<div class="card-body text-center">
<div class="mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-search"
viewBox="0 0 16 16"
>
<path
d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z"
/>
</svg>
</div>
<h4 class="card-title">Follow-up or Check Status</h4>
<p class="card-text">
Check on a previous request or view current mosquito activity
in your area.
</p>
<a
href="/mock/service-request-updates"
class="btn btn-primary mt-3"
>Get Updates</a
>
</div>
</div>
</div>
<!-- Service Option 2 -->
<div class="col-md-4">
<div class="card service-card h-100">
<div class="card-body text-center">
<div class="mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-water"
viewBox="0 0 16 16"
>
<path
d="M.036 3.314a.5.5 0 0 1 .65-.278l1.757.703a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.757-.703a.5.5 0 1 1 .372.928l-1.758.703a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0L.664 3.964a.5.5 0 0 1-.278-.65zm0 3a.5.5 0 0 1 .65-.278l1.757.703a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.757-.703a.5.5 0 1 1 .372.928l-1.758.703a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0L.664 6.964a.5.5 0 0 1-.278-.65zm0 3a.5.5 0 0 1 .65-.278l1.757.703a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.014-.406a2.5 2.5 0 0 1 1.857 0l1.015.406a1.5 1.5 0 0 0 1.114 0l1.757-.703a.5.5 0 1 1 .372.928l-1.758.703a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0l-1.014-.406a1.5 1.5 0 0 0-1.114 0l-1.015.406a2.5 2.5 0 0 1-1.857 0L.664 9.964a.5.5 0 0 1-.278-.65z"
/>
</svg>
</div>
<h4 class="card-title">Report a Green Pool</h4>
<p class="card-text">
Report stagnant water sources like abandoned pools that may
breed mosquitoes.
</p>
<a
href="/mock/service-request-pool"
class="btn btn-primary mt-3"
>Report Source</a
>
</div>
</div>
</div>
<!-- Service Option 3 -->
<div class="col-md-4">
<div class="card service-card h-100">
<div class="card-body text-center">
<div class="mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
fill="currentColor"
class="bi bi-bug"
viewBox="0 0 16 16"
>
<path
d="M4.355.522a.5.5 0 0 1 .623.333l.291.956A4.979 4.979 0 0 1 8 1c1.007 0 1.946.298 2.731.811l.29-.956a.5.5 0 1 1 .957.29l-.41 1.352A4.985 4.985 0 0 1 13 6h.5a.5.5 0 0 0 .5-.5V5a.5.5 0 0 1 1 0v.5A1.5 1.5 0 0 1 13.5 7H13v1h1.5a.5.5 0 0 1 0 1H13v1h.5a1.5 1.5 0 0 1 1.5 1.5v.5a.5.5 0 1 1-1 0v-.5a.5.5 0 0 0-.5-.5H13a5 5 0 0 1-10 0h-.5a.5.5 0 0 0-.5.5v.5a.5.5 0 1 1-1 0v-.5A1.5 1.5 0 0 1 2.5 10H3V9H1.5a.5.5 0 0 1 0-1H3V7h-.5A1.5 1.5 0 0 1 1 5.5V5a.5.5 0 0 1 1 0v.5a.5.5 0 0 0 .5.5H3c0-1.364.547-2.601 1.432-3.503l-.41-1.352a.5.5 0 0 1 .333-.623zM4 7v4a4 4 0 0 0 3.5 3.97V7H4zm4.5 0v7.97A4 4 0 0 0 12 11V7H8.5zM12 6a3.989 3.989 0 0 0-1.334-2.982A3.983 3.983 0 0 0 8 2a3.983 3.983 0 0 0-2.667 1.018A3.989 3.989 0 0 0 4 6h8z"
/>
</svg>
</div>
<h4 class="card-title">Report Mosquito Nuisance</h4>
<p class="card-text">
Report areas with high adult mosquito activity causing
discomfort or concern.
</p>
<a
href="/mock/service-request-mosquito"
class="btn btn-primary mt-3"
>Report Problem</a
>
</div>
</div>
</div>
</div>
<!-- Quick Report for Desktop - Only visible on medium screens and up -->
<div class="row mt-4 d-none d-md-block">
<div class="col-12">
<div class="card quick-report-desktop">
<div class="card-body py-3">
<div class="row align-items-center">
<div class="col-md-8">
<h5 class="mb-1">Need to make a quick report?</h5>
<p class="mb-0">
Use our streamlined form to report mosquito issues in
under 60 seconds
</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<a
href="/mock/service-request-quick"
class="btn btn-warning"
>Quick Report</a
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</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 {{ .DistrictName }}</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

@ -0,0 +1,367 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.integration-card {
border-left: 5px solid #0d6efd;
transition: all 0.2s;
}
.integration-card:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.integration-card.fieldseeker {
border-left-color: #0d6efd;
}
.integration-card.vectorsurv {
border-left-color: #198754;
}
.integration-card.veemac {
border-left-color: #dc3545;
}
.status-active {
color: #198754;
}
.status-inactive {
color: #dc3545;
}
.token-display {
font-family: monospace;
background-color: #f8f9fa;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid #dee2e6;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-4">
<!-- Header -->
<div class="mb-4">
<h1>Integrations</h1>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Important:</strong> This page allows you to configure
integration with third-party services. The credentials and tokens stored
here provide access to external systems and should be protected. Only
authorized personnel should modify these settings.
</div>
</div>
<!-- FieldSeeker GIS Integration -->
<div class="card mb-4 integration-card fieldseeker">
<div
class="card-header d-flex justify-content-between align-items-center"
>
<div>
<h2 class="h5 mb-0">Frontier Precision's FieldSeeker GIS</h2>
</div>
<img
src="https://via.placeholder.com/100x40?text=FieldSeeker"
alt="FieldSeeker Logo"
height="40"
/>
</div>
<div class="card-body">
<div class="table-responsive mb-3">
<table class="table table-borderless">
<tbody>
<tr>
<td width="30%"><strong>OAuth Token Status</strong></td>
<td>
<span class="status-active">
<i class="bi bi-check-circle-fill me-1"></i> Active
</span>
</td>
</tr>
<tr>
<td><strong>Token Expiration</strong></td>
<td>26 days remaining (Expires on Dec 31, 2025)</td>
</tr>
<tr>
<td><strong>Integration Method</strong></td>
<td>Web Hooks</td>
</tr>
<tr>
<td><strong>Permission Level</strong></td>
<td>Read & Write</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary">
<i class="bi bi-arrow-repeat me-2"></i>Refresh OAuth Token
</button>
<button class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Delete Token
</button>
</div>
</div>
</div>
<!-- VectorSurv Integration -->
<div class="card mb-4 integration-card vectorsurv">
<div
class="card-header d-flex justify-content-between align-items-center"
>
<div>
<h2 class="h5 mb-0">VectorSurv</h2>
</div>
<img
src="https://via.placeholder.com/100x40?text=VectorSurv"
alt="VectorSurv Logo"
height="40"
/>
</div>
<div class="card-body">
<div class="table-responsive mb-3">
<table class="table table-borderless">
<tbody>
<tr>
<td width="30%"><strong>API Token</strong></td>
<td>
<span class="token-display"
>vs_9f72b5e3******************************c11d</span
>
</td>
</tr>
<tr>
<td><strong>Last Synchronization</strong></td>
<td>December 5, 2025 at 08:34 AM (2 days ago)</td>
</tr>
<tr>
<td><strong>Synchronization Status</strong></td>
<td>
<span class="status-active">
<i class="bi bi-check-circle-fill me-1"></i> Active
(Scheduled daily at 2:00 AM)
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex gap-2">
<button
class="btn btn-success"
data-bs-toggle="modal"
data-bs-target="#vectorsurvModal"
>
<i class="bi bi-pencil-square me-2"></i>Edit Token
</button>
<button class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Remove Integration
</button>
</div>
</div>
</div>
<!-- VeeMac Integration -->
<div class="card mb-4 integration-card veemac">
<div
class="card-header d-flex justify-content-between align-items-center"
>
<div>
<h2 class="h5 mb-0">VeeMac</h2>
</div>
<img
src="https://via.placeholder.com/100x40?text=VeeMac"
alt="VeeMac Logo"
height="40"
/>
</div>
<div class="card-body">
<div class="table-responsive mb-3">
<table class="table table-borderless">
<tbody>
<tr>
<td width="30%"><strong>Username</strong></td>
<td>mosquito_district21</td>
</tr>
<tr>
<td><strong>Password</strong></td>
<td>••••••••••••</td>
</tr>
<tr>
<td><strong>Last Synchronization</strong></td>
<td>December 6, 2025 at 11:15 PM (Yesterday)</td>
</tr>
<tr>
<td><strong>Synchronization Status</strong></td>
<td>
<span class="status-inactive">
<i class="bi bi-x-circle-fill me-1"></i> Inactive (Manual
sync only)
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex gap-2">
<button
class="btn btn-success"
data-bs-toggle="modal"
data-bs-target="#veemacModal"
>
<i class="bi bi-pencil-square me-2"></i>Edit Credentials
</button>
<button class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Remove Integration
</button>
</div>
</div>
</div>
</div>
<!-- VectorSurv Edit Token Modal -->
<div
class="modal fade"
id="vectorsurvModal"
tabindex="-1"
aria-labelledby="vectorsurvModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="vectorsurvModalLabel">
Edit VectorSurv API Token
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="vectorsurvToken" class="form-label">API Token</label>
<input
type="text"
class="form-control"
id="vectorsurvToken"
value="vs_9f72b5e3c8a1d492f6b7e54321098c11d"
/>
<div class="form-text">
You can find this token in your VectorSurv account settings.
</div>
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="vectorsurvSyncCheck"
checked
/>
<label class="form-check-label" for="vectorsurvSyncCheck"
>Enable automatic synchronization</label
>
</div>
<div class="mb-3">
<label for="vectorsurvSyncTime" class="form-label"
>Sync Time</label
>
<input
type="time"
class="form-control"
id="vectorsurvSyncTime"
value="02:00"
/>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="button" class="btn btn-success">Save Changes</button>
</div>
</div>
</div>
</div>
<!-- VeeMac Edit Credentials Modal -->
<div
class="modal fade"
id="veemacModal"
tabindex="-1"
aria-labelledby="veemacModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="veemacModalLabel">
Edit VeeMac Credentials
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label for="veemacUsername" class="form-label">Username</label>
<input
type="text"
class="form-control"
id="veemacUsername"
value="mosquito_district21"
/>
</div>
<div class="mb-3">
<label for="veemacPassword" class="form-label">Password</label>
<input
type="password"
class="form-control"
id="veemacPassword"
value="password123"
/>
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
id="veemacSyncCheck"
/>
<label class="form-check-label" for="veemacSyncCheck"
>Enable automatic synchronization</label
>
</div>
<div class="mb-3">
<label for="veemacSyncFrequency" class="form-label"
>Sync Frequency</label
>
<select class="form-select" id="veemacSyncFrequency" disabled>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="hourly">Hourly</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="button" class="btn btn-danger">Save Changes</button>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,218 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.settings-card {
transition:
transform 0.2s,
box-shadow 0.2s;
height: 100%;
}
.settings-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.settings-icon {
font-size: 2.5rem;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
margin-bottom: 1.5rem;
}
.icon-users {
color: #6f42c1;
background-color: rgba(111, 66, 193, 0.1);
}
.icon-pesticides {
color: #198754;
background-color: rgba(25, 135, 84, 0.1);
}
.icon-integrations {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.1);
}
.icon-notifications {
color: #fd7e14;
background-color: rgba(253, 126, 20, 0.1);
}
.icon-general {
color: #6c757d;
background-color: rgba(108, 117, 125, 0.1);
}
.icon-equipment {
color: #dc3545;
background-color: rgba(220, 53, 69, 0.1);
}
.last-updated {
font-size: 0.8rem;
color: #6c757d;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-5">
<div class="row mb-4">
<div class="col">
<h1 class="display-5 mb-3">Settings</h1>
<p class="text-muted lead">
Configure your organization's preferences and integrations
</p>
</div>
</div>
<div class="row g-4">
<!-- User Management Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-users">
<i class="bi bi-people-fill"></i>
</div>
<h2 class="h4 mb-2">User Management</h2>
<p class="text-muted mb-4">
Manage staff accounts, roles, and permissions for your
organization.
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="{{ .URLs.SettingUser }}" class="btn btn-outline-primary">
Manage Users
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">23 users</span>
</div>
</div>
</div>
</div>
<!-- Pesticide Products Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-pesticides">
<i class="bi bi-droplet-fill"></i>
</div>
<h2 class="h4 mb-2">Pesticide Products</h2>
<p class="text-muted mb-4">
Configure products, application rates, and field recommendations.
</p>
<div class="d-flex justify-content-between align-items-center">
<a
href="{{ .URLs.SettingPesticide }}"
class="btn btn-outline-success"
>
Manage Products
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">12 active products</span>
</div>
</div>
</div>
</div>
<!-- Integrations Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-integrations">
<i class="bi bi-gear-wide-connected"></i>
</div>
<h2 class="h4 mb-2">Integrations</h2>
<p class="text-muted mb-4">
Configure connections with FieldSeeker, VectorSurv, and other
services.
</p>
<div class="d-flex justify-content-between align-items-center">
<a
href="{{ .URLs.SettingIntegration }}"
class="btn btn-outline-primary"
>
Manage Integrations
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">3 active connections</span>
</div>
</div>
</div>
</div>
<!-- Equipment Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-equipment">
<i class="bi bi-tools"></i>
</div>
<h2 class="h4 mb-2">Equipment</h2>
<p class="text-muted mb-4">
Manage your field equipment inventory, calibration, and
maintenance.
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="equipment.html" class="btn btn-outline-danger">
Manage Equipment
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">Updated 5 days ago</span>
</div>
</div>
</div>
</div>
<!-- Notifications Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-notifications">
<i class="bi bi-bell-fill"></i>
</div>
<h2 class="h4 mb-2">Notifications</h2>
<p class="text-muted mb-4">
Configure email alerts, SMS notifications, and reporting
preferences.
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="notifications.html" class="btn btn-outline-warning">
Manage Notifications
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">5 active alerts</span>
</div>
</div>
</div>
</div>
<!-- General Settings Card -->
<div class="col-12 col-md-6 col-lg-4">
<div class="card settings-card border-0 shadow-sm">
<div class="card-body p-4">
<div class="settings-icon icon-general">
<i class="bi bi-sliders"></i>
</div>
<h2 class="h4 mb-2">General Settings</h2>
<p class="text-muted mb-4">
Configure organization details, branding, and system preferences.
</p>
<div class="d-flex justify-content-between align-items-center">
<a href="general-settings.html" class="btn btn-outline-secondary">
Manage Settings
<i class="bi bi-arrow-right ms-1"></i>
</a>
<span class="last-updated">Updated yesterday</span>
</div>
</div>
</div>
</div>
</div>
<div class="mt-5 text-center text-muted">
<p class="small">
<i class="bi bi-shield-lock me-1"></i>
All changes made in settings are logged for audit purposes
</p>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,253 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.section-heading {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #dee2e6;
}
.info-label {
font-weight: 600;
color: #495057;
}
.target-icon {
display: inline-block;
width: 30px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 50%;
font-size: 14px;
font-weight: bold;
margin-right: 4px;
color: white;
}
.target-active {
background-color: #0d6efd;
}
.target-inactive {
background-color: #dee2e6;
color: #6c757d;
}
.tag {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.8rem;
margin: 0.2rem;
border-radius: 30px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
font-size: 0.9rem;
}
.tag i {
margin-right: 0.5rem;
}
.tag-enabled {
background-color: #d1e7dd;
color: #0f5132;
border-color: #a3cfbb;
}
.tag-ppe {
background-color: #e2e3e5;
color: #41464b;
border-color: #d3d6d8;
}
.tag-equipment {
background-color: #cff4fc;
color: #055160;
border-color: #9eeaf9;
}
.tag-suitability {
background-color: #fff3cd;
color: #664d03;
border-color: #ffecb5;
}
.tag-optional {
background-color: #f8f9fa;
color: #6c757d;
border-color: #dee2e6;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-4">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Settings</a></li>
<li class="breadcrumb-item">
<a href="pesticide-config.html">Pesticide</a>
</li>
<li class="breadcrumb-item active" aria-current="page">VectoMax FG</li>
</ol>
</nav>
<!-- Main Content -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<!-- Product Header -->
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h1 class="mb-2">VectoMax FG</h1>
<p class="text-muted mb-0">
Biological larvicide granules combining Bacillus thuringiensis
subspecies israelensis and Bacillus sphaericus for extended
residual control of mosquito larvae.
</p>
</div>
<span class="tag tag-enabled">
<i class="bi bi-check-circle-fill"></i> Enabled
</span>
</div>
<!-- General Information -->
<div class="mb-4">
<h2 class="section-heading">General Information</h2>
<div class="row g-3">
<div class="col-md-6 col-lg-4">
<div class="info-label">Formulation</div>
<div>Granule</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="info-label">EPA Registration Number</div>
<div>73049-429</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="info-label">Active Ingredients</div>
<div>
Bacillus thuringiensis subspecies israelensis (2.7%)<br />
Bacillus sphaericus (4.5%)
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="info-label">Biological Targeting</div>
<div class="mt-1">
<span class="target-icon target-active" title="Instar Stage 1"
>I1</span
>
<span class="target-icon target-active" title="Instar Stage 2"
>I2</span
>
<span class="target-icon target-active" title="Instar Stage 3"
>I3</span
>
<span class="target-icon target-active" title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-inactive" title="Pupae">P</span>
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="info-label">Application Rates</div>
<div>
Low: 5 lbs/acre<br />
High: 20 lbs/acre
</div>
</div>
<div class="col-md-6 col-lg-4">
<div class="info-label">Residual</div>
<div>Up to 30 days (environmental conditions dependent)</div>
</div>
</div>
</div>
<!-- Usage Notes -->
<div class="alert alert-info mb-4">
<div class="d-flex">
<div class="me-3">
<i class="bi bi-info-circle-fill fs-4"></i>
</div>
<div>
<h5 class="alert-heading">Key Usage Notes</h5>
<p class="mb-0">
Apply evenly across water surface. Use higher rate when L4
present or when organic load is high. Avoid application in ponds
with fish unless approved by a supervisor.
</p>
</div>
</div>
</div>
<!-- PPE Requirements -->
<div class="mb-4">
<h2 class="section-heading">PPE Requirements</h2>
<div>
<span class="tag tag-ppe">
<i class="bi bi-hand-thumbs-up"></i> Gloves
</span>
<span class="tag tag-ppe">
<i class="bi bi-eyeglasses"></i> Eye Protection
</span>
<span class="tag tag-optional">
<i class="bi bi-mask"></i> Respirator (Optional)
</span>
</div>
</div>
<!-- Equipment Supported -->
<div class="mb-4">
<h2 class="section-heading">Equipment Supported</h2>
<div>
<span class="tag tag-equipment">
<i class="bi bi-backpack"></i> Backpack Spreader
</span>
<span class="tag tag-equipment">
<i class="bi bi-hand-index-thumb"></i> Hand Spreader
</span>
<span class="tag tag-equipment">
<i class="bi bi-truck"></i> Truck Granule Unit
</span>
</div>
</div>
<!-- Suitability -->
<div class="mb-4">
<h2 class="section-heading">Suitability</h2>
<div class="row g-3">
<div class="col-md-6 col-lg-3">
<div class="info-label">Pools</div>
<div><span class="badge bg-success">Recommended</span></div>
</div>
<div class="col-md-6 col-lg-3">
<div class="info-label">Vegetation</div>
<div><span class="badge bg-info text-dark">OK</span></div>
</div>
<div class="col-md-6 col-lg-3">
<div class="info-label">High Organics</div>
<div><span class="badge bg-info text-dark">OK</span></div>
</div>
<div class="col-md-6 col-lg-3">
<div class="info-label">Organic Crop Restriction</div>
<div><span class="badge bg-secondary">None</span></div>
</div>
</div>
</div>
<!-- Actions -->
<div class="d-flex justify-content-between mt-5 pt-3 border-top">
<button class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i> Remove from Inventory
</button>
<button class="btn btn-success">
<i class="bi bi-plus-circle me-2"></i> Add to Allowed Inventory
</button>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,293 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.target-icon {
display: inline-block;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
margin-right: 2px;
color: white;
}
.target-active {
background-color: #0d6efd;
}
.target-inactive {
background-color: #dee2e6;
color: #6c757d;
}
.table-responsive {
overflow-x: auto;
}
th {
white-space: nowrap;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">Pesticide Products Configuration</h1>
<a
href="{{ .URLs.SettingPesticideAdd }}"
class="btn btn-primary"
id="addProductBtn"
>
<i class="bi bi-plus-circle me-2"></i>Add New Product
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover align-middle">
<thead class="table-light">
<tr>
<th>Product</th>
<th>Formulation</th>
<th>Targets</th>
<th>Residual (days)</th>
<th>Low Rate</th>
<th>Max Rate</th>
<th>Pools</th>
<th>Info</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Sample pesticide data -->
<tr>
<td><strong>BVA Oil</strong></td>
<td>Liquid</td>
<td>
<span
class="target-icon target-inactive"
title="Instar Stage 1"
>I1</span
>
<span
class="target-icon target-inactive"
title="Instar Stage 2"
>I2</span
>
<span
class="target-icon target-inactive"
title="Instar Stage 3"
>I3</span
>
<span
class="target-icon target-inactive"
title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-active" title="Pupae">P</span>
</td>
<td>1</td>
<td>0.5 gal/acre</td>
<td>5 gal/acre</td>
<td><span class="badge bg-success">Recommended</span></td>
<td>
<a
href="product-details.html?id=bva-oil"
class="btn btn-sm btn-info"
title="Product Information"
>
<i class="bi bi-info-circle"></i>
</a>
</td>
<td>
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<tr>
<td><strong>VectoMax FG</strong></td>
<td>Granule</td>
<td>
<span class="target-icon target-active" title="Instar Stage 1"
>I1</span
>
<span class="target-icon target-active" title="Instar Stage 2"
>I2</span
>
<span class="target-icon target-active" title="Instar Stage 3"
>I3</span
>
<span class="target-icon target-active" title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-inactive" title="Pupae"
>P</span
>
</td>
<td>30</td>
<td>5 lbs/acre</td>
<td>20 lbs/acre</td>
<td><span class="badge bg-success">Recommended</span></td>
<td>
<a
href="product-details.html?id=vectomax-fg"
class="btn btn-sm btn-info"
title="Product Information"
>
<i class="bi bi-info-circle"></i>
</a>
</td>
<td>
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<tr>
<td><strong>Censor</strong></td>
<td>Liquid</td>
<td>
<span class="target-icon target-active" title="Instar Stage 1"
>I1</span
>
<span class="target-icon target-active" title="Instar Stage 2"
>I2</span
>
<span class="target-icon target-active" title="Instar Stage 3"
>I3</span
>
<span class="target-icon target-active" title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-inactive" title="Pupae"
>P</span
>
</td>
<td>21</td>
<td>0.75 gal/acre</td>
<td>2.5 gal/acre</td>
<td><span class="badge bg-warning text-dark">Allowed</span></td>
<td>
<a
href="product-details.html?id=censor"
class="btn btn-sm btn-info"
title="Product Information"
>
<i class="bi bi-info-circle"></i>
</a>
</td>
<td>
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<tr>
<td><strong>AquaBac XT</strong></td>
<td>Liquid</td>
<td>
<span class="target-icon target-active" title="Instar Stage 1"
>I1</span
>
<span class="target-icon target-active" title="Instar Stage 2"
>I2</span
>
<span class="target-icon target-active" title="Instar Stage 3"
>I3</span
>
<span
class="target-icon target-inactive"
title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-inactive" title="Pupae"
>P</span
>
</td>
<td>14</td>
<td>0.25 gal/acre</td>
<td>2 gal/acre</td>
<td><span class="badge bg-danger">Prohibited</span></td>
<td>
<a
href="product-details.html?id=aquabac-xt"
class="btn btn-sm btn-info"
title="Product Information"
>
<i class="bi bi-info-circle"></i>
</a>
</td>
<td>
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
<tr>
<td><strong>Natular G30</strong></td>
<td>Granule</td>
<td>
<span class="target-icon target-active" title="Instar Stage 1"
>I1</span
>
<span class="target-icon target-active" title="Instar Stage 2"
>I2</span
>
<span class="target-icon target-active" title="Instar Stage 3"
>I3</span
>
<span class="target-icon target-active" title="Instar Stage 4"
>I4</span
>
<span class="target-icon target-inactive" title="Pupae"
>P</span
>
</td>
<td>30</td>
<td>5 lbs/acre</td>
<td>12 lbs/acre</td>
<td><span class="badge bg-secondary">Discouraged</span></td>
<td>
<a
href="product-details.html?id=natular-g30"
class="btn btn-sm btn-info"
title="Product Information"
>
<i class="bi bi-info-circle"></i>
</a>
</td>
<td>
<button class="btn btn-sm btn-primary" title="Edit">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" title="Delete">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,161 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.form-check-input.switch-lg {
width: 3em;
height: 1.5em;
}
.required-field::after {
content: " *";
color: red;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow">
<div class="card-header bg-white">
<div class="d-flex justify-content-between align-items-center">
<h3 class="mb-0">Add New User</h3>
<a
href="user-management.html"
class="btn btn-outline-secondary btn-sm"
>
<i class="bi bi-arrow-left me-1"></i> Back to Users
</a>
</div>
</div>
<div class="card-body">
<form id="addUserForm" class="needs-validation" novalidate>
<!-- Full Name -->
<div class="mb-3">
<label for="fullName" class="form-label required-field"
>Full Name</label
>
<input
type="text"
class="form-control"
id="fullName"
required
/>
<div class="invalid-feedback">
Please provide the user's full name.
</div>
</div>
<!-- Email Address -->
<div class="mb-3">
<label for="emailAddress" class="form-label required-field"
>Email Address</label
>
<input
type="email"
class="form-control"
id="emailAddress"
required
/>
<div class="invalid-feedback">
Please provide a valid email address.
</div>
<div class="form-text">
An invitation will be sent to this email address.
</div>
</div>
<!-- Username -->
<div class="mb-3">
<label for="username" class="form-label required-field"
>Username</label
>
<input
type="text"
class="form-control"
id="username"
required
/>
<div class="invalid-feedback">Please provide a username.</div>
<div class="form-text">
Username must be unique and contain only letters, numbers, and
underscores.
</div>
</div>
<div class="row">
<!-- Role -->
<div class="col-md-6 mb-3">
<label for="userRole" class="form-label required-field"
>Role</label
>
<select class="form-select" id="userRole" required>
<option value="" selected disabled>Select a role</option>
<option value="lead">Lead</option>
<option value="technician">Technician</option>
<option value="administrator">Administrator</option>
</select>
<div class="invalid-feedback">Please select a role.</div>
</div>
<!-- Initial Status -->
<div class="col-md-6 mb-3">
<label for="initialStatus" class="form-label"
>Initial Status</label
>
<select class="form-select" id="initialStatus">
<option value="invited" selected>Invited</option>
<option value="active">Active</option>
</select>
</div>
</div>
<!-- Permissions -->
<div class="mb-4">
<label class="form-label d-block">Permissions</label>
<div class="form-check form-switch">
<input
class="form-check-input switch-lg"
type="checkbox"
id="serveWarrants"
/>
<label class="form-check-label" for="serveWarrants">
Can serve warrants
</label>
</div>
</div>
<!-- Send welcome email checkbox -->
<div class="mb-4">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="sendWelcomeEmail"
checked
/>
<label class="form-check-label" for="sendWelcomeEmail">
Send welcome email with login instructions
</label>
</div>
</div>
<hr />
<!-- Form actions -->
<div class="d-flex justify-content-end gap-2">
<a href="user-management.html" class="btn btn-secondary">
Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-person-plus me-1"></i> Add User
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,168 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.form-check-input.switch-lg {
width: 3em;
height: 1.5em;
}
.status-badge {
width: 100px;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container-fluid p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="mb-0">User Management</h1>
<a
href="{{ .URLs.SettingUserAdd }}"
class="btn btn-primary"
id="addUserBtn"
>
<i class="bi bi-plus-circle me-2"></i>Add New User
</a>
</div>
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-light">
<tr>
<th>Full Name</th>
<th>Email Address</th>
<th>Username</th>
<th>Role</th>
<th>Serve Warrants</th>
<th>Status</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Sample user data -->
<tr>
<td>John Doe</td>
<td>john.doe@example.com</td>
<td>johndoe</td>
<td>
<select class="form-select form-select-sm">
<option>Lead</option>
<option selected>Technician</option>
<option>Administrator</option>
</select>
</td>
<td>
<div class="form-check form-switch">
<input
class="form-check-input switch-lg"
type="checkbox"
checked
/>
</div>
</td>
<td>
<span class="badge bg-success status-badge">Active</span>
</td>
<td>2023-06-15 09:45 AM</td>
<td>
<button class="btn btn-sm btn-warning" title="Deactivate">
<i class="bi bi-person-x"></i>
</button>
</td>
</tr>
<tr>
<td>Jane Smith</td>
<td>jane.smith@example.com</td>
<td>janesmith</td>
<td>
<select class="form-select form-select-sm">
<option selected>Lead</option>
<option>Technician</option>
<option>Administrator</option>
</select>
</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input switch-lg" type="checkbox" />
</div>
</td>
<td>
<span class="badge bg-success status-badge">Active</span>
</td>
<td>2023-06-17 14:20 PM</td>
<td>
<button class="btn btn-sm btn-warning" title="Deactivate">
<i class="bi bi-person-x"></i>
</button>
</td>
</tr>
<tr>
<td>Robert Johnson</td>
<td>robert.j@example.com</td>
<td>robertj</td>
<td>
<select class="form-select form-select-sm">
<option>Lead</option>
<option>Technician</option>
<option selected>Administrator</option>
</select>
</td>
<td>
<div class="form-check form-switch">
<input
class="form-check-input switch-lg"
type="checkbox"
checked
/>
</div>
</td>
<td>
<span class="badge bg-secondary status-badge"
>Deactivated</span
>
</td>
<td>2023-06-10 11:30 AM</td>
<td>
<button class="btn btn-sm btn-success" title="Activate">
<i class="bi bi-person-check"></i>
</button>
</td>
</tr>
<tr>
<td>Emily Wilson</td>
<td>emily.w@example.com</td>
<td>emilyw</td>
<td>
<select class="form-select form-select-sm">
<option>Lead</option>
<option selected>Technician</option>
<option>Administrator</option>
</select>
</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input switch-lg" type="checkbox" />
</div>
</td>
<td>
<span class="badge bg-warning text-dark status-badge"
>Invited</span
>
</td>
<td>Never</td>
<td>
<button class="btn btn-sm btn-info" title="Resend Invitation">
<i class="bi bi-envelope"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,8 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ end }}
{{ define "content" }}
<p>Imagine settings here</p>
{{ end }}

View file

@ -0,0 +1,105 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
.login-container {
max-width: 900px;
margin: 0 auto;
}
.login-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
overflow: hidden;
}
.login-form-section {
padding: 40px;
}
.product-info-section {
padding: 40px;
background-color: #f8f9fa;
}
.login-header {
margin-bottom: 25px;
}
</style>
{{ end }}
{{ define "content" }}
<div
class="container min-vh-100 d-flex align-items-center justify-content-center py-5"
>
<div class="login-container">
<div class="row login-box g-0">
<!-- Left side: Login Form -->
<div class="col-md-6 login-form-section">
<div class="login-header">
<h2>Welcome Back</h2>
<p class="text-muted">Please enter your credentials</p>
</div>
<form method="POST" action="/signin">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input
type="text"
class="form-control"
name="username"
required
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
name="password"
required
/>
</div>
{{ if .InvalidCredentials }}
<div class="alert alert-danger" role="alert">
The credentials you provided weren't recognized.
</div>
{{ end }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>
<div class="mt-3 text-center">
<p>Don't have an account? <a href="/signup">Sign up</a></p>
<a href="forgot-password.html">Forgot password?</a>
</div>
</form>
</div>
<!-- Right side: Product Information -->
<div class="col-md-6 product-info-section">
<div>
<img src="/static/img/nidus-logo-256-transparent.png" />
<h2>Nidus Sync</h2>
<p class="lead mb-4">
All your field data, sync'd to all your techs
</p>
<div class="mb-4">
<p>Something intelligent and intriguing</p>
</div>
<div class="mb-4">
<h5>Key Features</h5>
<ul>
<li>Works with <b>Fieldseeker</b></li>
<li>Works <i>with</i> Fieldseeker</li>
<li><b>Works</b> with Fieldseeker</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,131 @@
{{ template "sync/base.html" . }}
{{ define "title" }}Login{{ end }}
{{ define "extraheader" }}
<style>
.register-container {
max-width: 900px;
margin: 0 auto;
}
.register-box {
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
border-radius: 10px;
overflow: hidden;
}
.register-form-section {
padding: 40px;
}
.register-info-section {
padding: 40px;
background-color: #f8f9fa;
}
.register-header {
margin-bottom: 25px;
}
.logo-area {
text-align: center;
margin-bottom: 30px;
}
</style>
{{ end }}
{{ define "content" }}
<div
class="container min-vh-100 d-flex align-items-center justify-content-center py-5"
>
<div class="register-container">
<div class="row register-box g-0">
<!-- Left side: Registration Form -->
<div class="col-md-6 register-form-section">
<div class="register-header">
<h2>Create an Account</h2>
<p class="text-muted">Join us today to get started</p>
</div>
<form method="POST" action="/signup">
<div class="mb-3">
<label for="name" class="form-label">Display Name</label>
<input type="text" class="form-control" name="name" required />
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input
type="username"
class="form-control"
name="username"
required
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
name="password"
required
/>
</div>
<div class="mb-3 form-check">
<input
type="checkbox"
class="form-check-input"
name="terms"
required
/>
<label class="form-check-label" for="terms"
>I agree to the <a href="#">Terms of Service</a> and
<a href="#">Privacy Policy</a></label
>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Register</button>
</div>
<div class="mt-3 text-center">
<p>Already have an account? <a href="/signin">Sign in</a></p>
</div>
</form>
</div>
<!-- Right side: Account Information -->
<div class="col-md-6 register-info-section">
<div>
<div class="logo-area">
<img src="/static/img/nidus-logo-256-transparent.png" />
</div>
<div class="mb-4">
<h5>Who should register?</h5>
<p>
This platform is designed for professionals who need to manage
projects and collaborate with team members. Whether you're a
freelancer, small business owner, or part of a larger
organization, our tools can help streamline your workflow.
</p>
</div>
<div class="mb-4">
<h5>What happens after registration?</h5>
<p>
After you register with your email, you'll receive a
confirmation message with instructions to complete your account
setup. You'll then have access to all features and can customize
your workspace based on your specific needs.
</p>
</div>
<div class="mb-3">
<small class="text-muted"
>For any questions about account types or registration, please
contact our support team at support@yourproduct.com</small
>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,367 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ template "map" .MapData }}
<style>
.info-table {
width: 100%;
}
.info-table td {
padding: 5px 0;
}
.info-label {
font-weight: 600;
vertical-align: top;
}
.map-container {
background-color: #e9ecef;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin-bottom: 20px;
}
.section-header {
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.source-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.time-delta-positive {
color: #dc3545; /* red for late */
}
.time-delta-negative {
color: #28a745; /* green for early */
}
.time-delta-neutral {
color: #6c757d; /* gray for on time */
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 mb-5">
<!-- Source Header Section -->
<div class="row mb-2">
<div class="col-12">
<h1>Breeding Source Detail</h1>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<div class="source-info">
<div class="row">
<div class="col-md-6">
<div class="source-id">Source ID: {{ .Source.GlobalID }}</div>
<table class="info-table">
<tr>
<td class="info-label">Access:</td>
<td>{{ .Source.AccessDescription }}</td>
</tr>
<tr>
<td class="info-label">Address:</td>
<td>Not implemented</td>
</tr>
<tr>
<td class="info-label">Comments:</td>
<td>{{ .Source.Comments }}</td>
</tr>
<tr>
<td class="info-label">Deactivate Reason:</td>
<td>{{ .Source.DeactivateReason }}</td>
</tr>
<tr>
<td class="info-label">Description:</td>
<td>{{ .Source.Description }}</td>
</tr>
<tr>
<td class="info-label">Habitat:</td>
<td>{{ .Source.Habitat }}</td>
</tr>
<tr>
<td class="info-label">Jurisdiction:</td>
<td>{{ .Source.Jurisdiction }}</td>
</tr>
<tr>
<td class="info-label">Location Number:</td>
<td>{{ .Source.LocationNumber }}</td>
</tr>
<tr>
<td class="info-label">Name:</td>
<td>{{ .Source.Name }}</td>
</tr>
<tr>
<td>Status</td>
<td>
{{ if .Source.Active }}
<span class="badge bg-warning">Active</span>
{{ else }}
<span class="badge bg-info">Inactive</span>
{{ end }}
</td>
</tr>
<tr>
<td class="info-label">Priority:</td>
<td>{{ .Source.Priority }} ({{ .Source.ScalarPriority }})</td>
</tr>
<tr>
<td class="info-label">S Type:</td>
<td>{{ .Source.SourceType }}</td>
</tr>
<tr>
<td class="info-label">Source Status:</td>
<td>{{ .Source.SourceStatus }}</td>
</tr>
<tr>
<td class="info-label">Symbology:</td>
<td>{{ .Source.Symbology }}</td>
</tr>
<tr>
<td class="info-label">Use Type:</td>
<td>{{ .Source.UseType }}</td>
</tr>
<tr>
<td class="info-label">Water Origin:</td>
<td>{{ .Source.WaterOrigin }}</td>
</tr>
<tr>
<td class="info-label">Zone:</td>
<td>{{ .Source.Zone }}.{{ .Source.Zone2 }}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="info-table">
<tr>
<td class="info-label">Creation date</td>
<td>{{ .Source.Created|timeSincePtr }}</td>
</tr>
<tr>
<td class="info-label">Edit date</td>
<td>{{ .Source.EditedAt|timeSincePtr }}</td>
</tr>
<tr>
<td class="info-label">Larva Inspect Interval</td>
<td>{{ .Source.LarvaeInspectInterval }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Activity</td>
<td>{{ .Source.LastInspectionActivity }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Avg Larva</td>
<td>{{ .Source.LastInspectionAverageLarvae }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Avg Pupae</td>
<td>{{ .Source.LastInspectionAveragePupae }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Breeding</td>
<td>{{ .Source.LastInspectionBreeding }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Conditions</td>
<td>{{ .Source.LastInspectionConditions }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Date</td>
<td>{{ .Source.LastInspectionDate|timeSincePtr }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Species</td>
<td>{{ .Source.LastInspectionFieldSpecies }}</td>
</tr>
<tr>
<td class="info-label">Last Inspect Life Stages</td>
<td>{{ .Source.LastInspectionLifeStages }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Activity</td>
<td>{{ .Source.LastTreatmentActivity }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Date</td>
<td>{{ .Source.LastTreatmentDate|timeSincePtr }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Product</td>
<td>{{ .Source.LastTreatmentProduct }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Quantity</td>
<td>{{ .Source.LastTreatmentQuantity }}</td>
</tr>
<tr>
<td class="info-label">Last Treat Quantity Unit</td>
<td>{{ .Source.LastTreatmentQuantityUnit }}</td>
</tr>
<tr>
<td class="info-label">Next action date scheduled:</td>
<td>{{ .Source.NextActionScheduledDate|timeSincePtr }}</td>
</tr>
<tr>
<td class="info-label">Treatment Cadence:</td>
<td>Not implemented</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Map Section -->
<div class="row">
<div class="col-12">
<div class="map-container">
<div id="map"></div>
</div>
</div>
</div>
<!-- Two-Column Layout for Tables -->
<div class="row">
<!-- Left Column -->
<div class="col-md-6">
<!-- Treatments Section -->
<h2 class="section-header">Treatment History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<tbody>
<tr>
<th>Year</th>
<th>Start</th>
<th>End</th>
<th>Interval</th>
</tr>
{{ range .TreatmentModels }}
<tr>
<td>{{ .Year }}</td>
<td>{{ .SeasonStart|timeAsRelativeDate }}</td>
<td>{{ .SeasonEnd|timeAsRelativeDate }}</td>
<td>{{ .Interval|timeInterval }}</td>
</tr>
{{ end }}
</tbody>
</table>
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Treatment Date</th>
<th>Insecticide Used</th>
<th>Cadence Delta</th>
<th>Technician Notes</th>
</tr>
</thead>
<tbody>
{{ range .Treatments }}
<tr>
<td>{{ .Date|timeSincePtr }}</td>
<td>{{ .Product }}</td>
<td class="time-delta-neutral">
{{ .CadenceDelta|timeDelta }}
</td>
<td>{{ .Notes }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right Column -->
<div class="col-md-6">
<!-- Inspections Section -->
<h2 class="section-header">Inspection History</h2>
<div class="card mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Inspection Date</th>
<th>Action</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
{{ range .Inspections }}
<tr>
<td>{{ .Date|timeSincePtr }}</td>
<td>{{ .Action }}</td>
<td>{{ .Notes }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<h2 class="section-header">Nearby Mosquito Traps</h2>
{{ range .Traps }}
<div class="trap-info">
<table class="info-table">
<tbody>
<tr>
<td class="info-label">Trap ID:</td>
<td><a href="/trap/{{ .ID }}">{{ .ID }}</a></td>
</tr>
<tr>
<td class="info-label">Distance</td>
<td>{{ .Distance }}</td>
</tr>
</tbody>
</table>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Collection Date</th>
<th>Female Count</th>
<th>Male Count</th>
<th>Total Count</th>
</tr>
</thead>
<tbody>
<!-- Trap 1 with multiple collections -->
{{ range .Counts }}
<tr>
<td>{{ .Ended|timeSincePtr }}</td>
<td>{{ .Females }}</td>
<td>{{ .Males }}</td>
<td>{{ .Total }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,3 @@
<html>
<h1>Hi Eli</h1>
</html>

View file

@ -0,0 +1,181 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
<style>
.chat-container {
height: 80vh;
overflow-y: auto;
}
.message {
max-width: 75%;
margin-bottom: 10px;
border-radius: 15px;
}
.sender-message {
background-color: #f0f0f0;
}
.receiver-message {
background-color: #d1e7ff;
}
.message-time {
font-size: 0.7rem;
color: #6c757d;
margin-top: 3px;
}
.avatar {
width: 35px;
height: 35px;
border-radius: 50%;
}
</style>
{{ end }}
{{ define "content" }}
<div class="container py-4">
<!-- Chat Header -->
<div class="row mb-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body d-flex align-items-center">
<div class="me-3">
<img
src="https://via.placeholder.com/40"
class="avatar"
alt="User avatar"
/>
</div>
<div>
<h5 class="mb-0">Chat with Sarah Johnson</h5>
<small class="text-muted">Last active 5 minutes ago</small>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Messages -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body chat-container">
<!-- Date Divider -->
<div class="text-center mb-4">
<span class="badge bg-light text-dark">Today, 2:30 PM</span>
</div>
<!-- Receiver Message -->
<div class="d-flex mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar me-2 align-self-end"
alt="Receiver avatar"
/>
<div>
<div class="message sender-message p-3">
Hi there! How's the project coming along?
</div>
<div class="message-time">2:31 PM</div>
</div>
</div>
<!-- Sender Message -->
<div class="d-flex flex-row-reverse mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar ms-2 align-self-end"
alt="Sender avatar"
/>
<div class="d-flex flex-column align-items-end">
<div class="message receiver-message p-3">
Hey! It's going pretty well. I'm working on the UI mockups
right now.
</div>
<div class="message-time">2:33 PM</div>
</div>
</div>
<!-- Receiver Message -->
<div class="d-flex mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar me-2 align-self-end"
alt="Receiver avatar"
/>
<div>
<div class="message sender-message p-3">
That's great to hear! When do you think you'll be able to
share them with the team?
</div>
<div class="message-time">2:35 PM</div>
</div>
</div>
<!-- Sender Message -->
<div class="d-flex flex-row-reverse mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar ms-2 align-self-end"
alt="Sender avatar"
/>
<div class="d-flex flex-column align-items-end">
<div class="message receiver-message p-3">
I'm hoping to have something ready by tomorrow afternoon. I'm
just working out some details with the responsive design.
</div>
<div class="message-time">2:36 PM</div>
</div>
</div>
<!-- Sender Message -->
<div class="d-flex flex-row-reverse mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar ms-2 align-self-end"
alt="Sender avatar"
/>
<div class="d-flex flex-column align-items-end">
<div class="message receiver-message p-3">
Do you have any specific feedback on the initial concept I
shared last week?
</div>
<div class="message-time">2:37 PM</div>
</div>
</div>
<!-- Receiver Message -->
<div class="d-flex mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar me-2 align-self-end"
alt="Receiver avatar"
/>
<div>
<div class="message sender-message p-3">
Yes! The team loved it. The color scheme was particularly well
received. We just had some minor suggestions about the
navigation that I can share during our next call.
</div>
<div class="message-time">2:40 PM</div>
</div>
</div>
<!-- Sender Message -->
<div class="d-flex flex-row-reverse mb-4">
<img
src="https://via.placeholder.com/40"
class="avatar ms-2 align-self-end"
alt="Sender avatar"
/>
<div class="d-flex flex-column align-items-end">
<div class="message receiver-message p-3">
That sounds great! Looking forward to the feedback.
</div>
<div class="message-time">2:41 PM</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -0,0 +1,131 @@
{{ template "sync/authenticated.html" . }}
{{ define "title" }}Dash{{ end }}
{{ define "extraheader" }}
{{ template "map" .MapData }}
<style>
.info-table {
width: 100%;
}
.info-table td {
padding: 5px 0;
}
.info-label {
font-weight: 600;
vertical-align: top;
}
.map-container {
background-color: #e9ecef;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
margin-bottom: 20px;
}
.section-header {
margin-top: 30px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #dee2e6;
}
.source-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.time-delta-positive {
color: #dc3545; /* red for late */
}
.time-delta-negative {
color: #28a745; /* green for early */
}
.time-delta-neutral {
color: #6c757d; /* gray for on time */
}
</style>
{{ end }}
{{ define "content" }}
<div class="container mt-4 mb-5">
<!-- Trap Header Section -->
<div class="row mb-2">
<div class="col-12">
<h1>Trap Detail</h1>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<div class="source-info">
<div class="row">
<div class="col-md-6">
<div class="source-id">Trap ID: {{ .Trap.GlobalID }}</div>
<table class="info-table">
<tr>
<td class="info-label">Active:</td>
<td>{{ .Trap.Active }}</td>
</tr>
<tr>
<td class="info-label">Comments:</td>
<td>{{ .Trap.Comments }}</td>
</tr>
<tr>
<td class="info-label">Description:</td>
<td>{{ .Trap.Description }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Map Section -->
<div class="row">
<div class="col-12">
<div class="map-container">
<div id="map"></div>
</div>
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<h2 class="section-header">Trap Collections</h2>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Collection Date</th>
<th>Collection ID</th>
<th>Females</th>
<th>Male</th>
<th>Total</th>
</tr>
</thead>
<tbody>
<!-- Trap 1 with multiple collections -->
{{ range .Trap.Collections }}
<tr>
<td>{{ .EndDateTime|timeSincePtr }}</td>
<td>{{ .GlobalID }}</td>
<td>{{ .Count.Females }}</td>
<td>{{ .Count.Males }}</td>
<td>{{ .Count.Total }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{ end }}

12
main.go
View file

@ -15,6 +15,7 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/comms/email" "github.com/Gleipnir-Technology/nidus-sync/comms/email"
"github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/llm" "github.com/Gleipnir-Technology/nidus-sync/llm"
"github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/Gleipnir-Technology/nidus-sync/platform/text"
"github.com/Gleipnir-Technology/nidus-sync/rmo" "github.com/Gleipnir-Technology/nidus-sync/rmo"
@ -75,16 +76,21 @@ func main() {
os.Exit(3) os.Exit(3)
} }
err = html.LoadTemplates()
if err != nil {
log.Error().Err(err).Msg("Failed to load html templates")
os.Exit(4)
}
err = email.LoadTemplates() err = email.LoadTemplates()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to load email templates") log.Error().Err(err).Msg("Failed to load email templates")
os.Exit(4) os.Exit(5)
} }
err = text.StoreSources() err = text.StoreSources()
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to store text source phone numbers") log.Error().Err(err).Msg("Failed to store text source phone numbers")
os.Exit(5) os.Exit(6)
} }
router_logger := log.With().Logger() router_logger := log.With().Logger()
@ -121,7 +127,7 @@ func main() {
err = llm.CreateOpenAIClient(ctx, &openai_logger) err = llm.CreateOpenAIClient(ctx, &openai_logger)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to start openAI client") log.Error().Err(err).Msg("Failed to start openAI client")
os.Exit(6) os.Exit(7)
} }
background.Start(ctx) background.Start(ctx)
server := &http.Server{ server := &http.Server{

View file

@ -22,10 +22,6 @@ type ContentDistrictList struct {
URL ContentURL URL ContentURL
} }
var (
DistrictListT = buildTemplate("district-list", "base")
)
func districtBySlug(r *http.Request) (*models.Organization, error) { func districtBySlug(r *http.Request) (*models.Organization, error) {
slug := chi.URLParam(r, "slug") slug := chi.URLParam(r, "slug")
district, err := models.Organizations.Query( district, err := models.Organizations.Query(
@ -49,7 +45,7 @@ func getDistrictList(w http.ResponseWriter, r *http.Request) {
} }
html.RenderOrError( html.RenderOrError(
w, w,
DistrictListT, "rmo/district-list.html",
ContentDistrictList{ ContentDistrictList{
Districts: districts, Districts: districts,
URL: makeContentURL(nil), URL: makeContentURL(nil),

View file

@ -15,13 +15,6 @@ type ContentEmail struct {
Email string Email string
} }
var (
EmailConfirmT = buildTemplate("email-confirm", "base")
EmailConfirmCompleteT = buildTemplate("email-confirm-complete", "base")
EmailUnsubscribeT = buildTemplate("email-unsubscribe", "base")
EmailUnsubscribeCompleteT = buildTemplate("email-unsubscribe-complete", "base")
)
func getEmailByCode(w http.ResponseWriter, r *http.Request) { func getEmailByCode(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "code") id := chi.URLParam(r, "code")
//id := r.FormValue("id") //id := r.FormValue("id")
@ -48,7 +41,7 @@ func getEmailReportUnsubscribe(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email") email := r.FormValue("email")
html.RenderOrError( html.RenderOrError(
w, w,
EmailConfirmT, "rmo/email-unsubscribe.html",
ContentEmail{ ContentEmail{
Email: email, Email: email,
}, },
@ -63,7 +56,7 @@ func getEmailConfirm(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
EmailConfirmT, "rmo/email-confirm.html",
ContentEmail{ ContentEmail{
Email: email, Email: email,
}, },
@ -72,7 +65,7 @@ func getEmailConfirm(w http.ResponseWriter, r *http.Request) {
func getEmailConfirmComplete(w http.ResponseWriter, r *http.Request) { func getEmailConfirmComplete(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
EmailConfirmCompleteT, "rmo/email-confirm-complete.html",
map[string]string{}, map[string]string{},
) )
} }
@ -80,7 +73,7 @@ func getEmailUnsubscribe(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email") email := r.FormValue("email")
html.RenderOrError( html.RenderOrError(
w, w,
EmailUnsubscribeT, "rmo/email-unsubscribe.html",
ContentEmail{ ContentEmail{
Email: email, Email: email,
}, },
@ -89,7 +82,7 @@ func getEmailUnsubscribe(w http.ResponseWriter, r *http.Request) {
func getEmailUnsubscribeComplete(w http.ResponseWriter, r *http.Request) { func getEmailUnsubscribeComplete(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
EmailUnsubscribeCompleteT, "rmo/email-unsubscribe-complete.html",
map[string]string{}, map[string]string{},
) )
} }

View file

@ -8,14 +8,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
var (
mockDistrictRootT = buildTemplate("mock/district-root", "base")
mockNuisanceT = buildTemplate("mock/nuisance", "base")
mockNuisanceSubmitCompleteT = buildTemplate("mock/nuisance-submit-complete", "base")
mockRootT = buildTemplate("mock/root", "base")
mockWaterT = buildTemplate("mock/water", "base")
)
type ContentMock struct { type ContentMock struct {
District ContentDistrict District ContentDistrict
MapboxToken string MapboxToken string
@ -24,13 +16,13 @@ type ContentMock struct {
} }
func addMockRoutes(r chi.Router) { func addMockRoutes(r chi.Router) {
r.Get("/", renderMock(mockRootT)) r.Get("/", renderMock("rmo/mock/root.html"))
r.Get("/district/{slug}", renderMock(mockDistrictRootT)) r.Get("/district/{slug}", renderMock("rmo/mock/district-root.html"))
r.Get("/district/{slug}/nuisance", renderMock(mockNuisanceT)) r.Get("/district/{slug}/nuisance", renderMock("rmo/mock/nuisance.html"))
r.Get("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) r.Get("/district/{slug}/nuisance-submit-complete", renderMock("rmo/mock/nuisance-submit-complete.html"))
r.Get("/district/{slug}/water", renderMock(mockWaterT)) r.Get("/district/{slug}/water", renderMock("rmo/mock/water.html"))
r.Get("/nuisance", renderMock(mockNuisanceT)) r.Get("/nuisance", renderMock("rmo/mock/nuisance.html"))
r.Get("/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) r.Get("/nuisance-submit-complete", renderMock("rmo/mock/nuisance-submit-complete.html"))
} }
func makeContentURLMock(slug string) ContentURL { func makeContentURLMock(slug string) ContentURL {
@ -44,7 +36,7 @@ func makeContentURLMock(slug string) ContentURL {
func makeURLMock(slug, p string) string { func makeURLMock(slug, p string) string {
return config.MakeURLReport("/mock/district/%s/%s", slug, p) return config.MakeURLReport("/mock/district/%s/%s", slug, p)
} }
func renderMock(t *html.BuiltTemplate) func(http.ResponseWriter, *http.Request) { func renderMock(t string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug") slug := chi.URLParam(r, "slug")
if slug == "" { if slug == "" {

View file

@ -10,10 +10,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
var (
registerNotificationsCompleteT = buildTemplate("register-notifications-complete", "base")
)
func postRegisterNotifications(w http.ResponseWriter, r *http.Request) { func postRegisterNotifications(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {

View file

@ -33,15 +33,10 @@ type ContentNuisanceSubmitComplete struct {
URL ContentURL URL ContentURL
} }
var (
NuisanceT = buildTemplate("nuisance", "base")
SubmitCompleteT = buildTemplate("submit-complete", "base")
)
func getNuisance(w http.ResponseWriter, r *http.Request) { func getNuisance(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
NuisanceT, "rmo/nuisance.html",
ContentNuisance{ ContentNuisance{
District: nil, District: nil,
MapboxToken: config.MapboxToken, MapboxToken: config.MapboxToken,
@ -57,7 +52,7 @@ func getNuisanceDistrict(w http.ResponseWriter, r *http.Request) {
} }
html.RenderOrError( html.RenderOrError(
w, w,
NuisanceT, "rmo/nuisance.html",
ContentNuisance{ ContentNuisance{
District: newContentDistrict(district), District: newContentDistrict(district),
MapboxToken: config.MapboxToken, MapboxToken: config.MapboxToken,
@ -74,7 +69,7 @@ func getSubmitComplete(w http.ResponseWriter, r *http.Request) {
} }
html.RenderOrError( html.RenderOrError(
w, w,
SubmitCompleteT, "rmo/submit-complete.html",
ContentNuisanceSubmitComplete{ ContentNuisanceSubmitComplete{
District: newContentDistrict(district), District: newContentDistrict(district),
ReportID: report_id, ReportID: report_id,

View file

@ -36,15 +36,10 @@ type District struct {
Name string Name string
} }
var (
quickT = buildTemplate("quick", "base")
quickSubmitCompleteT = buildTemplate("quick-submit-complete", "base")
)
func getQuick(w http.ResponseWriter, r *http.Request) { func getQuick(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
quickT, "rmo/quick.html",
ContentQuick{}, ContentQuick{},
) )
} }
@ -80,7 +75,7 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) {
} }
html.RenderOrError( html.RenderOrError(
w, w,
quickSubmitCompleteT, "rmo/quick-submit-complete.html",
ContentQuickSubmitComplete{ ContentQuickSubmitComplete{
District: district, District: district,
ReportID: report.PublicID, ReportID: report.PublicID,
@ -91,7 +86,7 @@ func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) {
report := r.URL.Query().Get("report") report := r.URL.Query().Get("report")
html.RenderOrError( html.RenderOrError(
w, w,
registerNotificationsCompleteT, "rmo/register-notificition-complete.html",
ContentRegisterNotificationsComplete{ ContentRegisterNotificationsComplete{
ReportID: report, ReportID: report,
}, },

View file

@ -30,12 +30,6 @@ type ContentURL struct {
WaterSubmit string WaterSubmit string
} }
var (
PrivacyT = buildTemplate("privacy", "base")
RootT = buildTemplate("root", "base")
TermsT = buildTemplate("terms", "base")
)
func boolFromForm(r *http.Request, k string) bool { func boolFromForm(r *http.Request, k string) bool {
s := r.PostFormValue(k) s := r.PostFormValue(k)
if s == "on" { if s == "on" {
@ -47,7 +41,7 @@ func boolFromForm(r *http.Request, k string) bool {
func getPrivacy(w http.ResponseWriter, r *http.Request) { func getPrivacy(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
PrivacyT, "rmo/privacy.html",
ContentPrivacy{ ContentPrivacy{
Address: "2726 S Quinn Ave, Gilbert, AZ, USA", Address: "2726 S Quinn Ave, Gilbert, AZ, USA",
Company: "Gleipnir LLC", Company: "Gleipnir LLC",
@ -59,7 +53,7 @@ func getPrivacy(w http.ResponseWriter, r *http.Request) {
func getRoot(w http.ResponseWriter, r *http.Request) { func getRoot(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
RootT, "rmo/root.html",
ContentRoot{ ContentRoot{
URL: makeContentURL(nil), URL: makeContentURL(nil),
}, },
@ -73,7 +67,7 @@ func getRootDistrict(w http.ResponseWriter, r *http.Request) {
} }
html.RenderOrError( html.RenderOrError(
w, w,
RootT, "rmo/root.html",
ContentRoot{ ContentRoot{
District: newContentDistrict(district), District: newContentDistrict(district),
URL: makeContentURL(district), URL: makeContentURL(district),
@ -88,7 +82,7 @@ func getRobots(w http.ResponseWriter, r *http.Request) {
func getTerms(w http.ResponseWriter, r *http.Request) { func getTerms(w http.ResponseWriter, r *http.Request) {
html.RenderOrError( html.RenderOrError(
w, w,
TermsT, "rmo/terms.html",
ContentRoot{ ContentRoot{
URL: makeContentURL(nil), URL: makeContentURL(nil),
}, },

Some files were not shown because too many files have changed in this diff Show more