Add server-side rendered setup UI accessible via `heatwave web`. The dashboard is now re-rendered per request and includes a nav bar linking to the new /setup page. Setup provides full CRUD for profiles, rooms, devices, occupants, AC units (with room assignment), scenario toggles, and forecast fetching — all via POST/redirect/GET forms. - Add ShowNav field to DashboardData for conditional nav bar - Extract fetchForecastForProfile() for reuse by web handler - Create setup.html.tmpl with Tailwind-styled entity sections - Create web_handlers.go with 15 route handlers and flash cookies - Switch web.go from pre-rendered to per-request dashboard rendering - Graceful dashboard fallback when no forecast data exists
109 lines
2.7 KiB
Go
109 lines
2.7 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
type Occupant struct {
|
|
ID int64
|
|
RoomID int64
|
|
Count int
|
|
ActivityLevel string
|
|
Vulnerable bool
|
|
}
|
|
|
|
func (s *Store) CreateOccupant(roomID int64, count int, activityLevel string, vulnerable bool) (*Occupant, error) {
|
|
if activityLevel == "" {
|
|
activityLevel = "sedentary"
|
|
}
|
|
vuln := 0
|
|
if vulnerable {
|
|
vuln = 1
|
|
}
|
|
res, err := s.db.Exec(
|
|
`INSERT INTO occupants (room_id, count, activity_level, vulnerable) VALUES (?, ?, ?, ?)`,
|
|
roomID, count, activityLevel, vuln,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create occupant: %w", err)
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
return s.GetOccupant(id)
|
|
}
|
|
|
|
func (s *Store) GetOccupant(id int64) (*Occupant, error) {
|
|
o := &Occupant{}
|
|
var vuln int
|
|
err := s.db.QueryRow(
|
|
`SELECT id, room_id, count, activity_level, vulnerable FROM occupants WHERE id = ?`, id,
|
|
).Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln)
|
|
if err == sql.ErrNoRows {
|
|
return nil, fmt.Errorf("occupant not found: %d", id)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get occupant: %w", err)
|
|
}
|
|
o.Vulnerable = vuln != 0
|
|
return o, nil
|
|
}
|
|
|
|
func (s *Store) ListOccupants(roomID int64) ([]Occupant, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT id, room_id, count, activity_level, vulnerable FROM occupants WHERE room_id = ?`, roomID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var occupants []Occupant
|
|
for rows.Next() {
|
|
var o Occupant
|
|
var vuln int
|
|
if err := rows.Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln); err != nil {
|
|
return nil, err
|
|
}
|
|
o.Vulnerable = vuln != 0
|
|
occupants = append(occupants, o)
|
|
}
|
|
return occupants, rows.Err()
|
|
}
|
|
|
|
func (s *Store) ListAllOccupants(profileID int64) ([]Occupant, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT o.id, o.room_id, o.count, o.activity_level, o.vulnerable
|
|
FROM occupants o JOIN rooms r ON o.room_id = r.id WHERE r.profile_id = ?`, profileID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var occupants []Occupant
|
|
for rows.Next() {
|
|
var o Occupant
|
|
var vuln int
|
|
if err := rows.Scan(&o.ID, &o.RoomID, &o.Count, &o.ActivityLevel, &vuln); err != nil {
|
|
return nil, err
|
|
}
|
|
o.Vulnerable = vuln != 0
|
|
occupants = append(occupants, o)
|
|
}
|
|
return occupants, rows.Err()
|
|
}
|
|
|
|
func (s *Store) UpdateOccupant(id int64, field, value string) error {
|
|
allowed := map[string]bool{"count": true, "activity_level": true, "vulnerable": true}
|
|
if !allowed[field] {
|
|
return fmt.Errorf("invalid field: %s", field)
|
|
}
|
|
_, err := s.db.Exec(
|
|
fmt.Sprintf(`UPDATE occupants SET %s = ? WHERE id = ?`, field), value, id,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Store) DeleteOccupant(id int64) error {
|
|
_, err := s.db.Exec(`DELETE FROM occupants WHERE id = ?`, id)
|
|
return err
|
|
}
|