2026-09-08 15:05:36 +02:00
|
|
|
package httpapi
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/kristianwind/verdande/internal/ai"
|
|
|
|
|
"github.com/kristianwind/verdande/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Dagens plan: én besked om morgenen om det, der venter i dag.
|
|
|
|
|
//
|
|
|
|
|
// Klokken siger, hvad andre har gjort. Det her siger, hvad der ligger foran en —
|
|
|
|
|
// og det er en anden slags besked: den kommer, fordi det er blevet morgen, ikke
|
|
|
|
|
// fordi nogen har rørt noget. Uden den skal man selv åbne listen for at opdage,
|
|
|
|
|
// at der lå tre ting og forfaldt i går.
|
|
|
|
|
//
|
|
|
|
|
// Sendt som en besked frem for en mail, fordi røret allerede findes: klokken,
|
|
|
|
|
// push til telefonen og mærket på ikonet er ét kald, og en mail mere i indbakken
|
|
|
|
|
// er præcis det, dette program findes for at slippe for.
|
|
|
|
|
|
|
|
|
|
const planScope = "dailyplan"
|
|
|
|
|
|
|
|
|
|
// planSettings er, hvad én person har bedt om.
|
|
|
|
|
//
|
|
|
|
|
// Slået fra som udgangspunkt. En besked, man ikke har bedt om, klokken syv om
|
|
|
|
|
// morgenen, er ikke en hjælp — og modsat beaconet er der ingen, der har brug for,
|
|
|
|
|
// at det her er tændt for at kunne gøre sit arbejde.
|
|
|
|
|
type planSettings struct {
|
|
|
|
|
Enabled bool `json:"enabled"`
|
|
|
|
|
// Hour er lokal tid hos personen selv. Et tal og ikke et klokkeslæt: planen
|
|
|
|
|
// sendes på timen, og et minuttal ville love en præcision, en time-tikkende
|
|
|
|
|
// baggrundsjob ikke har.
|
|
|
|
|
Hour int `json:"hour"`
|
|
|
|
|
// LastSent er datoen, den sidst blev sendt, som "2006-01-02" i personens egen
|
|
|
|
|
// tidszone. En dato og ikke et tidsstempel, fordi spørgsmålet er "har de fået
|
|
|
|
|
// dagens plan i dag" — og det er et spørgsmål om dagen dér, hvor de er.
|
|
|
|
|
LastSent string `json:"last_sent,omitempty"`
|
2026-09-09 10:22:27 +02:00
|
|
|
// Plan er teksten, som den blev sendt, og PlanDate den dag den gælder.
|
|
|
|
|
//
|
|
|
|
|
// Gemt frem for regnet forfra, hver gang nogen kigger. To grunde, og den anden
|
|
|
|
|
// er den vigtige: en model pr. sidevisning er dyr, og en plan, der siger noget
|
|
|
|
|
// andet end den besked, man fik i morges, er værre end begge dele hver for
|
|
|
|
|
// sig. Det er den samme tekst — ellers er det ikke *dagens plan*, men et nyt
|
|
|
|
|
// gæt hver gang man kigger.
|
|
|
|
|
//
|
|
|
|
|
// PlanAt er klokkeslættet, den blev lavet, så kortet kan sige det: en plan fra
|
|
|
|
|
// klokken syv er en anden slags oplysning klokken fire end klokken otte.
|
|
|
|
|
Plan string `json:"plan,omitempty"`
|
|
|
|
|
PlanDate string `json:"plan_date,omitempty"`
|
|
|
|
|
PlanAt int64 `json:"plan_at,omitempty"`
|
2026-09-08 15:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) planSettings(ctx context.Context, userID string) (planSettings, error) {
|
|
|
|
|
out := planSettings{Hour: 7}
|
|
|
|
|
stored, err := s.db.UserSettings(ctx, userID, planScope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return out, err
|
|
|
|
|
}
|
|
|
|
|
if v, ok := stored["enabled"].(bool); ok {
|
|
|
|
|
out.Enabled = v
|
|
|
|
|
}
|
|
|
|
|
if v, ok := stored["hour"].(float64); ok && v >= 0 && v <= 23 {
|
|
|
|
|
out.Hour = int(v)
|
|
|
|
|
}
|
|
|
|
|
if v, ok := stored["last_sent"].(string); ok {
|
|
|
|
|
out.LastSent = v
|
|
|
|
|
}
|
2026-09-09 10:22:27 +02:00
|
|
|
if v, ok := stored["plan"].(string); ok {
|
|
|
|
|
out.Plan = v
|
|
|
|
|
}
|
|
|
|
|
if v, ok := stored["plan_date"].(string); ok {
|
|
|
|
|
out.PlanDate = v
|
|
|
|
|
}
|
|
|
|
|
if v, ok := stored["plan_at"].(float64); ok {
|
|
|
|
|
out.PlanAt = int64(v)
|
|
|
|
|
}
|
2026-09-08 15:05:36 +02:00
|
|
|
return out, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) savePlanSettings(ctx context.Context, userID string, p planSettings) error {
|
|
|
|
|
return s.db.SetUserSettings(ctx, userID, planScope, map[string]any{
|
|
|
|
|
"enabled": p.Enabled, "hour": p.Hour, "last_sent": p.LastSent,
|
2026-09-09 10:22:27 +02:00
|
|
|
"plan": p.Plan, "plan_date": p.PlanDate, "plan_at": p.PlanAt,
|
2026-09-08 15:05:36 +02:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 10:22:27 +02:00
|
|
|
// handleGetPlanSettings er både indstillingen og dagens plan.
|
|
|
|
|
//
|
|
|
|
|
// Ét endepunkt, fordi det er én ting: planen *er* ressourcen, og indstillingen er,
|
|
|
|
|
// hvornår den bliver lavet. To ville betyde to kald fra siden, der viser den, og
|
|
|
|
|
// to steder at huske at rydde en plan fra i går.
|
|
|
|
|
//
|
|
|
|
|
// En plan fra i går sendes ikke med. Den er ikke forkert, den er bare ikke dagens
|
|
|
|
|
// — og et kort, der viser gårsdagens plan under overskriften "i dag", er værre end
|
|
|
|
|
// et tomt kort.
|
2026-09-08 15:05:36 +02:00
|
|
|
func (s *Server) handleGetPlanSettings(w http.ResponseWriter, r *http.Request) {
|
2026-09-09 10:22:27 +02:00
|
|
|
user := userFrom(r.Context())
|
|
|
|
|
p, err := s.planSettings(r.Context(), user.ID)
|
2026-09-08 15:05:36 +02:00
|
|
|
if err != nil {
|
|
|
|
|
s.internal(w, r, "plan settings", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-09 10:22:27 +02:00
|
|
|
if p.PlanDate != time.Now().In(userLocation(user.Timezone)).Format("2006-01-02") {
|
|
|
|
|
p.Plan, p.PlanDate, p.PlanAt = "", "", 0
|
|
|
|
|
}
|
2026-09-08 15:05:36 +02:00
|
|
|
writeJSON(w, http.StatusOK, p)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type planRequest struct {
|
|
|
|
|
Enabled *bool `json:"enabled"`
|
|
|
|
|
Hour *int `json:"hour"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleSetPlanSettings(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
var req planRequest
|
|
|
|
|
if err := decodeJSON(w, r, &req); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
user := userFrom(r.Context())
|
|
|
|
|
p, err := s.planSettings(r.Context(), user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.internal(w, r, "plan settings", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if req.Enabled != nil {
|
|
|
|
|
p.Enabled = *req.Enabled
|
|
|
|
|
}
|
|
|
|
|
if req.Hour != nil {
|
|
|
|
|
if *req.Hour < 0 || *req.Hour > 23 {
|
|
|
|
|
writeFieldErrors(w, map[string]string{"hour": "must be between 0 and 23"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
p.Hour = *req.Hour
|
|
|
|
|
// Timen er flyttet, så dagens plan er ikke sendt på den nye tid endnu.
|
|
|
|
|
// Uden det her ville en, der flytter planen fra 07 til 16, først få den i
|
|
|
|
|
// morgen — hvilket ligner, at indstillingen ikke virkede.
|
|
|
|
|
p.LastSent = ""
|
|
|
|
|
}
|
|
|
|
|
if err := s.savePlanSettings(r.Context(), user.ID, p); err != nil {
|
|
|
|
|
s.internal(w, r, "save plan settings", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, p)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-09 10:22:27 +02:00
|
|
|
type planNowRequest struct {
|
|
|
|
|
// Silent er kortet på I dag, der beder om en plan, mens man kigger på den.
|
|
|
|
|
//
|
|
|
|
|
// En besked om noget, man står og ser på, er en besked, der lærer folk at
|
|
|
|
|
// ignorere klokken. Indstillingssidens knap sender derimod rigtigt — det er
|
|
|
|
|
// hele dens formål: at vise, at push virker på det her apparat.
|
|
|
|
|
Silent bool `json:"silent"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handlePlanNow laver dagens plan med det samme, til den der beder om det.
|
2026-09-08 15:05:36 +02:00
|
|
|
//
|
|
|
|
|
// Findes, fordi en indstilling, man skal vente til i morgen for at se virkningen
|
2026-09-09 10:22:27 +02:00
|
|
|
// af, er en indstilling, ingen tør slå til — og fordi kortet på I dag skal kunne
|
|
|
|
|
// lave en plan for den, der har beskeden slået fra.
|
2026-09-08 15:05:36 +02:00
|
|
|
func (s *Server) handlePlanNow(w http.ResponseWriter, r *http.Request) {
|
2026-09-09 10:22:27 +02:00
|
|
|
var req planNowRequest
|
|
|
|
|
if r.ContentLength > 0 {
|
|
|
|
|
if err := decodeJSON(w, r, &req); err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-08 15:05:36 +02:00
|
|
|
user := userFrom(r.Context())
|
2026-09-09 10:22:27 +02:00
|
|
|
|
2026-09-08 15:05:36 +02:00
|
|
|
plan, err := s.buildPlan(r.Context(), user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.internal(w, r, "build plan", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if plan == "" {
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"plan": "", "sent": false})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-09 10:22:27 +02:00
|
|
|
if err := s.rememberPlan(r.Context(), user, plan); err != nil {
|
|
|
|
|
s.internal(w, r, "save plan", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sent := false
|
|
|
|
|
if !req.Silent {
|
|
|
|
|
s.notify(r, &store.Notification{
|
|
|
|
|
UserID: user.ID, Kind: "daily.plan",
|
|
|
|
|
Title: planTitle(user.Locale), Body: plan,
|
|
|
|
|
})
|
|
|
|
|
sent = true
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
"plan": plan, "sent": sent,
|
|
|
|
|
"plan_at": time.Now().Unix(),
|
2026-09-08 15:05:36 +02:00
|
|
|
})
|
2026-09-09 10:22:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rememberPlan gemmer dagens plan, så den kan ses igen uden at blive lavet igen.
|
|
|
|
|
func (s *Server) rememberPlan(ctx context.Context, user *store.User, plan string) error {
|
|
|
|
|
p, err := s.planSettings(ctx, user.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
p.Plan = plan
|
|
|
|
|
p.PlanDate = now.In(userLocation(user.Timezone)).Format("2006-01-02")
|
|
|
|
|
p.PlanAt = now.Unix()
|
|
|
|
|
return s.savePlanSettings(ctx, user.ID, p)
|
2026-09-08 15:05:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DailyPlans er timeslaget: hvem er der morgen hos, og hvem har ikke fået planen
|
|
|
|
|
// endnu i dag.
|
|
|
|
|
//
|
|
|
|
|
// Kaldt hver time frem for lagt på et klokkeslæt — samme form som backup og
|
|
|
|
|
// beacon, og af samme grund: en instans, der sov klokken syv, ville ellers
|
|
|
|
|
// springe dagen over. Og fordi klokken syv er syv forskellige steder: personens
|
|
|
|
|
// egen tidszone afgør, hvornår det er morgen hos dem.
|
|
|
|
|
func (s *Server) DailyPlans(ctx context.Context) error {
|
|
|
|
|
users, err := s.db.ListUsers(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, u := range users {
|
|
|
|
|
user, err := s.db.UserByID(ctx, u.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
p, err := s.planSettings(ctx, user.ID)
|
|
|
|
|
if err != nil || !p.Enabled {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
now := time.Now().In(userLocation(user.Timezone))
|
|
|
|
|
today := now.Format("2006-01-02")
|
|
|
|
|
if p.LastSent == today || now.Hour() < p.Hour {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Kun samme dag. En instans, der har været slukket i tre dage, skal ikke
|
|
|
|
|
// sende tre planer, når den kommer op — de to af dem er om dage, der er
|
|
|
|
|
// gået, og en plan for i forgårs er ikke en plan.
|
|
|
|
|
if now.Hour() > p.Hour+3 {
|
|
|
|
|
p.LastSent = today
|
|
|
|
|
_ = s.savePlanSettings(ctx, user.ID, p)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plan, err := s.buildPlan(ctx, user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.log.Warn("build daily plan", "err", err, "user", user.ID)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Ingen plan er ikke en besked. En morgen uden noget at gøre er en god
|
|
|
|
|
// morgen, og en besked, der siger "der er ingenting", lærer folk at lade
|
|
|
|
|
// være med at åbne beskeder.
|
|
|
|
|
if plan != "" {
|
|
|
|
|
s.notifyDirect(ctx, &store.Notification{
|
|
|
|
|
UserID: user.ID, Kind: "daily.plan",
|
|
|
|
|
Title: planTitle(user.Locale), Body: plan,
|
|
|
|
|
})
|
2026-09-09 10:22:27 +02:00
|
|
|
// Gemt, og gemt her frem for i rememberPlan: rækken er allerede
|
|
|
|
|
// læst ind i `p`, og to gemninger oven i hinanden ville lade den
|
|
|
|
|
// ene overskrive den andens `last_sent`.
|
|
|
|
|
p.Plan, p.PlanDate, p.PlanAt = plan, today, time.Now().Unix()
|
2026-09-08 15:05:36 +02:00
|
|
|
}
|
|
|
|
|
p.LastSent = today
|
|
|
|
|
if err := s.savePlanSettings(ctx, user.ID, p); err != nil {
|
|
|
|
|
s.log.Warn("save plan settings", "err", err, "user", user.ID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildPlan skriver dagens plan.
|
|
|
|
|
//
|
|
|
|
|
// Tallene kommer fra basen og sætningen fra modellen — i den rækkefølge. En model,
|
|
|
|
|
// der selv skulle tælle, ville af og til tælle forkert, og en plan, der siger fire
|
|
|
|
|
// opgaver, hvor der er tre, er værre end ingen plan. Er der ingen model sat op,
|
|
|
|
|
// står tallene alene, og det er stadig det, man havde brug for at vide.
|
|
|
|
|
func (s *Server) buildPlan(ctx context.Context, user *store.User) (string, error) {
|
|
|
|
|
loc := userLocation(user.Timezone)
|
|
|
|
|
now := time.Now().In(loc)
|
|
|
|
|
today := now.Format("2006-01-02")
|
|
|
|
|
// I går, ikke i dag: grænserne i filteret er inklusive, så "før i dag" skrevet
|
|
|
|
|
// som DueBefore: today ville tælle dagens egne opgaver med som sprunget over —
|
|
|
|
|
// og en plan, der klokken syv om morgenen siger, at dagens opgaver allerede er
|
|
|
|
|
// overskredet, er en plan, man holder op med at læse.
|
|
|
|
|
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
|
|
|
|
|
|
|
|
|
|
overdue, err := s.db.ListTasks(ctx, user.ID, store.TaskFilter{DueBefore: yesterday, Limit: 50})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
due, err := s.db.ListTasks(ctx, user.ID, store.TaskFilter{
|
|
|
|
|
DueFrom: today, DueBefore: today, Limit: 50,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if len(overdue) == 0 && len(due) == 0 {
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plain := plainPlan(len(due), len(overdue), user.Locale)
|
|
|
|
|
|
|
|
|
|
cfg, err := s.aiConfigFor(ctx, user)
|
|
|
|
|
if err != nil || !cfg.Configured() {
|
|
|
|
|
return plain, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lines := make([]string, 0, len(due)+len(overdue))
|
|
|
|
|
for _, t := range overdue {
|
|
|
|
|
lines = append(lines, "- "+t.Content+" ("+overdueWord(user.Locale)+" "+t.DueDate+")")
|
|
|
|
|
}
|
|
|
|
|
for _, t := range due {
|
|
|
|
|
lines = append(lines, "- "+t.Content)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
written, err := ai.New(cfg).DailyPlan(ctx, lines, len(due), len(overdue), user.Locale)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Modellen er ikke det, planen står og falder med. Tallene er rigtige uden
|
|
|
|
|
// den, og en morgen uden besked, fordi en API-nøgle er udløbet, er en
|
|
|
|
|
// dårligere handel end en besked uden en velskrevet sætning.
|
|
|
|
|
s.log.Info("daily plan without the model", "err", err, "user", user.ID)
|
|
|
|
|
return plain, nil
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(written), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// aiConfigFor er aiConfig uden en request bag sig — baggrundsjobbet har ingen.
|
|
|
|
|
//
|
|
|
|
|
// AllowPrivate er falsk her, og det er med vilje: retten til at nå det private
|
|
|
|
|
// net blev givet til en administrator, der sad og skrev adressen ind, og et job,
|
|
|
|
|
// der kører klokken syv om morgenen, er ikke nogen, der sidder og skriver. En
|
|
|
|
|
// model på maskinen selv skal derfor bruges gennem fladen, ikke af planen.
|
|
|
|
|
func (s *Server) aiConfigFor(ctx context.Context, user *store.User) (ai.Config, error) {
|
|
|
|
|
values, err := s.db.UserSettings(ctx, user.ID, "ai")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return ai.Config{}, err
|
|
|
|
|
}
|
|
|
|
|
str := func(key string) string {
|
|
|
|
|
v, _ := values[key].(string)
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
return ai.Config{
|
|
|
|
|
Provider: ai.Provider(str("provider")),
|
|
|
|
|
APIKey: str("api_key"),
|
|
|
|
|
BaseURL: str("base_url"),
|
|
|
|
|
Model: str("model"),
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// notifyDirect er notify uden en request. Baggrundsjobbet har ingen bruger i
|
|
|
|
|
// konteksten og ingen actor — planen er ikke noget, nogen har gjort ved en.
|
|
|
|
|
func (s *Server) notifyDirect(ctx context.Context, n *store.Notification) {
|
|
|
|
|
if err := s.db.CreateNotification(ctx, n); err != nil {
|
|
|
|
|
s.log.Warn("create notification", "err", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.hub.PublishToUser(n.UserID, "notification", notificationJSON{
|
|
|
|
|
ID: n.ID, Kind: n.Kind, Title: n.Title, Body: n.Body,
|
|
|
|
|
CreatedAt: n.CreatedAt.Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
go s.pushToUser(n.UserID, n.Title, n.Body, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func planTitle(locale string) string {
|
|
|
|
|
if strings.HasPrefix(strings.ToLower(locale), "en") {
|
|
|
|
|
return "Today"
|
|
|
|
|
}
|
|
|
|
|
return "I dag"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func overdueWord(locale string) string {
|
|
|
|
|
if strings.HasPrefix(strings.ToLower(locale), "en") {
|
|
|
|
|
return "was due"
|
|
|
|
|
}
|
|
|
|
|
return "forfaldt"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// plainPlan er planen uden en model: to tal, og ikke et ord mere.
|
|
|
|
|
//
|
|
|
|
|
// Det er ikke en nødløsning, det er bunden. Det, man har brug for at vide klokken
|
|
|
|
|
// syv, er om der er noget, der er sprunget over — og det tal er rigtigt, uanset
|
|
|
|
|
// om nogen har lagt en API-nøgle ind.
|
|
|
|
|
func plainPlan(due, overdue int, locale string) string {
|
|
|
|
|
english := strings.HasPrefix(strings.ToLower(locale), "en")
|
|
|
|
|
var parts []string
|
|
|
|
|
if due > 0 {
|
|
|
|
|
if english {
|
|
|
|
|
parts = append(parts, strconv.Itoa(due)+" due today")
|
|
|
|
|
} else {
|
|
|
|
|
parts = append(parts, strconv.Itoa(due)+" forfalder i dag")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if overdue > 0 {
|
|
|
|
|
if english {
|
|
|
|
|
parts = append(parts, strconv.Itoa(overdue)+" overdue")
|
|
|
|
|
} else {
|
|
|
|
|
parts = append(parts, strconv.Itoa(overdue)+" er sprunget over")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, ", ")
|
|
|
|
|
}
|