@harborppm/cli 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/app.mjs +507 -84
- package/dist/words.mjs +301 -0
- package/lib/cli.mjs +114 -24
- package/lib/words.mjs +20 -0
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -128,10 +128,10 @@ function timeAgo(date, now = clockNow()) {
|
|
|
128
128
|
const words = `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
129
129
|
return diff > 0 ? `in ${words}` : `${words} ago`;
|
|
130
130
|
}
|
|
131
|
-
var MONTHS, DAYS$
|
|
131
|
+
var MONTHS$1, DAYS$2, pad$1, TOKENS, TOKEN, ISO, MINUTE;
|
|
132
132
|
var init_date_text = __esmMin((() => {
|
|
133
133
|
init_clock();
|
|
134
|
-
MONTHS = [
|
|
134
|
+
MONTHS$1 = [
|
|
135
135
|
"January",
|
|
136
136
|
"February",
|
|
137
137
|
"March",
|
|
@@ -145,7 +145,7 @@ var init_date_text = __esmMin((() => {
|
|
|
145
145
|
"November",
|
|
146
146
|
"December"
|
|
147
147
|
];
|
|
148
|
-
DAYS$
|
|
148
|
+
DAYS$2 = [
|
|
149
149
|
"Sunday",
|
|
150
150
|
"Monday",
|
|
151
151
|
"Tuesday",
|
|
@@ -157,13 +157,13 @@ var init_date_text = __esmMin((() => {
|
|
|
157
157
|
pad$1 = (n, width = 2) => String(n).padStart(width, "0");
|
|
158
158
|
TOKENS = {
|
|
159
159
|
yyyy: (d) => pad$1(d.getFullYear(), 4),
|
|
160
|
-
MMMM: (d) => MONTHS[d.getMonth()],
|
|
161
|
-
MMM: (d) => MONTHS[d.getMonth()].slice(0, 3),
|
|
160
|
+
MMMM: (d) => MONTHS$1[d.getMonth()],
|
|
161
|
+
MMM: (d) => MONTHS$1[d.getMonth()].slice(0, 3),
|
|
162
162
|
MM: (d) => pad$1(d.getMonth() + 1),
|
|
163
163
|
dd: (d) => pad$1(d.getDate()),
|
|
164
164
|
d: (d) => String(d.getDate()),
|
|
165
|
-
EEEE: (d) => DAYS$
|
|
166
|
-
EEE: (d) => DAYS$
|
|
165
|
+
EEEE: (d) => DAYS$2[d.getDay()],
|
|
166
|
+
EEE: (d) => DAYS$2[d.getDay()].slice(0, 3)
|
|
167
167
|
};
|
|
168
168
|
TOKEN = /yyyy|MMMM|MMM|MM|dd|d|EEEE|EEE/g;
|
|
169
169
|
ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?(Z|[+-]\d{2}(?::?\d{2})?)?)?$/;
|
|
@@ -2270,11 +2270,11 @@ function easter(year) {
|
|
|
2270
2270
|
function nth(year, month, dow, n) {
|
|
2271
2271
|
if (n > 0) {
|
|
2272
2272
|
const first = iso$4(year, month, 1);
|
|
2273
|
-
const shift = (dow - weekday(first) + 7) % 7;
|
|
2273
|
+
const shift = (dow - weekday$1(first) + 7) % 7;
|
|
2274
2274
|
return plus$1(first, shift + (n - 1) * 7);
|
|
2275
2275
|
}
|
|
2276
2276
|
const last = iso$4(year, month + 1, 0);
|
|
2277
|
-
return plus$1(last, -((weekday(last) - dow + 7) % 7));
|
|
2277
|
+
return plus$1(last, -((weekday$1(last) - dow + 7) % 7));
|
|
2278
2278
|
}
|
|
2279
2279
|
/** A country's public holidays in a year, by the day they're taken, oldest first. */
|
|
2280
2280
|
function holidaysIn(country, year) {
|
|
@@ -2290,7 +2290,7 @@ function holidaysIn(country, year) {
|
|
|
2290
2290
|
const out = [];
|
|
2291
2291
|
for (const h of days) {
|
|
2292
2292
|
let date = h.date;
|
|
2293
|
-
const wd = weekday(date);
|
|
2293
|
+
const wd = weekday$1(date);
|
|
2294
2294
|
if (h.move === "us" && (wd === 0 || wd === 6)) date = plus$1(date, wd === 6 ? -1 : 1);
|
|
2295
2295
|
else if (h.move === "next" && (wd === 0 || wd === 6)) {
|
|
2296
2296
|
date = plus$1(date, 1);
|
|
@@ -2389,19 +2389,19 @@ function harborHolidays(data, from, to) {
|
|
|
2389
2389
|
};
|
|
2390
2390
|
});
|
|
2391
2391
|
}
|
|
2392
|
-
var iso$4, weekday, plus$1, isWeekend, onOrBefore, fixed, fromEaster, MATARIKI, RULES, cache, PLAIN_CALENDAR;
|
|
2392
|
+
var iso$4, weekday$1, plus$1, isWeekend, onOrBefore, fixed, fromEaster, MATARIKI, RULES, cache, PLAIN_CALENDAR;
|
|
2393
2393
|
var init_holidays = __esmMin((() => {
|
|
2394
2394
|
init_date_fns();
|
|
2395
2395
|
init_format();
|
|
2396
2396
|
init_countries();
|
|
2397
2397
|
iso$4 = (y, m, d) => todayISO(new Date(y, m - 1, d, 12));
|
|
2398
|
-
weekday = (day) => (/* @__PURE__ */ new Date(`${day}T12:00:00`)).getDay();
|
|
2398
|
+
weekday$1 = (day) => (/* @__PURE__ */ new Date(`${day}T12:00:00`)).getDay();
|
|
2399
2399
|
plus$1 = (day, n) => todayISO(addDays(/* @__PURE__ */ new Date(`${day}T12:00:00`), n));
|
|
2400
2400
|
isWeekend = (day) => {
|
|
2401
|
-
const wd = weekday(day);
|
|
2401
|
+
const wd = weekday$1(day);
|
|
2402
2402
|
return wd === 0 || wd === 6;
|
|
2403
2403
|
};
|
|
2404
|
-
onOrBefore = (day, dow) => plus$1(day, -((weekday(day) - dow + 7) % 7));
|
|
2404
|
+
onOrBefore = (day, dow) => plus$1(day, -((weekday$1(day) - dow + 7) % 7));
|
|
2405
2405
|
fixed = (m, d) => (y) => iso$4(y, m, d);
|
|
2406
2406
|
fromEaster = (n) => (y) => plus$1(easter(y), n);
|
|
2407
2407
|
MATARIKI = {
|
|
@@ -2499,7 +2499,7 @@ var init_holidays = __esmMin((() => {
|
|
|
2499
2499
|
fixed(1, 1),
|
|
2500
2500
|
"next"
|
|
2501
2501
|
],
|
|
2502
|
-
["St Brigid's Day", (y) => weekday(iso$4(y, 2, 1)) === 5 ? iso$4(y, 2, 1) : nth(y, 2, 1, 1)],
|
|
2502
|
+
["St Brigid's Day", (y) => weekday$1(iso$4(y, 2, 1)) === 5 ? iso$4(y, 2, 1) : nth(y, 2, 1, 1)],
|
|
2503
2503
|
[
|
|
2504
2504
|
"St Patrick's Day",
|
|
2505
2505
|
fixed(3, 17),
|
|
@@ -2611,7 +2611,7 @@ var init_holidays = __esmMin((() => {
|
|
|
2611
2611
|
NL: [
|
|
2612
2612
|
["Nieuwjaarsdag", fixed(1, 1)],
|
|
2613
2613
|
["Tweede paasdag", fromEaster(1)],
|
|
2614
|
-
["Koningsdag", (y) => weekday(iso$4(y, 4, 27)) === 0 ? iso$4(y, 4, 26) : iso$4(y, 4, 27)],
|
|
2614
|
+
["Koningsdag", (y) => weekday$1(iso$4(y, 4, 27)) === 0 ? iso$4(y, 4, 26) : iso$4(y, 4, 27)],
|
|
2615
2615
|
["Hemelvaartsdag", fromEaster(39)],
|
|
2616
2616
|
["Tweede pinksterdag", fromEaster(50)],
|
|
2617
2617
|
["Eerste kerstdag", fixed(12, 25)],
|
|
@@ -3753,6 +3753,359 @@ function applyReckoning(actions, projectId, r, startDate) {
|
|
|
3753
3753
|
};
|
|
3754
3754
|
}
|
|
3755
3755
|
|
|
3756
|
+
// src/lib/plain-words.ts
|
|
3757
|
+
const DAYS$1 = [
|
|
3758
|
+
"sunday",
|
|
3759
|
+
"monday",
|
|
3760
|
+
"tuesday",
|
|
3761
|
+
"wednesday",
|
|
3762
|
+
"thursday",
|
|
3763
|
+
"friday",
|
|
3764
|
+
"saturday"
|
|
3765
|
+
];
|
|
3766
|
+
const MONTHS = [
|
|
3767
|
+
"january",
|
|
3768
|
+
"february",
|
|
3769
|
+
"march",
|
|
3770
|
+
"april",
|
|
3771
|
+
"may",
|
|
3772
|
+
"june",
|
|
3773
|
+
"july",
|
|
3774
|
+
"august",
|
|
3775
|
+
"september",
|
|
3776
|
+
"october",
|
|
3777
|
+
"november",
|
|
3778
|
+
"december"
|
|
3779
|
+
];
|
|
3780
|
+
const NUMBER_WORDS = {
|
|
3781
|
+
a: 1,
|
|
3782
|
+
an: 1,
|
|
3783
|
+
one: 1,
|
|
3784
|
+
two: 2,
|
|
3785
|
+
three: 3,
|
|
3786
|
+
four: 4,
|
|
3787
|
+
five: 5,
|
|
3788
|
+
six: 6,
|
|
3789
|
+
seven: 7,
|
|
3790
|
+
eight: 8,
|
|
3791
|
+
nine: 9,
|
|
3792
|
+
ten: 10
|
|
3793
|
+
};
|
|
3794
|
+
const loose = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
3795
|
+
function shift(iso, days) {
|
|
3796
|
+
const d = /* @__PURE__ */ new Date(`${iso}T12:00:00Z`);
|
|
3797
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
3798
|
+
return d.toISOString().slice(0, 10);
|
|
3799
|
+
}
|
|
3800
|
+
function weekday(iso) {
|
|
3801
|
+
return (/* @__PURE__ */ new Date(`${iso}T12:00:00Z`)).getUTCDay();
|
|
3802
|
+
}
|
|
3803
|
+
function dayOf$1(y, m, d) {
|
|
3804
|
+
const date = new Date(Date.UTC(y, m - 1, d, 12));
|
|
3805
|
+
if (date.getUTCMonth() !== m - 1 || date.getUTCDate() !== d) return null;
|
|
3806
|
+
return date.toISOString().slice(0, 10);
|
|
3807
|
+
}
|
|
3808
|
+
/** A month and day with no year: this year's, or next year's once it has passed. */
|
|
3809
|
+
function nextMonthDay(today, m, d) {
|
|
3810
|
+
const year = Number(today.slice(0, 4));
|
|
3811
|
+
const thisYear = dayOf$1(year, m, d);
|
|
3812
|
+
if (!thisYear) return null;
|
|
3813
|
+
return thisYear >= today ? thisYear : dayOf$1(year + 1, m, d);
|
|
3814
|
+
}
|
|
3815
|
+
const monthIndex = (word) => {
|
|
3816
|
+
const w = word.toLowerCase().replace(/\.$/, "");
|
|
3817
|
+
if (w.length < 3) return -1;
|
|
3818
|
+
return MONTHS.findIndex((m) => m.startsWith(w) && (w.length >= 3 || m === w));
|
|
3819
|
+
};
|
|
3820
|
+
const DAY_WORD = "(?:sun|mon|tues?|wed(?:nes)?|thu(?:rs?)?|fri|sat(?:ur)?)(?:day)?";
|
|
3821
|
+
const MONTH_WORD = "(?:jan|feb|mar|apr|may|jun|jul|aug|sept?|oct|nov|dec)[a-z]*\\.?";
|
|
3822
|
+
/** Every way of saying a day, longest first so "next friday" wins over "friday". */
|
|
3823
|
+
const WHEN = [
|
|
3824
|
+
"today",
|
|
3825
|
+
"tonight",
|
|
3826
|
+
"tomorrow",
|
|
3827
|
+
"tmrw",
|
|
3828
|
+
"the day after tomorrow",
|
|
3829
|
+
"end of (?:the )?week",
|
|
3830
|
+
"this week",
|
|
3831
|
+
"next week",
|
|
3832
|
+
"end of (?:the )?month",
|
|
3833
|
+
"next month",
|
|
3834
|
+
`in (?:\\d{1,2}|a|an|one|two|three|four|five|six|seven|eight|nine|ten) (?:days?|weeks?)`,
|
|
3835
|
+
`(?:this |next |on )?${DAY_WORD}`,
|
|
3836
|
+
`${MONTH_WORD} \\d{1,2}(?:st|nd|rd|th)?(?:,? \\d{4})?`,
|
|
3837
|
+
`\\d{1,2}(?:st|nd|rd|th)? (?:of )?${MONTH_WORD}(?:,? \\d{4})?`,
|
|
3838
|
+
"\\d{4}-\\d{1,2}-\\d{1,2}",
|
|
3839
|
+
"\\d{1,2}/\\d{1,2}(?:/\\d{2,4})?"
|
|
3840
|
+
].join("|");
|
|
3841
|
+
const LEAD$1 = "(?:by|due|due on|due by|on|before|until|till)";
|
|
3842
|
+
/** The day a phrase names, or null when it names none. */
|
|
3843
|
+
function dayFromWords(phrase, today) {
|
|
3844
|
+
const p = phrase.toLowerCase().replace(/\s+/g, " ").replace(/^on /, "").trim();
|
|
3845
|
+
if (p === "today" || p === "tonight") return today;
|
|
3846
|
+
if (p === "tomorrow" || p === "tmrw") return shift(today, 1);
|
|
3847
|
+
if (p === "the day after tomorrow") return shift(today, 2);
|
|
3848
|
+
const dow = weekday(today);
|
|
3849
|
+
if (/^end of (the )?week$/.test(p) || p === "this week") return shift(today, (5 - dow + 7) % 7);
|
|
3850
|
+
if (p === "next week") return shift(today, (1 - dow + 7) % 7 || 7);
|
|
3851
|
+
if (p === "next month") {
|
|
3852
|
+
const [y, m] = [Number(today.slice(0, 4)), Number(today.slice(5, 7))];
|
|
3853
|
+
return m === 12 ? dayOf$1(y + 1, 1, 1) : dayOf$1(y, m + 1, 1);
|
|
3854
|
+
}
|
|
3855
|
+
if (/^end of (the )?month$/.test(p)) {
|
|
3856
|
+
const [y, m] = [Number(today.slice(0, 4)), Number(today.slice(5, 7))];
|
|
3857
|
+
return shift(m === 12 ? dayOf$1(y + 1, 1, 1) : dayOf$1(y, m + 1, 1), -1);
|
|
3858
|
+
}
|
|
3859
|
+
let m = p.match(/^in (\w+) (day|week)s?$/);
|
|
3860
|
+
if (m) {
|
|
3861
|
+
const n = /^\d+$/.test(m[1]) ? Number(m[1]) : NUMBER_WORDS[m[1]];
|
|
3862
|
+
if (n == null || n > 366) return null;
|
|
3863
|
+
return shift(today, m[2] === "week" ? n * 7 : n);
|
|
3864
|
+
}
|
|
3865
|
+
m = p.match(/^(this |next )?([a-z]+)$/);
|
|
3866
|
+
if (m) {
|
|
3867
|
+
const target = DAYS$1.findIndex((d) => d.startsWith(m[2].slice(0, 3)) && m[2].length >= 3);
|
|
3868
|
+
if (target >= 0 && new RegExp(`^${DAY_WORD}$`).test(m[2])) {
|
|
3869
|
+
const ahead = (target - dow + 7) % 7 || 7;
|
|
3870
|
+
return shift(today, m[1] === "next " ? ahead + 7 : ahead);
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
m = p.match(/^([a-z]+\.?) (\d{1,2})(?:st|nd|rd|th)?(?:,? (\d{4}))?$/);
|
|
3874
|
+
if (m && monthIndex(m[1]) >= 0) {
|
|
3875
|
+
const month = monthIndex(m[1]) + 1;
|
|
3876
|
+
return m[3] ? dayOf$1(Number(m[3]), month, Number(m[2])) : nextMonthDay(today, month, Number(m[2]));
|
|
3877
|
+
}
|
|
3878
|
+
m = p.match(/^(\d{1,2})(?:st|nd|rd|th)? (?:of )?([a-z]+\.?)(?:,? (\d{4}))?$/);
|
|
3879
|
+
if (m && monthIndex(m[2]) >= 0) {
|
|
3880
|
+
const month = monthIndex(m[2]) + 1;
|
|
3881
|
+
return m[3] ? dayOf$1(Number(m[3]), month, Number(m[1])) : nextMonthDay(today, month, Number(m[1]));
|
|
3882
|
+
}
|
|
3883
|
+
m = p.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
3884
|
+
if (m) return dayOf$1(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
3885
|
+
m = p.match(/^(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?$/);
|
|
3886
|
+
if (m) {
|
|
3887
|
+
if (!m[3]) return nextMonthDay(today, Number(m[1]), Number(m[2]));
|
|
3888
|
+
return dayOf$1(Number(m[3]) < 100 ? 2e3 + Number(m[3]) : Number(m[3]), Number(m[1]), Number(m[2]));
|
|
3889
|
+
}
|
|
3890
|
+
return null;
|
|
3891
|
+
}
|
|
3892
|
+
const PRIORITY_WORDS = {
|
|
3893
|
+
urgent: "urgent",
|
|
3894
|
+
asap: "urgent",
|
|
3895
|
+
top: "urgent",
|
|
3896
|
+
p0: "urgent",
|
|
3897
|
+
high: "high",
|
|
3898
|
+
p1: "high",
|
|
3899
|
+
medium: "medium",
|
|
3900
|
+
normal: "medium",
|
|
3901
|
+
p2: "medium",
|
|
3902
|
+
low: "low",
|
|
3903
|
+
p3: "low"
|
|
3904
|
+
};
|
|
3905
|
+
/** Removes found spans from the text, tidying the spaces and commas they leave. */
|
|
3906
|
+
function without(text, spans) {
|
|
3907
|
+
let out = text;
|
|
3908
|
+
for (const s of [...spans].sort((a, b) => b.start - a.start)) out = `${out.slice(0, s.start)} ${out.slice(s.end)}`;
|
|
3909
|
+
return out.replace(/\s+([,.;:!?])/g, "$1").replace(/[,;:]+([,.;:!?])/g, "$1").replace(/\s{2,}/g, " ").replace(/(?:[\s,;:–—-]+)$/u, "").replace(/^[\s,;:]+/, "").trim();
|
|
3910
|
+
}
|
|
3911
|
+
/** What a new task's words say, and the title they leave. */
|
|
3912
|
+
function readTaskWords(input, ctx) {
|
|
3913
|
+
const skip = ctx.skip ?? /* @__PURE__ */ new Set();
|
|
3914
|
+
const text = input;
|
|
3915
|
+
const taken = [];
|
|
3916
|
+
const free = (start, end) => !taken.some((t) => start < t.end && end > t.start);
|
|
3917
|
+
let priority = null;
|
|
3918
|
+
if (!skip.has("priority")) for (const re of [
|
|
3919
|
+
/(?:^|\s)!(urgent|asap|high|medium|normal|low|p[0-3])\b/gi,
|
|
3920
|
+
/(?:^|[\s,])(urgent|top|high|medium|normal|low) priority\b/gi,
|
|
3921
|
+
/(?:^|[\s,])priority (urgent|high|medium|normal|low|p[0-3])\b/gi,
|
|
3922
|
+
/(?:^|[\s,])(urgent|asap)$/gi
|
|
3923
|
+
]) {
|
|
3924
|
+
const m = re.exec(text);
|
|
3925
|
+
if (m) {
|
|
3926
|
+
const lead = m[0].length - m[0].trimStart().length;
|
|
3927
|
+
const start = m.index + lead + (m[0].trimStart().startsWith(",") ? 1 : 0);
|
|
3928
|
+
priority = {
|
|
3929
|
+
value: PRIORITY_WORDS[m[1].toLowerCase()],
|
|
3930
|
+
start,
|
|
3931
|
+
end: m.index + m[0].length
|
|
3932
|
+
};
|
|
3933
|
+
taken.push(priority);
|
|
3934
|
+
break;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
let project = null;
|
|
3938
|
+
if (!skip.has("project")) for (const m of text.matchAll(/(?:^|\s)#([\p{L}\p{N}_-]+)/gu)) {
|
|
3939
|
+
const key = loose(m[1]);
|
|
3940
|
+
if (!key) continue;
|
|
3941
|
+
const exact = ctx.projects.filter((p) => loose(p.name) === key);
|
|
3942
|
+
const begins = ctx.projects.filter((p) => loose(p.name).startsWith(key));
|
|
3943
|
+
const hit = exact.length === 1 ? exact[0] : begins.length === 1 ? begins[0] : null;
|
|
3944
|
+
if (!hit) continue;
|
|
3945
|
+
const start = m.index + (m[0].length - m[0].trimStart().length);
|
|
3946
|
+
project = {
|
|
3947
|
+
value: hit.id,
|
|
3948
|
+
start,
|
|
3949
|
+
end: m.index + m[0].length
|
|
3950
|
+
};
|
|
3951
|
+
taken.push(project);
|
|
3952
|
+
break;
|
|
3953
|
+
}
|
|
3954
|
+
let owner = null;
|
|
3955
|
+
if (!skip.has("owner")) {
|
|
3956
|
+
const byKey = (key) => {
|
|
3957
|
+
const full = ctx.people.filter((p) => loose(p.name) === key);
|
|
3958
|
+
if (full.length === 1) return full[0];
|
|
3959
|
+
const first = ctx.people.filter((p) => loose(p.name.split(/\s+/)[0] ?? "") === key);
|
|
3960
|
+
if (first.length === 1) return first[0];
|
|
3961
|
+
const begins = ctx.people.filter((p) => loose(p.name).startsWith(key));
|
|
3962
|
+
return key.length >= 3 && begins.length === 1 ? begins[0] : null;
|
|
3963
|
+
};
|
|
3964
|
+
for (const m of text.matchAll(/(?:^|\s)@([\p{L}\p{N}._-]+)/gu)) {
|
|
3965
|
+
const hit = byKey(loose(m[1]));
|
|
3966
|
+
if (!hit) continue;
|
|
3967
|
+
const start = m.index + (m[0].length - m[0].trimStart().length);
|
|
3968
|
+
if (!free(start, m.index + m[0].length)) continue;
|
|
3969
|
+
owner = {
|
|
3970
|
+
value: hit.id,
|
|
3971
|
+
start,
|
|
3972
|
+
end: m.index + m[0].length
|
|
3973
|
+
};
|
|
3974
|
+
break;
|
|
3975
|
+
}
|
|
3976
|
+
if (!owner) for (const m of text.matchAll(/(?:^|[\s,])(?:assign(?:ed)? (?:it )?to|owner(?: is)?:?) (\p{L}+(?: \p{L}+)?)/giu)) {
|
|
3977
|
+
const words = m[1].split(" ");
|
|
3978
|
+
const two = words.length === 2 ? byKey(loose(m[1])) : null;
|
|
3979
|
+
const one = byKey(loose(words[0]));
|
|
3980
|
+
const hit = two ?? one;
|
|
3981
|
+
if (!hit) continue;
|
|
3982
|
+
const lead = m[0].length - m[0].replace(/^[\s,]+/, "").length;
|
|
3983
|
+
const start = m.index + lead;
|
|
3984
|
+
const end = two ? m.index + m[0].length : m.index + m[0].length - (words.length === 2 ? words[1].length + 1 : 0);
|
|
3985
|
+
if (!free(start, end)) continue;
|
|
3986
|
+
owner = {
|
|
3987
|
+
value: hit.id,
|
|
3988
|
+
start,
|
|
3989
|
+
end
|
|
3990
|
+
};
|
|
3991
|
+
break;
|
|
3992
|
+
}
|
|
3993
|
+
if (owner) taken.push(owner);
|
|
3994
|
+
}
|
|
3995
|
+
let due = null;
|
|
3996
|
+
if (!skip.has("due")) {
|
|
3997
|
+
const led = new RegExp(`(?:^|[\\s,])(${LEAD$1} (${WHEN}))(?=$|[\\s,.;:!?])`, "gi");
|
|
3998
|
+
for (const m of text.matchAll(led)) {
|
|
3999
|
+
const day = dayFromWords(m[2], ctx.today);
|
|
4000
|
+
if (!day) continue;
|
|
4001
|
+
const start = m.index + m[0].indexOf(m[1]);
|
|
4002
|
+
const end = start + m[1].length;
|
|
4003
|
+
if (!free(start, end)) continue;
|
|
4004
|
+
due = {
|
|
4005
|
+
value: day,
|
|
4006
|
+
start,
|
|
4007
|
+
end
|
|
4008
|
+
};
|
|
4009
|
+
break;
|
|
4010
|
+
}
|
|
4011
|
+
if (!due) {
|
|
4012
|
+
const rest = without(text, taken);
|
|
4013
|
+
const tail = new RegExp(`(?:^|[\\s,])(${WHEN})[.!]?$`, "i").exec(rest);
|
|
4014
|
+
const short = tail && /^(?:sun|mon|tue|wed|thu|fri|sat)$/i.test(tail[1]);
|
|
4015
|
+
const said = tail && /\b(?:to|for|from|after|with|since)$/i.test(rest.slice(0, tail.index).trimEnd());
|
|
4016
|
+
if (tail && !short && !said && rest.trim().length > tail[1].length) {
|
|
4017
|
+
const day = dayFromWords(tail[1], ctx.today);
|
|
4018
|
+
if (day) {
|
|
4019
|
+
const at = text.toLowerCase().lastIndexOf(tail[1].toLowerCase());
|
|
4020
|
+
if (at >= 0 && free(at, at + tail[1].length)) due = {
|
|
4021
|
+
value: day,
|
|
4022
|
+
start: at,
|
|
4023
|
+
end: at + tail[1].length
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (due) taken.push(due);
|
|
4029
|
+
}
|
|
4030
|
+
const span = (f) => f ? text.slice(f.start, f.end).trim() : "";
|
|
4031
|
+
let title = without(text, taken);
|
|
4032
|
+
const first = text.search(/\S/);
|
|
4033
|
+
if (taken.some((t) => t.start === first)) title = title.charAt(0).toUpperCase() + title.slice(1);
|
|
4034
|
+
return {
|
|
4035
|
+
title,
|
|
4036
|
+
due: due ? {
|
|
4037
|
+
value: due.value,
|
|
4038
|
+
text: span(due)
|
|
4039
|
+
} : null,
|
|
4040
|
+
owner: owner ? {
|
|
4041
|
+
value: owner.value,
|
|
4042
|
+
text: span(owner)
|
|
4043
|
+
} : null,
|
|
4044
|
+
priority: priority ? {
|
|
4045
|
+
value: priority.value,
|
|
4046
|
+
text: span(priority)
|
|
4047
|
+
} : null,
|
|
4048
|
+
project: project ? {
|
|
4049
|
+
value: project.value,
|
|
4050
|
+
text: span(project)
|
|
4051
|
+
} : null
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// src/lib/swatches.ts
|
|
4056
|
+
var SWATCHES, HEALTH_LABEL$1, TASK_COLUMNS, TASK_STATUS_LABEL, PRIORITY_LABEL, CONSTRAINT_KIND_LABEL, CONSTRAINT_KIND_HINT;
|
|
4057
|
+
var init_swatches = __esmMin((() => {
|
|
4058
|
+
SWATCHES = [
|
|
4059
|
+
"pine",
|
|
4060
|
+
"clay",
|
|
4061
|
+
"sage",
|
|
4062
|
+
"sand",
|
|
4063
|
+
"slate",
|
|
4064
|
+
"sea",
|
|
4065
|
+
"rust",
|
|
4066
|
+
"moss"
|
|
4067
|
+
];
|
|
4068
|
+
HEALTH_LABEL$1 = {
|
|
4069
|
+
"on-track": "On track",
|
|
4070
|
+
"at-risk": "At risk",
|
|
4071
|
+
"off-track": "Off track",
|
|
4072
|
+
paused: "Paused",
|
|
4073
|
+
done: "Done"
|
|
4074
|
+
};
|
|
4075
|
+
TASK_COLUMNS = [
|
|
4076
|
+
"backlog",
|
|
4077
|
+
"todo",
|
|
4078
|
+
"doing",
|
|
4079
|
+
"review",
|
|
4080
|
+
"done"
|
|
4081
|
+
];
|
|
4082
|
+
TASK_STATUS_LABEL = {
|
|
4083
|
+
backlog: "Backlog",
|
|
4084
|
+
todo: "To do",
|
|
4085
|
+
doing: "In play",
|
|
4086
|
+
review: "Review",
|
|
4087
|
+
done: "Done"
|
|
4088
|
+
};
|
|
4089
|
+
PRIORITY_LABEL = {
|
|
4090
|
+
low: "Low",
|
|
4091
|
+
medium: "Medium",
|
|
4092
|
+
high: "High",
|
|
4093
|
+
urgent: "Urgent"
|
|
4094
|
+
};
|
|
4095
|
+
CONSTRAINT_KIND_LABEL = {
|
|
4096
|
+
resource: "A person",
|
|
4097
|
+
policy: "A policy",
|
|
4098
|
+
external: "Outside the building",
|
|
4099
|
+
collision: "A collision"
|
|
4100
|
+
};
|
|
4101
|
+
CONSTRAINT_KIND_HINT = {
|
|
4102
|
+
resource: "Someone whose calendar sets the date.",
|
|
4103
|
+
policy: "A rule that creates the wait.",
|
|
4104
|
+
external: "A vendor, a city, a review board.",
|
|
4105
|
+
collision: "Two things sharing one window."
|
|
4106
|
+
};
|
|
4107
|
+
}));
|
|
4108
|
+
|
|
3756
4109
|
// src/lib/raid.ts
|
|
3757
4110
|
/** Still needs someone's attention: not closed, resolved, decided or superseded. */
|
|
3758
4111
|
function isActive(item) {
|
|
@@ -3963,67 +4316,13 @@ var init_raid = __esmMin((() => {
|
|
|
3963
4316
|
ids = (v) => Array.isArray(v) ? [...new Set(v.filter((x) => typeof x === "string" && !!x))] : [];
|
|
3964
4317
|
}));
|
|
3965
4318
|
|
|
3966
|
-
// src/lib/swatches.ts
|
|
3967
|
-
var SWATCHES, HEALTH_LABEL$1, TASK_COLUMNS, TASK_STATUS_LABEL, PRIORITY_LABEL, CONSTRAINT_KIND_LABEL, CONSTRAINT_KIND_HINT;
|
|
3968
|
-
var init_swatches = __esmMin((() => {
|
|
3969
|
-
SWATCHES = [
|
|
3970
|
-
"pine",
|
|
3971
|
-
"clay",
|
|
3972
|
-
"sage",
|
|
3973
|
-
"sand",
|
|
3974
|
-
"slate",
|
|
3975
|
-
"sea",
|
|
3976
|
-
"rust",
|
|
3977
|
-
"moss"
|
|
3978
|
-
];
|
|
3979
|
-
HEALTH_LABEL$1 = {
|
|
3980
|
-
"on-track": "On track",
|
|
3981
|
-
"at-risk": "At risk",
|
|
3982
|
-
"off-track": "Off track",
|
|
3983
|
-
paused: "Paused",
|
|
3984
|
-
done: "Done"
|
|
3985
|
-
};
|
|
3986
|
-
TASK_COLUMNS = [
|
|
3987
|
-
"backlog",
|
|
3988
|
-
"todo",
|
|
3989
|
-
"doing",
|
|
3990
|
-
"review",
|
|
3991
|
-
"done"
|
|
3992
|
-
];
|
|
3993
|
-
TASK_STATUS_LABEL = {
|
|
3994
|
-
backlog: "Backlog",
|
|
3995
|
-
todo: "To do",
|
|
3996
|
-
doing: "In play",
|
|
3997
|
-
review: "Review",
|
|
3998
|
-
done: "Done"
|
|
3999
|
-
};
|
|
4000
|
-
PRIORITY_LABEL = {
|
|
4001
|
-
low: "Low",
|
|
4002
|
-
medium: "Medium",
|
|
4003
|
-
high: "High",
|
|
4004
|
-
urgent: "Urgent"
|
|
4005
|
-
};
|
|
4006
|
-
CONSTRAINT_KIND_LABEL = {
|
|
4007
|
-
resource: "A person",
|
|
4008
|
-
policy: "A policy",
|
|
4009
|
-
external: "Outside the building",
|
|
4010
|
-
collision: "A collision"
|
|
4011
|
-
};
|
|
4012
|
-
CONSTRAINT_KIND_HINT = {
|
|
4013
|
-
resource: "Someone whose calendar sets the date.",
|
|
4014
|
-
policy: "A rule that creates the wait.",
|
|
4015
|
-
external: "A vendor, a city, a review board.",
|
|
4016
|
-
collision: "Two things sharing one window."
|
|
4017
|
-
};
|
|
4018
|
-
}));
|
|
4019
|
-
|
|
4020
4319
|
// src/tui/create.ts
|
|
4021
4320
|
init_date_fns();
|
|
4022
4321
|
init_format();
|
|
4322
|
+
init_swatches();
|
|
4023
4323
|
init_indexes();
|
|
4024
4324
|
init_date_text();
|
|
4025
4325
|
init_raid();
|
|
4026
|
-
init_swatches();
|
|
4027
4326
|
const scale = (labels) => [
|
|
4028
4327
|
1,
|
|
4029
4328
|
2,
|
|
@@ -4064,6 +4363,28 @@ function planFromChoices(data, finished, name, programId) {
|
|
|
4064
4363
|
...rest.map((p) => choice(p, false))
|
|
4065
4364
|
];
|
|
4066
4365
|
}
|
|
4366
|
+
/**
|
|
4367
|
+
* The terminal's new task box reads its title as the website's does:
|
|
4368
|
+
* "Book the venue @Quinn by Friday !high #Website" adds “Book the venue”
|
|
4369
|
+
* for Quinn, due Friday, high priority, in Website.
|
|
4370
|
+
*/
|
|
4371
|
+
function readNewTask(text, data, today) {
|
|
4372
|
+
const words = readTaskWords(text, {
|
|
4373
|
+
today,
|
|
4374
|
+
people: data.people,
|
|
4375
|
+
projects: data.projects
|
|
4376
|
+
});
|
|
4377
|
+
const bits = [
|
|
4378
|
+
words.owner && (data.people.find((p) => p.id === words.owner.value)?.name ?? "someone"),
|
|
4379
|
+
words.due && `due ${longDate(words.due.value)}`,
|
|
4380
|
+
words.priority && `${PRIORITY_LABEL[words.priority.value].toLowerCase()} priority`,
|
|
4381
|
+
words.project && (data.projects.find((p) => p.id === words.project.value)?.name ?? "project")
|
|
4382
|
+
].filter((b) => Boolean(b));
|
|
4383
|
+
return {
|
|
4384
|
+
words,
|
|
4385
|
+
line: bits.length && words.title ? [`Adds “${words.title}”`, ...bits].join(" · ") : null
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4067
4388
|
const CREATE_TITLE = {
|
|
4068
4389
|
portfolio: "New portfolio",
|
|
4069
4390
|
project: "New project",
|
|
@@ -6334,7 +6655,7 @@ applyTheme("dark");
|
|
|
6334
6655
|
* from, the key hints along the bottom, health and priority marks.
|
|
6335
6656
|
*/
|
|
6336
6657
|
/** One line of text. Enter keeps it, Esc leaves it as it was. */
|
|
6337
|
-
function TextField({ label, initial = "", placeholder, hint, onSubmit, onCancel }) {
|
|
6658
|
+
function TextField({ label, initial = "", placeholder, hint, hintFor, onSubmit, onCancel }) {
|
|
6338
6659
|
const [value, setShown] = useState(initial);
|
|
6339
6660
|
const typed = useRef(initial);
|
|
6340
6661
|
const setValue = (next) => {
|
|
@@ -6379,7 +6700,7 @@ function TextField({ label, initial = "", placeholder, hint, onSubmit, onCancel
|
|
|
6379
6700
|
/* @__PURE__ */ jsx(Text, {
|
|
6380
6701
|
color: theme.muted,
|
|
6381
6702
|
wrap: "truncate-end",
|
|
6382
|
-
children: hint ?? "Enter to keep · Esc to leave it"
|
|
6703
|
+
children: hintFor?.(value) ?? hint ?? "Enter to keep · Esc to leave it"
|
|
6383
6704
|
})
|
|
6384
6705
|
]
|
|
6385
6706
|
});
|
|
@@ -12355,6 +12676,8 @@ function Splash({ opening }) {
|
|
|
12355
12676
|
|
|
12356
12677
|
// src/tui/views/overlays.tsx
|
|
12357
12678
|
init_search();
|
|
12679
|
+
init_clock();
|
|
12680
|
+
init_format();
|
|
12358
12681
|
/** Find a task by key or words; Enter opens it. */
|
|
12359
12682
|
function SearchBox({ width, onOpen, onClose }) {
|
|
12360
12683
|
const tasks = useHarbor((s) => s.tasks);
|
|
@@ -12444,11 +12767,19 @@ function SearchBox({ width, onOpen, onClose }) {
|
|
|
12444
12767
|
]
|
|
12445
12768
|
});
|
|
12446
12769
|
}
|
|
12447
|
-
/**
|
|
12770
|
+
/**
|
|
12771
|
+
* Add a task: which project (when it isn't known), then its title, read in
|
|
12772
|
+
* plain words as the website reads it (@name, by Friday, !high, #project).
|
|
12773
|
+
*/
|
|
12448
12774
|
function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
12449
12775
|
const { store } = useDeps();
|
|
12450
12776
|
const projects = useHarbor((s) => s.projects);
|
|
12777
|
+
const people = useHarbor((s) => s.people);
|
|
12451
12778
|
const me = useHarbor((s) => s.currentUserId);
|
|
12779
|
+
const read = (title) => readNewTask(title, {
|
|
12780
|
+
people,
|
|
12781
|
+
projects
|
|
12782
|
+
}, todayISO(clockNow()));
|
|
12452
12783
|
const [project, setProject] = useState(() => projectId ?? soleOpenProject(projects));
|
|
12453
12784
|
const chosen = projects.find((p) => p.id === project);
|
|
12454
12785
|
if (!chosen) return /* @__PURE__ */ jsx(Box, {
|
|
@@ -12468,21 +12799,24 @@ function NewTask({ projectId, status, width, onDone, onFull }) {
|
|
|
12468
12799
|
width,
|
|
12469
12800
|
children: /* @__PURE__ */ jsx(TextField, {
|
|
12470
12801
|
label: `New task in ${chosen.name}${status ? ` · ${STATUS_LABEL[status]}` : ""}`,
|
|
12471
|
-
placeholder: "What needs doing?",
|
|
12802
|
+
placeholder: "What needs doing? Add @name, by Friday, #project",
|
|
12472
12803
|
hint: `Enter adds it (yours, ${STATUS_LABEL[status ?? "todo"].toLowerCase()}) · Esc leaves it`,
|
|
12804
|
+
hintFor: (title) => read(title).line,
|
|
12473
12805
|
onCancel: () => onDone(null),
|
|
12474
12806
|
onSubmit: (title) => {
|
|
12475
12807
|
if (!title.trim()) return onDone(null);
|
|
12476
|
-
const
|
|
12808
|
+
const { words } = read(title.trim());
|
|
12809
|
+
const projectId = words.project?.value ?? chosen.id;
|
|
12810
|
+
const full = roomFor(store, projectId, status ?? "todo");
|
|
12477
12811
|
if (full) return onFull(full);
|
|
12478
12812
|
onDone(store.getState().addTask({
|
|
12479
|
-
projectId
|
|
12480
|
-
title: title.trim(),
|
|
12813
|
+
projectId,
|
|
12814
|
+
title: words.title || title.trim(),
|
|
12481
12815
|
description: "",
|
|
12482
12816
|
status: status ?? "todo",
|
|
12483
|
-
priority: "medium",
|
|
12484
|
-
assigneeId: me || null,
|
|
12485
|
-
dueDate: null
|
|
12817
|
+
priority: words.priority?.value ?? "medium",
|
|
12818
|
+
assigneeId: words.owner?.value ?? (me || null),
|
|
12819
|
+
dueDate: words.due?.value ?? null
|
|
12486
12820
|
}));
|
|
12487
12821
|
}
|
|
12488
12822
|
})
|
|
@@ -13298,6 +13632,36 @@ function applyDock(actions, projectId, changes) {
|
|
|
13298
13632
|
return made;
|
|
13299
13633
|
}
|
|
13300
13634
|
|
|
13635
|
+
// src/lib/time-limits.ts
|
|
13636
|
+
var MAX_ENTRY_MINUTES;
|
|
13637
|
+
var init_time_limits = __esmMin((() => {
|
|
13638
|
+
MAX_ENTRY_MINUTES = 1440;
|
|
13639
|
+
}));
|
|
13640
|
+
|
|
13641
|
+
// src/lib/time-log.ts
|
|
13642
|
+
/** "1h 30m", "45m", "12h". */
|
|
13643
|
+
function formatMinutes(total) {
|
|
13644
|
+
const n = Math.max(0, Math.round(total));
|
|
13645
|
+
const h = Math.floor(n / 60);
|
|
13646
|
+
const m = n % 60;
|
|
13647
|
+
if (h === 0) return `${m}m`;
|
|
13648
|
+
return m === 0 ? `${h}h` : `${h}h ${m}m`;
|
|
13649
|
+
}
|
|
13650
|
+
/** Hours with up to two decimals, for spreadsheets: 90 → 1.5. */
|
|
13651
|
+
function hours(minutes) {
|
|
13652
|
+
return Math.round(minutes / 60 * 100) / 100;
|
|
13653
|
+
}
|
|
13654
|
+
/** All the time on one task. */
|
|
13655
|
+
function taskMinutes(task) {
|
|
13656
|
+
let n = 0;
|
|
13657
|
+
for (const e of task.time ?? []) n += e.minutes;
|
|
13658
|
+
return n;
|
|
13659
|
+
}
|
|
13660
|
+
/** A task's entries, newest day first. */
|
|
13661
|
+
function entriesNewestFirst(task) {
|
|
13662
|
+
return [...task.time ?? []].sort((a, b) => (b.on + b.at).localeCompare(a.on + a.at));
|
|
13663
|
+
}
|
|
13664
|
+
|
|
13301
13665
|
// node_modules/zod/v4/core/util.js
|
|
13302
13666
|
function getEnumValues(entries) {
|
|
13303
13667
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
@@ -18837,7 +19201,8 @@ function tasksSheetCsv(data, rows) {
|
|
|
18837
19201
|
"Done",
|
|
18838
19202
|
"Blocked by",
|
|
18839
19203
|
"Created",
|
|
18840
|
-
"Description"
|
|
19204
|
+
"Description",
|
|
19205
|
+
"Hours logged"
|
|
18841
19206
|
], rows.map((t) => {
|
|
18842
19207
|
const project = projects.get(t.projectId);
|
|
18843
19208
|
const program = project ? programs.get(project.programId) : void 0;
|
|
@@ -18853,7 +19218,8 @@ function tasksSheetCsv(data, rows) {
|
|
|
18853
19218
|
t.status === "done" ? "yes" : "no",
|
|
18854
19219
|
blockerIds(t).map((id) => tasks.get(id)).filter((b) => Boolean(b)).map(keyOf).join("; "),
|
|
18855
19220
|
t.createdAt?.slice(0, 10) ?? "",
|
|
18856
|
-
t.description
|
|
19221
|
+
t.description,
|
|
19222
|
+
t.time?.length ? hours(taskMinutes(t)) : ""
|
|
18857
19223
|
];
|
|
18858
19224
|
}));
|
|
18859
19225
|
}
|
|
@@ -19508,6 +19874,16 @@ var init_fog = __esmMin((() => {
|
|
|
19508
19874
|
}));
|
|
19509
19875
|
|
|
19510
19876
|
// src/lib/mcp/tools.ts
|
|
19877
|
+
/**
|
|
19878
|
+
* What an agent can read about a harbor, as MCP tools. Each tool is a pure
|
|
19879
|
+
* function of one harbor's document (health already stamped) and who is
|
|
19880
|
+
* asking, so it is tested against the demo without a server. The route in
|
|
19881
|
+
* src/routes/api/mcp.ts signs the caller in, picks the harbor and loads it.
|
|
19882
|
+
*
|
|
19883
|
+
* Read only: nothing here changes a workspace. Answers carry ids so an agent
|
|
19884
|
+
* can ask about one thing next, and every list is capped and pageable, since
|
|
19885
|
+
* a harbor can hold thousands of tasks.
|
|
19886
|
+
*/
|
|
19511
19887
|
init_thanks();
|
|
19512
19888
|
init_task_keys();
|
|
19513
19889
|
init_brief();
|
|
@@ -19537,6 +19913,20 @@ init_passage();
|
|
|
19537
19913
|
init_board();
|
|
19538
19914
|
init_holidays();
|
|
19539
19915
|
init_soundings();
|
|
19916
|
+
/** A task's logged time for get_task: the total and the latest entries. */
|
|
19917
|
+
function timeOut(t, people) {
|
|
19918
|
+
const entries = entriesNewestFirst(t);
|
|
19919
|
+
return {
|
|
19920
|
+
total: formatMinutes(taskMinutes(t)),
|
|
19921
|
+
minutes: taskMinutes(t),
|
|
19922
|
+
latest: entries.slice(0, 10).map((e) => ({
|
|
19923
|
+
on: e.on,
|
|
19924
|
+
by: nameOf$2(people, e.personId),
|
|
19925
|
+
time: formatMinutes(e.minutes),
|
|
19926
|
+
...e.note ? { note: e.note } : {}
|
|
19927
|
+
}))
|
|
19928
|
+
};
|
|
19929
|
+
}
|
|
19540
19930
|
/**
|
|
19541
19931
|
* Where the website opens one task or project, switching to its harbor
|
|
19542
19932
|
* first (src/routes/open.tsx). Nothing without an origin.
|
|
@@ -20236,7 +20626,7 @@ const HARBOR_TOOLS = [
|
|
|
20236
20626
|
tool({
|
|
20237
20627
|
name: "get_task",
|
|
20238
20628
|
title: "Task details",
|
|
20239
|
-
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), and its url on the website." + DATA_NOTE,
|
|
20629
|
+
description: "One task: its owner (assignee) and anyone also on it (alsoOn), notes, checklist, its files, what it waits on, what waits on it, its latest comments, the earliest due date its owner's queue suggests (pilotDue: fits says whether the due date holds; absent with no owner, an agent owner, or once done), its place in its board column (`position`, from 1 at the top), time logged on it (timeLogged: the total and the latest entries, when there is any), and its url on the website." + DATA_NOTE,
|
|
20240
20630
|
input: object({
|
|
20241
20631
|
harbor: harborArg,
|
|
20242
20632
|
task: string().min(1).max(100).describe("Task id or key (\"RR-42\"), from search_tasks or my_work.")
|
|
@@ -20285,6 +20675,7 @@ const HARBOR_TOOLS = [
|
|
|
20285
20675
|
step: i.title,
|
|
20286
20676
|
done: i.done
|
|
20287
20677
|
})),
|
|
20678
|
+
...t.time?.length ? { timeLogged: timeOut(t, people) } : {},
|
|
20288
20679
|
waitingOn: blockersOf(t, data.tasks).map(brief),
|
|
20289
20680
|
waitedOnBy: waiters.map(brief),
|
|
20290
20681
|
...t.sounding ? { sounding: soundingOut(data, t, me(ctx)?.id ?? null, ctx.now) } : {},
|
|
@@ -22876,6 +23267,37 @@ function tasksSlice({ set, get, write }) {
|
|
|
22876
23267
|
checklist: (t.checklist ?? []).filter((i) => i.id !== itemId)
|
|
22877
23268
|
} : t) });
|
|
22878
23269
|
},
|
|
23270
|
+
logTime: (taskId, entry) => {
|
|
23271
|
+
const minutes = Math.round(entry.minutes);
|
|
23272
|
+
if (!Number.isFinite(minutes) || minutes < 1 || minutes > 1440) return null;
|
|
23273
|
+
const task = get().tasks.find((t) => t.id === taskId);
|
|
23274
|
+
if (!task || (task.time?.length ?? 0) >= 500) return null;
|
|
23275
|
+
const personId = entry.personId ?? get().currentUserId;
|
|
23276
|
+
if (!personId) return null;
|
|
23277
|
+
const id = uid("tm");
|
|
23278
|
+
const note = entry.note?.trim().slice(0, 200);
|
|
23279
|
+
const row = {
|
|
23280
|
+
id,
|
|
23281
|
+
personId,
|
|
23282
|
+
minutes,
|
|
23283
|
+
on: entry.on ?? todayISO(),
|
|
23284
|
+
...note ? { note } : {},
|
|
23285
|
+
at: stamp()
|
|
23286
|
+
};
|
|
23287
|
+
set({ tasks: get().tasks.map((t) => t.id === taskId ? {
|
|
23288
|
+
...t,
|
|
23289
|
+
touchedAt: stamp(),
|
|
23290
|
+
time: [...t.time ?? [], row]
|
|
23291
|
+
} : t) });
|
|
23292
|
+
return id;
|
|
23293
|
+
},
|
|
23294
|
+
removeTime: (taskId, entryId) => {
|
|
23295
|
+
set({ tasks: get().tasks.map((t) => t.id === taskId && t.time?.some((e) => e.id === entryId) ? {
|
|
23296
|
+
...t,
|
|
23297
|
+
touchedAt: stamp(),
|
|
23298
|
+
time: t.time.filter((e) => e.id !== entryId)
|
|
23299
|
+
} : t) });
|
|
23300
|
+
},
|
|
22879
23301
|
addAttachment: (a) => {
|
|
22880
23302
|
const id = a.id ?? uid("f");
|
|
22881
23303
|
const row = {
|
|
@@ -23099,6 +23521,7 @@ var init_tasks = __esmMin((() => {
|
|
|
23099
23521
|
init_automations();
|
|
23100
23522
|
init_focus();
|
|
23101
23523
|
init_format();
|
|
23524
|
+
init_time_limits();
|
|
23102
23525
|
init_soundings();
|
|
23103
23526
|
init_clock();
|
|
23104
23527
|
init_utils();
|