diff options
-rw-r--r-- | db.go | 4 | ||||
-rw-r--r-- | model.go | 13 | ||||
-rw-r--r-- | page.go | 61 | ||||
-rw-r--r-- | templates/index.html | 13 | ||||
-rw-r--r-- | templates/parts/entry.html | 37 | ||||
-rw-r--r-- | templates/parts/entryRows.html | 2 | ||||
-rw-r--r-- | templates/parts/taskEntry.html | 8 | ||||
-rw-r--r-- | templates/parts/taskRows.html | 17 | ||||
-rw-r--r-- | tidsreg.go | 160 |
9 files changed, 258 insertions, 57 deletions
@@ -70,7 +70,7 @@ func (db *InMemDb) StartNewEntry(now time.Time, task *Entry) (*Entry, error) { From: task.From, To: nil, Date: DateFromStd(now), - Tag: task.Tag, + Tasks: task.Tasks, Comment: task.Comment, } @@ -108,8 +108,8 @@ func (db *InMemDb) SaveEntry(entry *Entry) (*Entry, error) { existent := db.entries[entry.Id] existent.Comment = entry.Comment - existent.Tag = entry.Tag existent.From = entry.From + existent.Tasks = entry.Tasks existent.To = entry.To existent.Date = entry.Date return existent, nil @@ -12,7 +12,7 @@ type Entry struct { Date Date From *Time To *Time - Tag *string + Tasks map[int]*time.Duration Comment string } @@ -27,8 +27,13 @@ type Date string type Time string -var timeMatch = regexp.MustCompile("\\d\\d:\\d\\d") -var dateMatch = regexp.MustCompile("\\d\\d\\d\\d-\\d\\d-\\d\\d") +type EntrySubDuration string + +var ( + timeMatch = regexp.MustCompile("[0-9]{2}:[0-9]{2}") + dateMatch = regexp.MustCompile("[0-9]{4}-[0-9]{2}-[0-9]{2}") + subDurationMatch = regexp.MustCompile("[0-9]") +) var ( ErrInvalidTime = errors.New("Invalid time string") @@ -41,7 +46,7 @@ func NewEntry(date Date) *Entry { From: nil, Date: date, To: nil, - Tag: nil, + Tasks: map[int]*time.Duration{}, Comment: "", } @@ -1,6 +1,8 @@ package main -import "time" +import ( + "time" +) type Service struct { Db Database @@ -14,10 +16,12 @@ type DateInfo struct { } type EntryPage struct { - Entry *Entry - Detached bool - Tracking *Entry - DateInfo DateInfo + Entry *Entry + Detached bool + Tracking *Entry + DateInfo DateInfo + GroupToTasks map[string]([]*Task) + TaskCount int } type RootPage struct { @@ -25,11 +29,12 @@ type RootPage struct { Entry *EntryPage Entries []*Entry Task *TaskPage - Tasks []*Task + Tasks *TasksPage } type TasksPage struct { - Tasks []*Task + Tasks []*Task + GroupToTasks map[string]([]*Task) } type TaskPage struct { @@ -73,8 +78,26 @@ func (srv *Service) GetTasksPage() (*TasksPage, error) { if err != nil { return nil, err } + + mp := make(map[string]([]*Task)) + + for _, task := range tasks { + key := task.Group + if key == nil { + empty := "" + key = &empty + } + mparr, ok := mp[*key] + if !ok { + mparr = make([]*Task, 0) + } + mparr = append(mparr, task) + mp[*key] = mparr + } + return &TasksPage{ - Tasks: tasks, + Tasks: tasks, + GroupToTasks: mp, }, nil } @@ -83,11 +106,19 @@ func (srv *Service) GetEntryPage(detached *Entry, dt DateInfo) (*EntryPage, erro if err != nil { return nil, err } + + tasksPage, err := srv.GetTasksPage() + if err != nil { + return nil, err + } + page := &EntryPage{ - Entry: tracking, - Detached: false, - Tracking: tracking, - DateInfo: dt, + Entry: tracking, + Detached: false, + Tracking: tracking, + DateInfo: dt, + GroupToTasks: tasksPage.GroupToTasks, + TaskCount: 0, } if detached != nil { @@ -95,6 +126,10 @@ func (srv *Service) GetEntryPage(detached *Entry, dt DateInfo) (*EntryPage, erro page.Detached = true } + if page.Entry != nil { + page.TaskCount = len(page.Entry.Tasks) + } + return page, nil } @@ -115,7 +150,7 @@ func (srv *Service) GetRootPage(date Date) (*RootPage, error) { return nil, err } - tasks, err := srv.Db.GetTasks() + tasks, err := srv.GetTasksPage() if err != nil { return nil, err } diff --git a/templates/index.html b/templates/index.html index 18ca5f0..99bfc31 100644 --- a/templates/index.html +++ b/templates/index.html @@ -8,6 +8,7 @@ <!-- TODO REMOVE --> <script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.js" integrity="sha384-qbtR4rS9RrUMECUWDWM2+YGgN3U4V4ZncZ0BvUcg9FGct0jqXz3PUdVpU1p0yrXS" crossorigin="anonymous"></script> + <script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/response-targets.js"></script> <style> .flex { @@ -73,16 +74,22 @@ padding: 10px; max-width: 500px; } + +#error-box { + color: red; + font-weight: bold; +} </style> </head> - <body> + <body hx-ext="response-targets"> <div id="controls-bar"> <button hx-get="/" hx-replace-url="true" hx-params="date" hx-vals='{"date": "{{.DateInfo.Yesterday}}"}' hx-target="body">I går</button> <input type="date" hx-get="/" hx-replace-url="true" hx-params="date" hx-vals='js:{date: event.target.value}' autocomplete="off" hx-target="body" hx-trigger="change" value="{{.DateInfo.Date}}" /> <button hx-get="/" hx-replace-url="true" hx-params="date" hx-vals='{"date": "{{.DateInfo.Tomorrow}}"}' hx-target="body">I morgen</button> + <div id="error-box"></div> </div> - <div class="flex"> + <div class="flex" hx-target-*="#error-box"> <div id="entry-bar"> {{template "entry.html" .Entry}} <div> @@ -120,7 +127,7 @@ </select> </form> {{ template "task.html" .Task }} - {{ template "taskRows.html" . }} + {{ template "taskRows.html" .Tasks }} </div> </div> </body> diff --git a/templates/parts/entry.html b/templates/parts/entry.html index 52f2eb3..4be2073 100644 --- a/templates/parts/entry.html +++ b/templates/parts/entry.html @@ -16,17 +16,32 @@ </div> </div> <div class="entry-box"> - <b>Mærker</b><br> - <select> - <option value="-">-</option> - <option value="SVT-232">SVT-232</option> - <option value="Ferie">Ferie</option> - </select><br> - <select> - <option value="-">-</option> - <option value="SVT-232">SVT-232</option> - <option value="Ferie">Ferie</option> - </select> + <b>Opgaver</b><br> + <input readonly style="display: none;" name="task-count" value="{{ .TaskCount }}" /> + <table> + <tbody> + {{ if .Entry }} + {{ range $x := taskMapToArr .Entry.Tasks }} + <tr> + <td> + <select name="task-{{$x.Key}}-id" hx-trigger="changedTasks from:body" hx-get="/task/select?selected={{$x.Id}}"> + {{ template "taskEntry.html" arr $.GroupToTasks $x.Id }} + </select> + </td> + <td><input name="task-{{$x.Key}}-part" type="text" class="short" {{if $x.Sel}}value="{{$x.Sel}}"{{end}}/></td> + </tr> + {{ end }} + {{ end }} + <tr> + <td> + <select name="task-new-id" hx-trigger="changedTasks from:body" hx-get="/task/select"> + {{ template "taskEntry.html" (arr .GroupToTasks -1) }} + </select> + </td> + <td><input disabled name="task-new-part" type="text" class="short" /></td> + </tr> + </tbody> + </table> </div> <div class="entry-box"> <b>Kommentar</b><br> diff --git a/templates/parts/entryRows.html b/templates/parts/entryRows.html index 8f10ee3..3749ddb 100644 --- a/templates/parts/entryRows.html +++ b/templates/parts/entryRows.html @@ -4,7 +4,7 @@ <td>{{ $entry.Id }}</td> <td><input type="time" disabled value="{{$entry.From }}" /></td> <td><input type="time" disabled value="{{if $entry.To}}{{$entry.To }}{{end}}" /></td> - <td>{{ if $entry.Tag}}{{ $entry.Tag }}{{end}}</td> + <td></td> <td>{{ $entry.Comment }}</td> <td> {{ if $entry.To }}<button hx-trigger="click" hx-swap="outerHTML" hx-target="#entry-form" hx-get="/edit?id={{$entry.Id}}">e</button>{{end}} diff --git a/templates/parts/taskEntry.html b/templates/parts/taskEntry.html new file mode 100644 index 0000000..3c3be3f --- /dev/null +++ b/templates/parts/taskEntry.html @@ -0,0 +1,8 @@ +<option>-</option> +{{ range $group, $tasks := index . 0}} +<optgroup label="{{$group}}"> + {{ range $task := $tasks }} + <option {{if eq $task.Id (index $ 1)}}selected{{end}} value="{{$task.Id}}">{{$task.Text}}</option> + {{ end }} +</optgroup> +{{ end }} diff --git a/templates/parts/taskRows.html b/templates/parts/taskRows.html index a2a6020..bef2366 100644 --- a/templates/parts/taskRows.html +++ b/templates/parts/taskRows.html @@ -1,11 +1,20 @@ <table> <tbody hx-swap="outerHTML" hx-trigger="changedTasks from:body" hx-get="/task/list"> - {{ range $task := .Tasks }} + {{ range $group, $tasks := .GroupToTasks }} <tr> - <td>Trifork</td> - <td /> - <td /> + <td>{{ $group }}</td> + <td></td> + <td></td> + <td></td> </tr> + {{ range $task := $tasks }} + <tr> + <td></td> + <td>{{ $task.Text }}</td> + <td>{{ if $task.Ident }}{{ $task.Ident }}{{end}}</td> + <td><button hx-trigger="click" hx-swap="outerHTML" hx-target="#taskForm" hx-get="/task/edit?id={{$task.Id}}" >e</button></td> + </tr> + {{ end }} {{ end }} </tbody> </table> @@ -18,20 +18,27 @@ type Server struct { srv *Service } +func parseIdInt(str string) (int, error) { + var id int = -1 + if str != "" { + idLong, err := strconv.ParseInt(str, 10, 32) + if err != nil { + return -1, err + } + id = int(idLong) + } + return id, nil +} + func parseTaskFromForm(r *http.Request) (*Task, error) { err := r.ParseForm() if err != nil { return nil, err } - var id int64 - if r.PostFormValue("id") == "" { - id = -1 - } else { - id, err = strconv.ParseInt(r.PostFormValue("id"), 10, 32) - if err != nil { - return nil, err - } + id, err := parseIdInt(r.PostFormValue("id")) + if err != nil { + return nil, err } var group *string @@ -57,12 +64,59 @@ func parseTaskFromForm(r *http.Request) (*Task, error) { }, nil } +func readTaskSelectFromPost(r *http.Request, index string) (int, *time.Duration, bool, error) { + idS := r.PostFormValue(fmt.Sprintf("task-%s-id", index)) + if idS == "" || idS == "-" { + return -1, nil, false, nil + } + id, err := parseIdInt(idS) + if err != nil { + return -1, nil, false, err + } + + durS := r.PostFormValue(fmt.Sprintf("task-%s-part", index)) + if durS == "" { + return id, nil, true, nil + } + dur, err := time.ParseDuration(durS) + if err != nil { + return -1, nil, false, err + } + + return id, &dur, true, nil +} + func parseEntryFromForm(r *http.Request, useToday bool) (*Entry, error) { err := r.ParseForm() if err != nil { return nil, err } + tasks := map[int]*time.Duration{} + taskCount, err := strconv.ParseInt(r.PostFormValue("task-count"), 10, 32) + if err != nil { + return nil, fmt.Errorf("Error reading task-count: %w", err) + } + for i := 0; i < int(taskCount); i++ { + id, dur, ok, err := readTaskSelectFromPost(r, fmt.Sprintf("%d", i)) + if err != nil { + return nil, err + } + if !ok { + continue + } + tasks[id] = dur + } + { + id, dur, ok, err := readTaskSelectFromPost(r, "new") + if err != nil { + return nil, fmt.Errorf("Error reading new task select: %w", err) + } + if ok { + tasks[id] = dur + } + } + var date Date dateP, err := DateFromString(r.PostFormValue("date")) if err != nil { @@ -85,28 +139,51 @@ func parseEntryFromForm(r *http.Request, useToday bool) (*Entry, error) { if err != nil { return nil, err } - var id int64 - if r.PostFormValue("id") == "" { - id = -1 - } else { - id, err = strconv.ParseInt(r.PostFormValue("id"), 10, 32) - if err != nil { - return nil, err - } + id, err := parseIdInt(r.PostFormValue("id")) + if err != nil { + return nil, err } + log.Printf("hej hej %+v\n", tasks) task := &Entry{ Id: int(id), From: from, Date: date, To: to, + Tasks: tasks, Comment: r.PostFormValue("comment"), } return task, nil } func (s *Server) renderTemplate(w http.ResponseWriter, name string, page interface{}) { - tmpl, err := template.ParseFiles("templates/index.html", "templates/parts/entry.html", "templates/parts/entryRows.html", "templates/parts/task.html", "templates/parts/taskRows.html") + tmpl := template.New("").Funcs(template.FuncMap { + "arr": (func (els ...interface{}) []interface{} { + return els + }), + "taskMapToArr": (func (taskMap map[int]*time.Duration) [](struct {Key int; Id int; Sel *string}) { + res := make([](struct {Key int; Id int; Sel *string}), 0, len(taskMap)) + count := 0 + for taskId, sel := range taskMap { + thing := struct {Key int; Id int; Sel *string}{ + Key: count, + Id: taskId, + Sel: nil, + } + if sel != nil { + formatted := sel.String() + thing.Sel = &formatted + } + res = append(res, thing) + + count += 1 + } + + fmt.Printf("Hej, %+v, lul: %+v\n", res, taskMap) + return res + }), + }) + tmpl, err := tmpl.ParseFiles("templates/index.html", "templates/parts/entry.html", "templates/parts/entryRows.html", "templates/parts/task.html", "templates/parts/taskRows.html", "templates/parts/taskEntry.html") if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return @@ -269,12 +346,11 @@ func (s *Server) getNew(w http.ResponseWriter, r *http.Request) { } func (s *Server) getEdit(w http.ResponseWriter, r *http.Request) { - idLarge, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 32) + id, err := parseIdInt(r.URL.Query().Get("id")) if err != nil { writeError(w, err.Error(), http.StatusInternalServerError) return } - id := int(idLarge) entries, err := s.srv.Db.QueryEntry(&id, nil) if err != nil { @@ -359,6 +435,50 @@ func (s *Server) getTaskEmpty(w http.ResponseWriter, _ *http.Request) { s.renderTemplate(w, "task.html", page) } +func (s *Server) getTaskEdit(w http.ResponseWriter, r *http.Request) { + id, err := parseIdInt(r.URL.Query().Get("id")) + if err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } + + page, err := s.srv.GetTaskPage(&id, nil) + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + + s.renderTemplate(w, "task.html", page) +} + +func (s *Server) getTaskSelect(w http.ResponseWriter, r *http.Request) { + page, err := s.srv.GetTasksPage() + if err != nil { + writeError(w, err.Error(), http.StatusInternalServerError) + return + } + + var selected int = -1 + { + selectedStr := r.URL.Query().Get("selected") + if selectedStr != "" { + selectedLong, err := strconv.ParseInt(selectedStr, 10, 32) + if err != nil { + writeError(w, err.Error(), http.StatusBadRequest) + return + } + selected = int(selectedLong) + } + } + + args := []interface{}{ + &page.GroupToTasks, + selected, + } + + s.renderTemplate(w, "taskEntry.html", args) +} + func main() { fmt.Println("Hello world!") @@ -378,6 +498,8 @@ func main() { r.HandleFunc("/task/list", s.getTasksList).Methods("GET") r.HandleFunc("/task/save", s.postTaskSave).Methods("POST") r.HandleFunc("/task/empty", s.getTaskEmpty).Methods("GET") + r.HandleFunc("/task/edit", s.getTaskEdit).Methods("GET") + r.HandleFunc("/task/select", s.getTaskSelect).Methods("GET") r.HandleFunc("/", s.rootHandle) http.Handle("/", r) |