diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go
index e36ed3d..88b4414 100644
--- a/cmd/mavweb/main.go
+++ b/cmd/mavweb/main.go
@@ -1041,6 +1041,14 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri
return "", errors.New("invalid id")
}
+ if action == "promote" {
+ msg, err := promoteCandidate(ctx, core, r, id)
+ if err != nil {
+ return "", err
+ }
+ return msg, nil
+ }
+
if action == "edit" {
// The three fields capture set, and only those (Vikunja #509). Status
// is not editable here: that ladder is one-way and has its own buttons.
@@ -1095,6 +1103,82 @@ func applyTaskPost(ctx context.Context, core ipc.CoreAPI, r *http.Request) (stri
// fmtTaskDateValue renders a due date the way requires, or
// "" for no date. Separate from fmtTaskDate, which renders it for reading.
+// promoteCandidate turns a candidate into open work with the three things the
+// board needs (Vikunja #511): a definition of done, an optional blocker, and an
+// optional date.
+//
+// The definition of done is required, and the refusal is the store's — this
+// only reaches it in a readable order. The blocker is a NAME here and an entity
+// id in the row: identity lives in Nexus, so the name is resolved first and a
+// name Nexus cannot resolve stops the promotion instead of being stored.
+//
+// A date set here writes a reminder, which is the one unprompted delivery the
+// persona allows: he asked to be told, on a day he named.
+func promoteCandidate(ctx context.Context, core ipc.CoreAPI, r *http.Request, id int64) (string, error) {
+ doneWhen := strings.TrimSpace(r.FormValue("done_when"))
+ if doneWhen == "" {
+ return "", errors.New("write a definition of done — what has to be true for this to be finished")
+ }
+ text := strings.TrimSpace(r.FormValue("text"))
+ if text == "" {
+ return "", errors.New("empty task text")
+ }
+ due, err := formDue(r, now())
+ if err != nil {
+ return "", err
+ }
+
+ blockedOn := ""
+ if name := strings.TrimSpace(r.FormValue("blocked_on")); name != "" {
+ ref, err := core.ResolveEntity(ctx, name, []string{"person"})
+ switch {
+ case errors.Is(err, ipc.ErrNotImplemented):
+ return "", errors.New("no identity service here, so blocked-on cannot be stored — leave it empty")
+ case errors.Is(err, ipc.ErrNoEntity):
+ return "", fmt.Errorf("nexus does not know %q", name)
+ case err != nil:
+ return "", fmt.Errorf("resolving %q: %w", name, err)
+ case ref.Ambiguous:
+ // Asking, not picking: a task blocked on the wrong person is a
+ // mistake nobody can see afterwards.
+ return "", fmt.Errorf("%q matches %s — say which", name, strings.Join(ref.Candidates, ", "))
+ }
+ blockedOn = ref.ID
+ }
+
+ if err := core.SetTaskFields(ctx, id, doneWhen, blockedOn); err != nil {
+ return "", err
+ }
+ if due != nil {
+ wgt, err := formWeight(r)
+ if err != nil {
+ return "", err
+ }
+ if err := core.EditTask(ctx, id, text, due, wgt); err != nil {
+ return "", err
+ }
+ }
+ if err := core.SetTaskStatus(ctx, id, "open", now(), "tap:web"); err != nil {
+ if errors.Is(err, ipc.ErrTaskNoDoneWhen) {
+ return "", errors.New("write a definition of done before confirming this candidate")
+ }
+ return "", err
+ }
+ if due == nil {
+ return "confirmed", nil
+ }
+ // A date-only field has no hour. Nine in the morning, because the reminder
+ // is about a day's work and being told at midnight is being told the night
+ // before.
+ fire := time.Date(due.Year(), due.Month(), due.Day(), 9, 0, 0, 0, due.Location())
+ if _, err := core.CreateReminder(ctx, fire, text, ""); err != nil {
+ // The task IS promoted; only the reminder failed. Saying "confirmed"
+ // and nothing else would leave him expecting a nudge that will not come.
+ return "", fmt.Errorf("confirmed, but the reminder did not save: %w", err)
+ }
+ return "confirmed, and maven will remind you that morning", nil
+}
+
// formWeight reads the importance select. Out-of-range clamps rather than
// rejects — a bad select is not worth a 400 — but trailing garbage is refused,
// because strconv is not Sscanf and "3junk" is not a 3.
diff --git a/cmd/mavweb/tasks.html b/cmd/mavweb/tasks.html
index 583a486..427feb5 100644
--- a/cmd/mavweb/tasks.html
+++ b/cmd/mavweb/tasks.html
@@ -37,16 +37,27 @@
found, not confirmed {{len .Candidates}}
maven derived these from something she read. nothing counts as your work until you confirm it.
+
confirming asks for a definition of done: what has to be true for this to be finished. a task without one can never leave the board. a date here also books a reminder that morning.