@harborppm/cli 0.69.0 → 0.70.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/dist/app.mjs +474 -202
- package/lib/format.mjs +2 -1
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -4858,9 +4858,216 @@ var init_board = __esmMin((() => {
|
|
|
4858
4858
|
];
|
|
4859
4859
|
}));
|
|
4860
4860
|
|
|
4861
|
+
//#endregion
|
|
4862
|
+
//#region src/lib/first-voyage.ts
|
|
4863
|
+
function isVoyageProject(projectId) {
|
|
4864
|
+
return projectId === VOYAGE_PROJECT_ID;
|
|
4865
|
+
}
|
|
4866
|
+
/** The harbor's projects without First voyage: the work people brought. */
|
|
4867
|
+
function ownProjects(projects) {
|
|
4868
|
+
return projects.filter((p) => !isVoyageProject(p.id));
|
|
4869
|
+
}
|
|
4870
|
+
var VOYAGE_PROJECT_ID, STEPS, voyageTaskId, VOYAGE_NO_FORECAST, SEEDED;
|
|
4871
|
+
var init_first_voyage = __esmMin((() => {
|
|
4872
|
+
VOYAGE_PROJECT_ID = "voyage";
|
|
4873
|
+
STEPS = [
|
|
4874
|
+
{
|
|
4875
|
+
step: "task",
|
|
4876
|
+
title: "Add a task of your own",
|
|
4877
|
+
description: "Press New, or the + at the top of a column on any board. Anything real will do. This ticks itself off when you add one."
|
|
4878
|
+
},
|
|
4879
|
+
{
|
|
4880
|
+
step: "move",
|
|
4881
|
+
title: "Move a task to In progress",
|
|
4882
|
+
description: "Open Board above and drag a card along, or change its status in the task's panel. This one counts too. It ticks itself off."
|
|
4883
|
+
},
|
|
4884
|
+
{
|
|
4885
|
+
step: "crew",
|
|
4886
|
+
title: "Bring someone aboard, or tick this to skip",
|
|
4887
|
+
description: "Invite from your menu or from People. Harbor is $49 a month for the whole crew, not per seat. Ticks itself off when someone's in People."
|
|
4888
|
+
},
|
|
4889
|
+
{
|
|
4890
|
+
step: "terminal",
|
|
4891
|
+
title: "Try Harbor in your terminal",
|
|
4892
|
+
description: "Run `npx @harborppm/cli mine` (Node 22 or later) and sign in when your browser asks. `harbor` on its own opens the whole harbor full screen. Ticks itself off once the terminal is connected; skip it if a terminal isn't your thing."
|
|
4893
|
+
},
|
|
4894
|
+
{
|
|
4895
|
+
step: "agent",
|
|
4896
|
+
title: "Ask an AI app to add a task for you",
|
|
4897
|
+
description: "Connect Claude, or any app that speaks MCP, at harborppm.com/ai, then ask it to add a task here. Harbor asks you first and History names the app. Ticks itself off once an app is connected."
|
|
4898
|
+
},
|
|
4899
|
+
{
|
|
4900
|
+
step: "wrap",
|
|
4901
|
+
title: "Mark this project Done, or delete it",
|
|
4902
|
+
description: "That's the voyage. Set its health to Done to put it away, or delete it from Details. Your own work stays."
|
|
4903
|
+
}
|
|
4904
|
+
];
|
|
4905
|
+
voyageTaskId = (step) => `voyage-${step}`;
|
|
4906
|
+
VOYAGE_NO_FORECAST = "It's practice, not the crew's pace, so there's nothing to forecast.";
|
|
4907
|
+
SEEDED = new Set(STEPS.map((s) => voyageTaskId(s.step)));
|
|
4908
|
+
}));
|
|
4909
|
+
|
|
4910
|
+
//#endregion
|
|
4911
|
+
//#region src/lib/passage.ts
|
|
4912
|
+
/** Hours as a person would say them: "2h", "1.5h", "30m". */
|
|
4913
|
+
function hoursText(h) {
|
|
4914
|
+
if (h > 0 && h < 1) return `${Math.round(h * 60)}m`;
|
|
4915
|
+
return `${Math.round(h * 10) / 10}h`;
|
|
4916
|
+
}
|
|
4917
|
+
/** Hours kept to the half hour, within what one day can take. */
|
|
4918
|
+
function clampHours(h) {
|
|
4919
|
+
if (!Number.isFinite(h)) return 2;
|
|
4920
|
+
return Math.min(12, Math.max(MIN_HOURS, Math.round(h * 2) / 2));
|
|
4921
|
+
}
|
|
4922
|
+
function dayHoursOf(person) {
|
|
4923
|
+
const h = person?.dayHours;
|
|
4924
|
+
return typeof h === "number" && h > 0 ? Math.min(16, h) : 7;
|
|
4925
|
+
}
|
|
4926
|
+
/** The Monday of the week a day falls in (a Saturday or Sunday looks ahead to the next week). */
|
|
4927
|
+
function weekOf(day = todayISO()) {
|
|
4928
|
+
const d = at$1(day);
|
|
4929
|
+
const wd = d.getDay();
|
|
4930
|
+
const back = wd === 0 ? -1 : wd === 6 ? -2 : wd - 1;
|
|
4931
|
+
return iso$3(addDays(d, -back));
|
|
4932
|
+
}
|
|
4933
|
+
/** Monday to Friday of the week starting `monday`. */
|
|
4934
|
+
function weekDays(monday) {
|
|
4935
|
+
const d = at$1(monday);
|
|
4936
|
+
return [
|
|
4937
|
+
0,
|
|
4938
|
+
1,
|
|
4939
|
+
2,
|
|
4940
|
+
3,
|
|
4941
|
+
4
|
|
4942
|
+
].map((i) => iso$3(addDays(d, i)));
|
|
4943
|
+
}
|
|
4944
|
+
function shiftWeek(monday, weeks) {
|
|
4945
|
+
return iso$3(addDays(at$1(monday), weeks * 7));
|
|
4946
|
+
}
|
|
4947
|
+
/** Whether a week should read back how it went: from its Friday on. */
|
|
4948
|
+
function isReviewTime(monday, today = todayISO()) {
|
|
4949
|
+
return today >= weekDays(monday)[4];
|
|
4950
|
+
}
|
|
4951
|
+
/** One person's placements, every week. */
|
|
4952
|
+
function placementsOf(data, personId) {
|
|
4953
|
+
return (data.reflections ?? []).filter((r) => isPassage(r) && r.authorId === personId);
|
|
4954
|
+
}
|
|
4955
|
+
/** The week as its owner plans it. Placements whose task is gone are left out. */
|
|
4956
|
+
function weekPlan(data, personId, monday) {
|
|
4957
|
+
const tasks = byId(data.tasks);
|
|
4958
|
+
const dates = weekDays(monday);
|
|
4959
|
+
const dayHours = dayHoursOf(data.people.find((p) => p.id === personId));
|
|
4960
|
+
const inWeek = placementsOf(data, personId).filter((r) => r.date && dates.includes(r.date) && r.taskId && tasks.has(r.taskId));
|
|
4961
|
+
const days = dates.map((date) => {
|
|
4962
|
+
const items = inWeek.filter((r) => r.date === date).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).map((r) => ({
|
|
4963
|
+
id: r.id,
|
|
4964
|
+
task: tasks.get(r.taskId),
|
|
4965
|
+
date,
|
|
4966
|
+
hours: clampHours(r.hours ?? 2)
|
|
4967
|
+
}));
|
|
4968
|
+
const hours = items.reduce((sum, i) => sum + i.hours, 0);
|
|
4969
|
+
return {
|
|
4970
|
+
date,
|
|
4971
|
+
items,
|
|
4972
|
+
hours,
|
|
4973
|
+
over: hours > dayHours
|
|
4974
|
+
};
|
|
4975
|
+
});
|
|
4976
|
+
const placed = new Set(inWeek.map((r) => r.taskId));
|
|
4977
|
+
const open = data.tasks.filter((t) => t.assigneeId === personId && t.status !== "done" && !placed.has(t.id) && !isVoyageProject(t.projectId));
|
|
4978
|
+
const unplanned = open.filter((t) => t.status !== "review").sort(byDue);
|
|
4979
|
+
const inReview = open.filter((t) => t.status === "review").sort(byDue);
|
|
4980
|
+
return {
|
|
4981
|
+
monday,
|
|
4982
|
+
days,
|
|
4983
|
+
dayHours,
|
|
4984
|
+
planned: days.reduce((sum, d) => sum + d.hours, 0),
|
|
4985
|
+
capacity: dayHours * 5,
|
|
4986
|
+
unplanned,
|
|
4987
|
+
inReview
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4990
|
+
/** Planned against done for a week: what was on the plan, what got finished, and what came in besides. */
|
|
4991
|
+
function weekReview(data, personId, monday) {
|
|
4992
|
+
const plan = weekPlan(data, personId, monday);
|
|
4993
|
+
const first = plan.days[0].date;
|
|
4994
|
+
const sunday = iso$3(addDays(at$1(monday), 6));
|
|
4995
|
+
const doneInWeek = (t) => t.status === "done" && Boolean(t.completedAt) && todayISO(new Date(t.completedAt)) >= first && todayISO(new Date(t.completedAt)) <= sunday;
|
|
4996
|
+
const hoursBy = /* @__PURE__ */ new Map();
|
|
4997
|
+
for (const d of plan.days) for (const i of d.items) {
|
|
4998
|
+
const had = hoursBy.get(i.task.id);
|
|
4999
|
+
hoursBy.set(i.task.id, {
|
|
5000
|
+
task: i.task,
|
|
5001
|
+
hours: (had?.hours ?? 0) + i.hours
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
const planned = [...hoursBy.values()].map(({ task, hours }) => ({
|
|
5005
|
+
task,
|
|
5006
|
+
hours,
|
|
5007
|
+
done: task.status === "done" && (!task.completedAt || todayISO(new Date(task.completedAt)) <= sunday)
|
|
5008
|
+
}));
|
|
5009
|
+
const alsoDone = data.tasks.filter((t) => t.assigneeId === personId && doneInWeek(t) && !hoursBy.has(t.id) && !isVoyageProject(t.projectId)).sort((a, b) => (a.completedAt ?? "").localeCompare(b.completedAt ?? ""));
|
|
5010
|
+
return {
|
|
5011
|
+
monday,
|
|
5012
|
+
planned,
|
|
5013
|
+
doneCount: planned.filter((p) => p.done).length,
|
|
5014
|
+
alsoDone,
|
|
5015
|
+
plannedHours: plan.planned,
|
|
5016
|
+
doneHours: planned.filter((p) => p.done).reduce((sum, p) => sum + p.hours, 0)
|
|
5017
|
+
};
|
|
5018
|
+
}
|
|
5019
|
+
/** A line for the week's header: "32h of 35h planned", or how full a day is. */
|
|
5020
|
+
function plannedText(planned, capacity) {
|
|
5021
|
+
return `${hoursText(planned)} of ${hoursText(capacity)} planned`;
|
|
5022
|
+
}
|
|
5023
|
+
/** The one line the Friday view leads with. */
|
|
5024
|
+
function reviewText(r) {
|
|
5025
|
+
if (r.planned.length === 0) return r.alsoDone.length ? `Nothing was planned, and you finished ${r.alsoDone.length}.` : "Nothing was planned this week.";
|
|
5026
|
+
const also = r.alsoDone.length ? `, and ${r.alsoDone.length} more besides` : "";
|
|
5027
|
+
return `You finished ${r.doneCount} of the ${r.planned.length} you planned${also}.`;
|
|
5028
|
+
}
|
|
5029
|
+
/** Placements of this person's from before the weeks kept, which a new plan clears. */
|
|
5030
|
+
function stalePlacements(data, personId, today = todayISO()) {
|
|
5031
|
+
const cutoff = shiftWeek(weekOf(today), -12);
|
|
5032
|
+
return new Set(placementsOf(data, personId).filter((r) => (r.date ?? "") < cutoff).map((r) => r.id));
|
|
5033
|
+
}
|
|
5034
|
+
var DAY_HOURS, DEFAULT_HOURS$1, MIN_HOURS, MAX_HOURS, HOUR_CHOICES, isPassage, iso$3, at$1, byDue, DAY_WORDS;
|
|
5035
|
+
var init_passage = __esmMin((() => {
|
|
5036
|
+
init_date_fns();
|
|
5037
|
+
init_date_text();
|
|
5038
|
+
init_format();
|
|
5039
|
+
init_indexes();
|
|
5040
|
+
init_first_voyage();
|
|
5041
|
+
DAY_HOURS = 7;
|
|
5042
|
+
DEFAULT_HOURS$1 = 2;
|
|
5043
|
+
MIN_HOURS = .5;
|
|
5044
|
+
MAX_HOURS = 12;
|
|
5045
|
+
HOUR_CHOICES = [
|
|
5046
|
+
.5,
|
|
5047
|
+
1,
|
|
5048
|
+
2,
|
|
5049
|
+
3,
|
|
5050
|
+
4,
|
|
5051
|
+
6,
|
|
5052
|
+
8
|
|
5053
|
+
];
|
|
5054
|
+
isPassage = (r) => r.kind === "passage";
|
|
5055
|
+
iso$3 = (d) => todayISO(d);
|
|
5056
|
+
at$1 = (day) => parseIso(`${day}T12:00:00`);
|
|
5057
|
+
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999");
|
|
5058
|
+
DAY_WORDS = [
|
|
5059
|
+
"mon",
|
|
5060
|
+
"tue",
|
|
5061
|
+
"wed",
|
|
5062
|
+
"thu",
|
|
5063
|
+
"fri"
|
|
5064
|
+
];
|
|
5065
|
+
}));
|
|
5066
|
+
|
|
4861
5067
|
//#endregion
|
|
4862
5068
|
//#region src/lib/keep.ts
|
|
4863
5069
|
init_board();
|
|
5070
|
+
init_passage();
|
|
4864
5071
|
/** Whole days left in the trial, rounded up: 30 on the first day, 1 on the last. 0 once it's over. */
|
|
4865
5072
|
function trialDaysLeft(trialEndsAt, now = /* @__PURE__ */ new Date()) {
|
|
4866
5073
|
if (!trialEndsAt) return 0;
|
|
@@ -5295,7 +5502,7 @@ function roadmapWindow(zoom, today, shift = 0) {
|
|
|
5295
5502
|
};
|
|
5296
5503
|
}
|
|
5297
5504
|
/** Where a day falls in the window, 0–1, or null outside it. */
|
|
5298
|
-
function at
|
|
5505
|
+
function at(iso, w) {
|
|
5299
5506
|
if (!iso) return null;
|
|
5300
5507
|
if (iso < w.start || iso >= w.end) return null;
|
|
5301
5508
|
return differenceInCalendarDays(day$4(iso), day$4(w.start)) / w.days;
|
|
@@ -5582,6 +5789,7 @@ var init_work = __esmMin((() => {
|
|
|
5582
5789
|
//#region src/tui/model.ts
|
|
5583
5790
|
init_date_fns();
|
|
5584
5791
|
init_date_text();
|
|
5792
|
+
init_first_voyage();
|
|
5585
5793
|
init_indexes();
|
|
5586
5794
|
init_tide();
|
|
5587
5795
|
init_format();
|
|
@@ -5762,7 +5970,7 @@ function roadmapCells(start, end, milestones, w, width) {
|
|
|
5762
5970
|
if (s.clippedEnd) cells[b - 1] = "›";
|
|
5763
5971
|
}
|
|
5764
5972
|
for (const m of milestones) {
|
|
5765
|
-
const pos = at
|
|
5973
|
+
const pos = at(m.due, w);
|
|
5766
5974
|
if (pos == null) continue;
|
|
5767
5975
|
cells[Math.min(width - 1, Math.floor(pos * width))] = m.done ? "◇" : "◆";
|
|
5768
5976
|
}
|
|
@@ -5805,6 +6013,7 @@ function filterTasks(tasks, f) {
|
|
|
5805
6013
|
* there's no forecast (all done, paused, or nothing finished lately).
|
|
5806
6014
|
*/
|
|
5807
6015
|
function tideLine(data, project, today) {
|
|
6016
|
+
if (isVoyageProject(project.id)) return null;
|
|
5808
6017
|
const tasks = tasksOf(data.tasks, project.id);
|
|
5809
6018
|
const tide = tideForecast(project, tasks, parseIso(`${today}T12:00:00`), tideOptions(data));
|
|
5810
6019
|
if (tide.kind === "no-pace") return {
|
|
@@ -6218,6 +6427,59 @@ function slipLine(data, project, today) {
|
|
|
6218
6427
|
if (!cost?.likely || cost.likely.daysLate <= 0) return null;
|
|
6219
6428
|
return slipHeadline(cost);
|
|
6220
6429
|
}
|
|
6430
|
+
/**
|
|
6431
|
+
* Your week as the website lays it out (passage.ts): each day with what's on
|
|
6432
|
+
* it against the hours you work, then what's yours still to place.
|
|
6433
|
+
*/
|
|
6434
|
+
function weekLines(data, me, monday, today) {
|
|
6435
|
+
const plan = weekPlan(data, me, monday);
|
|
6436
|
+
const lines = [];
|
|
6437
|
+
for (const d of plan.days) {
|
|
6438
|
+
lines.push({
|
|
6439
|
+
kind: "day",
|
|
6440
|
+
date: d.date,
|
|
6441
|
+
label: format(parseIso(`${d.date}T12:00:00`), "EEE MMM d"),
|
|
6442
|
+
load: d.items.length ? `${hoursText(d.hours)} of ${hoursText(plan.dayHours)}${d.over ? ` · too full by ${hoursText(d.hours - plan.dayHours)}` : ""}` : "nothing planned",
|
|
6443
|
+
over: d.over,
|
|
6444
|
+
today: d.date === today
|
|
6445
|
+
});
|
|
6446
|
+
for (const placement of d.items) lines.push({
|
|
6447
|
+
kind: "placed",
|
|
6448
|
+
placement
|
|
6449
|
+
});
|
|
6450
|
+
}
|
|
6451
|
+
lines.push({
|
|
6452
|
+
kind: "head",
|
|
6453
|
+
text: `Yours to place · ${plan.unplanned.length}`
|
|
6454
|
+
});
|
|
6455
|
+
if (plan.unplanned.length === 0) lines.push({
|
|
6456
|
+
kind: "note",
|
|
6457
|
+
text: "Everything of yours is on a day."
|
|
6458
|
+
});
|
|
6459
|
+
for (const task of plan.unplanned) lines.push({
|
|
6460
|
+
kind: "open",
|
|
6461
|
+
task
|
|
6462
|
+
});
|
|
6463
|
+
if (plan.inReview.length) lines.push({
|
|
6464
|
+
kind: "note",
|
|
6465
|
+
text: `${plan.inReview.length} in review, waiting on someone else, left out.`
|
|
6466
|
+
});
|
|
6467
|
+
return {
|
|
6468
|
+
plan,
|
|
6469
|
+
lines
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
/** How the week went, as lines to read: the website's Friday view. */
|
|
6473
|
+
function weekReviewLines(data, me, monday) {
|
|
6474
|
+
const r = weekReview(data, me, monday);
|
|
6475
|
+
const out = [reviewText(r)];
|
|
6476
|
+
if (r.planned.length) out.push(`${hoursText(r.doneHours)} of the ${hoursText(r.plannedHours)} you planned is done.`);
|
|
6477
|
+
if (r.planned.length) out.push("", "Planned");
|
|
6478
|
+
for (const p of r.planned) out.push(` ${p.done ? "✓" : "○"} ${keyOf$1(data, p.task).padEnd(7)}${p.task.title} ${hoursText(p.hours)}`);
|
|
6479
|
+
if (r.alsoDone.length) out.push("", "Also done");
|
|
6480
|
+
for (const t of r.alsoDone) out.push(` ✓ ${keyOf$1(data, t).padEnd(7)}${t.title}`);
|
|
6481
|
+
return out;
|
|
6482
|
+
}
|
|
6221
6483
|
|
|
6222
6484
|
//#endregion
|
|
6223
6485
|
//#region src/tui/theme.ts
|
|
@@ -6749,7 +7011,7 @@ function programIsBlocked(data, program) {
|
|
|
6749
7011
|
}
|
|
6750
7012
|
function daysOverdue(dueDate, now) {
|
|
6751
7013
|
if (!dueDate) return 0;
|
|
6752
|
-
const today = iso$
|
|
7014
|
+
const today = iso$2(now);
|
|
6753
7015
|
if (today !== overdueDay) {
|
|
6754
7016
|
overdueDay = today;
|
|
6755
7017
|
overdueByDue.clear();
|
|
@@ -6768,7 +7030,7 @@ function daysOverdue(dueDate, now) {
|
|
|
6768
7030
|
}
|
|
6769
7031
|
function daysUntil$1(date, now) {
|
|
6770
7032
|
if (!date) return null;
|
|
6771
|
-
const today = iso$
|
|
7033
|
+
const today = iso$2(now);
|
|
6772
7034
|
if (today !== untilDay) {
|
|
6773
7035
|
untilDay = today;
|
|
6774
7036
|
untilByDate.clear();
|
|
@@ -6833,7 +7095,7 @@ function effectiveProjectHealth(project, data, now) {
|
|
|
6833
7095
|
}
|
|
6834
7096
|
/** Milestones past their date and still open. */
|
|
6835
7097
|
function lateMilestonesOf(milestones, now) {
|
|
6836
|
-
const today = iso$
|
|
7098
|
+
const today = iso$2(now);
|
|
6837
7099
|
return milestones.filter((m) => !m.done && m.dueDate < today);
|
|
6838
7100
|
}
|
|
6839
7101
|
/**
|
|
@@ -6938,7 +7200,7 @@ function programLateCount(programId, data, now) {
|
|
|
6938
7200
|
for (const p of data.projects) if (p.programId === programId) late += lateTasksOf([...tasksOf(data.tasks, p.id)], now).length;
|
|
6939
7201
|
return late;
|
|
6940
7202
|
}
|
|
6941
|
-
var iso$
|
|
7203
|
+
var iso$2, overdueDay, overdueByDue, untilDay, untilByDate;
|
|
6942
7204
|
var init_derived_health = __esmMin((() => {
|
|
6943
7205
|
init_date_fns();
|
|
6944
7206
|
init_date_text();
|
|
@@ -6946,7 +7208,7 @@ var init_derived_health = __esmMin((() => {
|
|
|
6946
7208
|
init_indexes();
|
|
6947
7209
|
init_health_history();
|
|
6948
7210
|
init_clock();
|
|
6949
|
-
iso$
|
|
7211
|
+
iso$2 = (d) => {
|
|
6950
7212
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
6951
7213
|
};
|
|
6952
7214
|
overdueDay = "";
|
|
@@ -7417,7 +7679,7 @@ function briefOf(data, opts = {}) {
|
|
|
7417
7679
|
const portfolioId = opts.portfolioId ?? null;
|
|
7418
7680
|
const programs = portfolioId ? data.programs.filter((p) => p.portfolioId === portfolioId) : data.programs;
|
|
7419
7681
|
const programIds = new Set(programs.map((p) => p.id));
|
|
7420
|
-
const projects = data.projects.filter((p) => programIds.has(p.programId));
|
|
7682
|
+
const projects = ownProjects(data.projects.filter((p) => programIds.has(p.programId)));
|
|
7421
7683
|
const projectIds = new Set(projects.map((p) => p.id));
|
|
7422
7684
|
const tasks = data.tasks.filter((t) => projectIds.has(t.projectId));
|
|
7423
7685
|
const risks = data.raidItems.filter((r) => r.kind === "risk" && r.programId != null && programIds.has(r.programId));
|
|
@@ -7631,6 +7893,7 @@ var init_brief = __esmMin((() => {
|
|
|
7631
7893
|
init_decisions();
|
|
7632
7894
|
init_barnacles();
|
|
7633
7895
|
init_baseline();
|
|
7896
|
+
init_first_voyage();
|
|
7634
7897
|
BRIEF_BARNACLES_SHOWN = 5;
|
|
7635
7898
|
BRIEF_QUIET = "Nothing late or at risk. Say what shipped — or enjoy the quiet week.";
|
|
7636
7899
|
BRIEF_LATE_SHOWN = 8;
|
|
@@ -7995,6 +8258,7 @@ init_raid();
|
|
|
7995
8258
|
init_swatches();
|
|
7996
8259
|
init_task_keys();
|
|
7997
8260
|
init_format();
|
|
8261
|
+
init_first_voyage();
|
|
7998
8262
|
/** How many items of one kind a slide lists before "and N more". */
|
|
7999
8263
|
const LIST_CAP = 4;
|
|
8000
8264
|
/** When a program's changes are counted from: its last meeting, else a week back. */
|
|
@@ -8105,7 +8369,7 @@ function bridgeSlides(doc, programs, now) {
|
|
|
8105
8369
|
if (program.health === "done") continue;
|
|
8106
8370
|
const since = bridgeSince(program, now);
|
|
8107
8371
|
const milestone = doc.milestones.filter((m) => m.programId === program.id && !m.done).sort((a, b) => a.dueDate.localeCompare(b.dueDate))[0] ?? null;
|
|
8108
|
-
const projects = doc.projects.filter((p) => p.programId === program.id && p.health !== "done").sort((a, b) => rank[a.health] - rank[b.health] || a.endDate.localeCompare(b.endDate));
|
|
8372
|
+
const projects = doc.projects.filter((p) => p.programId === program.id && p.health !== "done" && !isVoyageProject(p.id)).sort((a, b) => rank[a.health] - rank[b.health] || a.endDate.localeCompare(b.endDate));
|
|
8109
8373
|
for (const project of projects) {
|
|
8110
8374
|
const changes = projectChanges(doc, project, since, now);
|
|
8111
8375
|
if (changes.length === 0) {
|
|
@@ -9752,13 +10016,13 @@ function InboxView({ active, width, height, go }) {
|
|
|
9752
10016
|
/** Open tasks for one person, bucketed by the week they are due, plus what is already late or has no date. */
|
|
9753
10017
|
function weeklyLoad(tasks, personId, now, weeks = 6, calendar) {
|
|
9754
10018
|
const mine = tasks.filter((t) => t.assigneeId === personId && t.status !== "done");
|
|
9755
|
-
const today = iso$
|
|
10019
|
+
const today = iso$1(now);
|
|
9756
10020
|
const origin = startOfWeek(now, { weekStartsOn: 1 });
|
|
9757
10021
|
const buckets = Array.from({ length: weeks }, (_, i) => {
|
|
9758
|
-
const start = iso$
|
|
10022
|
+
const start = iso$1(addDays(origin, i * 7));
|
|
9759
10023
|
const off = [];
|
|
9760
10024
|
if (calendar?.active) for (let d = 0; d < 5; d++) {
|
|
9761
|
-
const date = iso$
|
|
10025
|
+
const date = iso$1(addDays(origin, i * 7 + d));
|
|
9762
10026
|
const why = calendar.offOn(personId, date);
|
|
9763
10027
|
if (why) off.push({
|
|
9764
10028
|
date,
|
|
@@ -9793,58 +10057,14 @@ function weeklyLoad(tasks, personId, now, weeks = 6, calendar) {
|
|
|
9793
10057
|
weeks: buckets
|
|
9794
10058
|
};
|
|
9795
10059
|
}
|
|
9796
|
-
var iso$
|
|
10060
|
+
var iso$1;
|
|
9797
10061
|
var init_load = __esmMin((() => {
|
|
9798
10062
|
init_date_fns();
|
|
9799
|
-
iso$
|
|
10063
|
+
iso$1 = (d) => {
|
|
9800
10064
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
9801
10065
|
};
|
|
9802
10066
|
}));
|
|
9803
10067
|
|
|
9804
|
-
//#endregion
|
|
9805
|
-
//#region src/lib/first-voyage.ts
|
|
9806
|
-
function isVoyageProject(projectId) {
|
|
9807
|
-
return projectId === VOYAGE_PROJECT_ID;
|
|
9808
|
-
}
|
|
9809
|
-
var VOYAGE_PROJECT_ID, STEPS, voyageTaskId, SEEDED;
|
|
9810
|
-
var init_first_voyage = __esmMin((() => {
|
|
9811
|
-
VOYAGE_PROJECT_ID = "voyage";
|
|
9812
|
-
STEPS = [
|
|
9813
|
-
{
|
|
9814
|
-
step: "task",
|
|
9815
|
-
title: "Add a task of your own",
|
|
9816
|
-
description: "Press New, or the + at the top of a column on any board. Anything real will do. This ticks itself off when you add one."
|
|
9817
|
-
},
|
|
9818
|
-
{
|
|
9819
|
-
step: "move",
|
|
9820
|
-
title: "Move a task to In progress",
|
|
9821
|
-
description: "Open Board above and drag a card along, or change its status in the task's panel. This one counts too. It ticks itself off."
|
|
9822
|
-
},
|
|
9823
|
-
{
|
|
9824
|
-
step: "crew",
|
|
9825
|
-
title: "Bring someone aboard, or tick this to skip",
|
|
9826
|
-
description: "Invite from your menu or from People. Harbor is $49 a month for the whole crew, not per seat. Ticks itself off when someone's in People."
|
|
9827
|
-
},
|
|
9828
|
-
{
|
|
9829
|
-
step: "terminal",
|
|
9830
|
-
title: "Try Harbor in your terminal",
|
|
9831
|
-
description: "Run `npx @harborppm/cli mine` (Node 22 or later) and sign in when your browser asks. `harbor` on its own opens the whole harbor full screen. Ticks itself off once the terminal is connected; skip it if a terminal isn't your thing."
|
|
9832
|
-
},
|
|
9833
|
-
{
|
|
9834
|
-
step: "agent",
|
|
9835
|
-
title: "Ask an AI app to add a task for you",
|
|
9836
|
-
description: "Connect Claude, or any app that speaks MCP, at harborppm.com/ai, then ask it to add a task here. Harbor asks you first and History names the app. Ticks itself off once an app is connected."
|
|
9837
|
-
},
|
|
9838
|
-
{
|
|
9839
|
-
step: "wrap",
|
|
9840
|
-
title: "Mark this project Done, or delete it",
|
|
9841
|
-
description: "That's the voyage. Set its health to Done to put it away, or delete it from Details. Your own work stays."
|
|
9842
|
-
}
|
|
9843
|
-
];
|
|
9844
|
-
voyageTaskId = (step) => `voyage-${step}`;
|
|
9845
|
-
SEEDED = new Set(STEPS.map((s) => voyageTaskId(s.step)));
|
|
9846
|
-
}));
|
|
9847
|
-
|
|
9848
10068
|
//#endregion
|
|
9849
10069
|
//#region src/lib/handover.ts
|
|
9850
10070
|
/** Everything a person carries that someone else would have to pick up, grouped in KIND_ORDER. */
|
|
@@ -18883,7 +19103,7 @@ function reviewCsv(data, programs, now) {
|
|
|
18883
19103
|
//#endregion
|
|
18884
19104
|
//#region src/lib/crew-clocks.ts
|
|
18885
19105
|
/** Working hours when only a time zone is set. */
|
|
18886
|
-
const DEFAULT_HOURS
|
|
19106
|
+
const DEFAULT_HOURS = {
|
|
18887
19107
|
start: "09:00",
|
|
18888
19108
|
end: "17:00"
|
|
18889
19109
|
};
|
|
@@ -18957,8 +19177,8 @@ function clockOf(person, at) {
|
|
|
18957
19177
|
const zone = person?.timeZone;
|
|
18958
19178
|
if (!isZone(zone)) return null;
|
|
18959
19179
|
const wall = wallIn(zone, at);
|
|
18960
|
-
const start = minutesOf(person?.workStart) ?? minutesOf(DEFAULT_HOURS
|
|
18961
|
-
let end = minutesOf(person?.workEnd) ?? minutesOf(DEFAULT_HOURS
|
|
19180
|
+
const start = minutesOf(person?.workStart) ?? minutesOf(DEFAULT_HOURS.start);
|
|
19181
|
+
let end = minutesOf(person?.workEnd) ?? minutesOf(DEFAULT_HOURS.end);
|
|
18962
19182
|
if (end <= start) end = Math.min(1440, start + 480);
|
|
18963
19183
|
const weekday = wall.weekday >= 1 && wall.weekday <= 5;
|
|
18964
19184
|
const working = weekday && wall.minutes >= start && wall.minutes < end;
|
|
@@ -19440,150 +19660,12 @@ var init_fog = __esmMin((() => {
|
|
|
19440
19660
|
fileCache = /* @__PURE__ */ new WeakMap();
|
|
19441
19661
|
}));
|
|
19442
19662
|
|
|
19443
|
-
//#endregion
|
|
19444
|
-
//#region src/lib/passage.ts
|
|
19445
|
-
/** Hours as a person would say them: "2h", "1.5h", "30m". */
|
|
19446
|
-
function hoursText(h) {
|
|
19447
|
-
if (h > 0 && h < 1) return `${Math.round(h * 60)}m`;
|
|
19448
|
-
return `${Math.round(h * 10) / 10}h`;
|
|
19449
|
-
}
|
|
19450
|
-
/** Hours kept to the half hour, within what one day can take. */
|
|
19451
|
-
function clampHours(h) {
|
|
19452
|
-
if (!Number.isFinite(h)) return 2;
|
|
19453
|
-
return Math.min(12, Math.max(MIN_HOURS, Math.round(h * 2) / 2));
|
|
19454
|
-
}
|
|
19455
|
-
function dayHoursOf(person) {
|
|
19456
|
-
const h = person?.dayHours;
|
|
19457
|
-
return typeof h === "number" && h > 0 ? Math.min(16, h) : 7;
|
|
19458
|
-
}
|
|
19459
|
-
/** The Monday of the week a day falls in (a Saturday or Sunday looks ahead to the next week). */
|
|
19460
|
-
function weekOf(day = todayISO()) {
|
|
19461
|
-
const d = at(day);
|
|
19462
|
-
const wd = d.getDay();
|
|
19463
|
-
const back = wd === 0 ? -1 : wd === 6 ? -2 : wd - 1;
|
|
19464
|
-
return iso$1(addDays(d, -back));
|
|
19465
|
-
}
|
|
19466
|
-
/** Monday to Friday of the week starting `monday`. */
|
|
19467
|
-
function weekDays(monday) {
|
|
19468
|
-
const d = at(monday);
|
|
19469
|
-
return [
|
|
19470
|
-
0,
|
|
19471
|
-
1,
|
|
19472
|
-
2,
|
|
19473
|
-
3,
|
|
19474
|
-
4
|
|
19475
|
-
].map((i) => iso$1(addDays(d, i)));
|
|
19476
|
-
}
|
|
19477
|
-
function shiftWeek(monday, weeks) {
|
|
19478
|
-
return iso$1(addDays(at(monday), weeks * 7));
|
|
19479
|
-
}
|
|
19480
|
-
/** One person's placements, every week. */
|
|
19481
|
-
function placementsOf(data, personId) {
|
|
19482
|
-
return (data.reflections ?? []).filter((r) => isPassage(r) && r.authorId === personId);
|
|
19483
|
-
}
|
|
19484
|
-
/** The week as its owner plans it. Placements whose task is gone are left out. */
|
|
19485
|
-
function weekPlan(data, personId, monday) {
|
|
19486
|
-
const tasks = byId(data.tasks);
|
|
19487
|
-
const dates = weekDays(monday);
|
|
19488
|
-
const dayHours = dayHoursOf(data.people.find((p) => p.id === personId));
|
|
19489
|
-
const inWeek = placementsOf(data, personId).filter((r) => r.date && dates.includes(r.date) && r.taskId && tasks.has(r.taskId));
|
|
19490
|
-
const days = dates.map((date) => {
|
|
19491
|
-
const items = inWeek.filter((r) => r.date === date).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).map((r) => ({
|
|
19492
|
-
id: r.id,
|
|
19493
|
-
task: tasks.get(r.taskId),
|
|
19494
|
-
date,
|
|
19495
|
-
hours: clampHours(r.hours ?? 2)
|
|
19496
|
-
}));
|
|
19497
|
-
const hours = items.reduce((sum, i) => sum + i.hours, 0);
|
|
19498
|
-
return {
|
|
19499
|
-
date,
|
|
19500
|
-
items,
|
|
19501
|
-
hours,
|
|
19502
|
-
over: hours > dayHours
|
|
19503
|
-
};
|
|
19504
|
-
});
|
|
19505
|
-
const placed = new Set(inWeek.map((r) => r.taskId));
|
|
19506
|
-
const open = data.tasks.filter((t) => t.assigneeId === personId && t.status !== "done" && !placed.has(t.id));
|
|
19507
|
-
const unplanned = open.filter((t) => t.status !== "review").sort(byDue);
|
|
19508
|
-
const inReview = open.filter((t) => t.status === "review").sort(byDue);
|
|
19509
|
-
return {
|
|
19510
|
-
monday,
|
|
19511
|
-
days,
|
|
19512
|
-
dayHours,
|
|
19513
|
-
planned: days.reduce((sum, d) => sum + d.hours, 0),
|
|
19514
|
-
capacity: dayHours * 5,
|
|
19515
|
-
unplanned,
|
|
19516
|
-
inReview
|
|
19517
|
-
};
|
|
19518
|
-
}
|
|
19519
|
-
/** Planned against done for a week: what was on the plan, what got finished, and what came in besides. */
|
|
19520
|
-
function weekReview(data, personId, monday) {
|
|
19521
|
-
const plan = weekPlan(data, personId, monday);
|
|
19522
|
-
const first = plan.days[0].date;
|
|
19523
|
-
const sunday = iso$1(addDays(at(monday), 6));
|
|
19524
|
-
const doneInWeek = (t) => t.status === "done" && Boolean(t.completedAt) && todayISO(new Date(t.completedAt)) >= first && todayISO(new Date(t.completedAt)) <= sunday;
|
|
19525
|
-
const hoursBy = /* @__PURE__ */ new Map();
|
|
19526
|
-
for (const d of plan.days) for (const i of d.items) {
|
|
19527
|
-
const had = hoursBy.get(i.task.id);
|
|
19528
|
-
hoursBy.set(i.task.id, {
|
|
19529
|
-
task: i.task,
|
|
19530
|
-
hours: (had?.hours ?? 0) + i.hours
|
|
19531
|
-
});
|
|
19532
|
-
}
|
|
19533
|
-
const planned = [...hoursBy.values()].map(({ task, hours }) => ({
|
|
19534
|
-
task,
|
|
19535
|
-
hours,
|
|
19536
|
-
done: task.status === "done" && (!task.completedAt || todayISO(new Date(task.completedAt)) <= sunday)
|
|
19537
|
-
}));
|
|
19538
|
-
const alsoDone = data.tasks.filter((t) => t.assigneeId === personId && doneInWeek(t) && !hoursBy.has(t.id)).sort((a, b) => (a.completedAt ?? "").localeCompare(b.completedAt ?? ""));
|
|
19539
|
-
return {
|
|
19540
|
-
monday,
|
|
19541
|
-
planned,
|
|
19542
|
-
doneCount: planned.filter((p) => p.done).length,
|
|
19543
|
-
alsoDone,
|
|
19544
|
-
plannedHours: plan.planned,
|
|
19545
|
-
doneHours: planned.filter((p) => p.done).reduce((sum, p) => sum + p.hours, 0)
|
|
19546
|
-
};
|
|
19547
|
-
}
|
|
19548
|
-
/** The one line the Friday view leads with. */
|
|
19549
|
-
function reviewText(r) {
|
|
19550
|
-
if (r.planned.length === 0) return r.alsoDone.length ? `Nothing was planned, and you finished ${r.alsoDone.length}.` : "Nothing was planned this week.";
|
|
19551
|
-
const also = r.alsoDone.length ? `, and ${r.alsoDone.length} more besides` : "";
|
|
19552
|
-
return `You finished ${r.doneCount} of the ${r.planned.length} you planned${also}.`;
|
|
19553
|
-
}
|
|
19554
|
-
/** Placements of this person's from before the weeks kept, which a new plan clears. */
|
|
19555
|
-
function stalePlacements(data, personId, today = todayISO()) {
|
|
19556
|
-
const cutoff = shiftWeek(weekOf(today), -12);
|
|
19557
|
-
return new Set(placementsOf(data, personId).filter((r) => (r.date ?? "") < cutoff).map((r) => r.id));
|
|
19558
|
-
}
|
|
19559
|
-
var DAY_HOURS, DEFAULT_HOURS, MIN_HOURS, MAX_HOURS, isPassage, iso$1, at, byDue, DAY_WORDS;
|
|
19560
|
-
var init_passage = __esmMin((() => {
|
|
19561
|
-
init_date_fns();
|
|
19562
|
-
init_date_text();
|
|
19563
|
-
init_format();
|
|
19564
|
-
init_indexes();
|
|
19565
|
-
DAY_HOURS = 7;
|
|
19566
|
-
DEFAULT_HOURS = 2;
|
|
19567
|
-
MIN_HOURS = .5;
|
|
19568
|
-
MAX_HOURS = 12;
|
|
19569
|
-
isPassage = (r) => r.kind === "passage";
|
|
19570
|
-
iso$1 = (d) => todayISO(d);
|
|
19571
|
-
at = (day) => parseIso(`${day}T12:00:00`);
|
|
19572
|
-
byDue = (a, b) => (a.dueDate ?? "9999").localeCompare(b.dueDate ?? "9999");
|
|
19573
|
-
DAY_WORDS = [
|
|
19574
|
-
"mon",
|
|
19575
|
-
"tue",
|
|
19576
|
-
"wed",
|
|
19577
|
-
"thu",
|
|
19578
|
-
"fri"
|
|
19579
|
-
];
|
|
19580
|
-
}));
|
|
19581
|
-
|
|
19582
19663
|
//#endregion
|
|
19583
19664
|
//#region src/lib/mcp/tools.ts
|
|
19584
19665
|
init_thanks();
|
|
19585
19666
|
init_task_keys();
|
|
19586
19667
|
init_brief();
|
|
19668
|
+
init_first_voyage();
|
|
19587
19669
|
init_indexes();
|
|
19588
19670
|
init_tide();
|
|
19589
19671
|
init_barnacles();
|
|
@@ -19685,6 +19767,7 @@ const STATUSES = [
|
|
|
19685
19767
|
* Given the spend ledger, also what each week late costs (src/lib/slip-cost.ts).
|
|
19686
19768
|
*/
|
|
19687
19769
|
function finishForecast(project, tasks, now, spend, data) {
|
|
19770
|
+
if (isVoyageProject(project.id)) return { none: VOYAGE_NO_FORECAST };
|
|
19688
19771
|
const tide = tideForecast(project, tasks, now, data ? tideOptions(data) : {});
|
|
19689
19772
|
if (tide.kind !== "forecast") return { none: tideQuiet(tide) };
|
|
19690
19773
|
const lastDue = dueAfterFinish(tide);
|
|
@@ -29731,6 +29814,189 @@ function TodayView({ active, width, height, go }) {
|
|
|
29731
29814
|
});
|
|
29732
29815
|
}
|
|
29733
29816
|
|
|
29817
|
+
//#endregion
|
|
29818
|
+
//#region src/tui/views/week.tsx
|
|
29819
|
+
init_date_text();
|
|
29820
|
+
init_passage();
|
|
29821
|
+
const dayName = (date) => format(parseIso(`${date}T12:00:00`), "EEEE");
|
|
29822
|
+
/**
|
|
29823
|
+
* Your week, as on the website: your tasks laid on the five working days
|
|
29824
|
+
* against the hours you work, and from Friday how it went. Only you see it.
|
|
29825
|
+
* p puts a task on a day (or moves it), h sets its hours, d takes it off,
|
|
29826
|
+
* [ and ] step through the weeks, v swaps plan and how it went.
|
|
29827
|
+
*/
|
|
29828
|
+
function WeekView({ active, width, height, go }) {
|
|
29829
|
+
const { store, today } = useDeps();
|
|
29830
|
+
const tasks = useHarbor((s) => s.tasks);
|
|
29831
|
+
const projects = useHarbor((s) => s.projects);
|
|
29832
|
+
const people = useHarbor((s) => s.people);
|
|
29833
|
+
const reflections = useHarbor((s) => s.reflections);
|
|
29834
|
+
const me = useHarbor((s) => s.currentUserId);
|
|
29835
|
+
const day = today();
|
|
29836
|
+
const [monday, setMonday] = useState(() => weekOf(day));
|
|
29837
|
+
const [review, setReview] = useState(() => isReviewTime(weekOf(day), day));
|
|
29838
|
+
const [at, setAt] = useState(0);
|
|
29839
|
+
const data = useMemo(() => ({
|
|
29840
|
+
tasks,
|
|
29841
|
+
projects,
|
|
29842
|
+
people,
|
|
29843
|
+
reflections
|
|
29844
|
+
}), [
|
|
29845
|
+
tasks,
|
|
29846
|
+
projects,
|
|
29847
|
+
people,
|
|
29848
|
+
reflections
|
|
29849
|
+
]);
|
|
29850
|
+
const { plan, lines } = useMemo(() => weekLines(data, me, monday, day), [
|
|
29851
|
+
data,
|
|
29852
|
+
me,
|
|
29853
|
+
monday,
|
|
29854
|
+
day
|
|
29855
|
+
]);
|
|
29856
|
+
const read = useMemo(() => weekReviewLines(data, me, monday), [
|
|
29857
|
+
data,
|
|
29858
|
+
me,
|
|
29859
|
+
monday
|
|
29860
|
+
]);
|
|
29861
|
+
const picks = lines.flatMap((l, i) => l.kind === "placed" || l.kind === "open" ? [i] : []);
|
|
29862
|
+
const sel = Math.min(at, Math.max(0, picks.length - 1));
|
|
29863
|
+
const line = picks.length ? lines[picks[sel]] : void 0;
|
|
29864
|
+
const task = line?.kind === "placed" ? line.placement.task : line?.kind === "open" ? line.task : void 0;
|
|
29865
|
+
const dates = weekDays(monday);
|
|
29866
|
+
const dayChoices = (except) => dates.filter((d) => d !== except).map((d) => ({
|
|
29867
|
+
label: `${dayName(d)}${d === day ? " (today)" : ""}`,
|
|
29868
|
+
value: d
|
|
29869
|
+
}));
|
|
29870
|
+
const goWeek = (weeks) => {
|
|
29871
|
+
const next = weeks === 0 ? weekOf(day) : shiftWeek(monday, weeks);
|
|
29872
|
+
setMonday(next);
|
|
29873
|
+
setReview(isReviewTime(next, day) && next <= weekOf(day));
|
|
29874
|
+
setAt(0);
|
|
29875
|
+
};
|
|
29876
|
+
useInput((input, key) => {
|
|
29877
|
+
if (input === "[") return goWeek(-1);
|
|
29878
|
+
if (input === "]") return goWeek(1);
|
|
29879
|
+
if (input === "t") return goWeek(0);
|
|
29880
|
+
if (input === "v") return setReview((v) => !v);
|
|
29881
|
+
if (review) return;
|
|
29882
|
+
if (key.upArrow || input === "k") setAt(step(sel, -1, picks.length));
|
|
29883
|
+
else if (key.downArrow || input === "j") setAt(step(sel, 1, picks.length));
|
|
29884
|
+
else if (key.return && task) go.openTask(task.id);
|
|
29885
|
+
else if (input === "p" && line?.kind === "open") go.pick(`Plan “${line.task.title}” on`, dayChoices(), (d) => {
|
|
29886
|
+
if (store.getState().planTask(line.task.id, d)) go.notify(`On ${dayName(d)}. h changes its hours.`);
|
|
29887
|
+
});
|
|
29888
|
+
else if (input === "p" && line?.kind === "placed") go.pick(`Move “${line.placement.task.title}” to`, dayChoices(line.placement.date), (d) => store.getState().movePlacement(line.placement.id, d));
|
|
29889
|
+
else if (input === "h" && line?.kind === "placed") go.pick(`Hours for “${line.placement.task.title}” on ${dayName(line.placement.date)}`, HOUR_CHOICES.map((h) => ({
|
|
29890
|
+
label: hoursText(h),
|
|
29891
|
+
value: h
|
|
29892
|
+
})), (h) => store.getState().setPlacementHours(line.placement.id, h));
|
|
29893
|
+
else if ((input === "d" || key.delete) && line?.kind === "placed") {
|
|
29894
|
+
store.getState().unplan(line.placement.id);
|
|
29895
|
+
go.notify(`Took ${keyOf$1({ projects }, line.placement.task)} off ${dayName(line.placement.date)}.`);
|
|
29896
|
+
}
|
|
29897
|
+
}, { isActive: active });
|
|
29898
|
+
const title = `Your week · ${format(parseIso(`${dates[0]}T12:00:00`), "MMM d")} to ${format(parseIso(`${dates[4]}T12:00:00`), "MMM d")}`;
|
|
29899
|
+
const rows = Math.max(3, height - 4);
|
|
29900
|
+
if (review) return /* @__PURE__ */ jsxs(Box, {
|
|
29901
|
+
flexDirection: "column",
|
|
29902
|
+
width,
|
|
29903
|
+
height,
|
|
29904
|
+
paddingX: 1,
|
|
29905
|
+
children: [
|
|
29906
|
+
/* @__PURE__ */ jsx(Heading, { children: `${title} · How it went` }),
|
|
29907
|
+
read.slice(0, rows).map((l, i) => /* @__PURE__ */ jsx(Text, {
|
|
29908
|
+
wrap: "truncate-end",
|
|
29909
|
+
color: i === 0 ? void 0 : theme.muted,
|
|
29910
|
+
children: l || " "
|
|
29911
|
+
}, i)),
|
|
29912
|
+
/* @__PURE__ */ jsx(Box, {
|
|
29913
|
+
marginTop: 1,
|
|
29914
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
29915
|
+
color: theme.muted,
|
|
29916
|
+
wrap: "truncate-end",
|
|
29917
|
+
children: "Only you see this · v the plan · [ ] other weeks · t this week"
|
|
29918
|
+
})
|
|
29919
|
+
})
|
|
29920
|
+
]
|
|
29921
|
+
});
|
|
29922
|
+
const view = windowOf(lines, picks[sel] ?? 0, rows);
|
|
29923
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
29924
|
+
flexDirection: "column",
|
|
29925
|
+
width,
|
|
29926
|
+
height,
|
|
29927
|
+
paddingX: 1,
|
|
29928
|
+
children: [
|
|
29929
|
+
/* @__PURE__ */ jsx(Heading, {
|
|
29930
|
+
right: plannedText(plan.planned, plan.capacity),
|
|
29931
|
+
children: title
|
|
29932
|
+
}),
|
|
29933
|
+
view.items.map((l, i) => {
|
|
29934
|
+
const selected = active && view.start + i === picks[sel];
|
|
29935
|
+
if (l.kind === "day") return /* @__PURE__ */ jsxs(Text, {
|
|
29936
|
+
wrap: "truncate-end",
|
|
29937
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
29938
|
+
bold: true,
|
|
29939
|
+
children: l.label
|
|
29940
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
29941
|
+
color: l.over ? theme.warn : theme.muted,
|
|
29942
|
+
children: [
|
|
29943
|
+
" ",
|
|
29944
|
+
l.load,
|
|
29945
|
+
l.today ? " · today" : ""
|
|
29946
|
+
]
|
|
29947
|
+
})]
|
|
29948
|
+
}, l.date);
|
|
29949
|
+
if (l.kind === "placed") return /* @__PURE__ */ jsxs(Box, {
|
|
29950
|
+
width: width - 2,
|
|
29951
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
29952
|
+
flexGrow: 1,
|
|
29953
|
+
flexShrink: 1,
|
|
29954
|
+
children: /* @__PURE__ */ jsx(TaskLine, {
|
|
29955
|
+
task: l.placement.task,
|
|
29956
|
+
projects,
|
|
29957
|
+
today: day,
|
|
29958
|
+
selected,
|
|
29959
|
+
width: width - 8,
|
|
29960
|
+
showProject: false
|
|
29961
|
+
})
|
|
29962
|
+
}), /* @__PURE__ */ jsxs(Text, {
|
|
29963
|
+
color: theme.muted,
|
|
29964
|
+
children: [" ", hoursText(l.placement.hours).padStart(4)]
|
|
29965
|
+
})]
|
|
29966
|
+
}, l.placement.id);
|
|
29967
|
+
if (l.kind === "open") return /* @__PURE__ */ jsx(TaskLine, {
|
|
29968
|
+
task: l.task,
|
|
29969
|
+
projects,
|
|
29970
|
+
today: day,
|
|
29971
|
+
selected,
|
|
29972
|
+
width: width - 2
|
|
29973
|
+
}, `open-${l.task.id}`);
|
|
29974
|
+
if (l.kind === "head") return /* @__PURE__ */ jsx(Box, {
|
|
29975
|
+
marginTop: view.start + i === 0 ? 0 : 1,
|
|
29976
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
29977
|
+
bold: true,
|
|
29978
|
+
wrap: "truncate-end",
|
|
29979
|
+
children: l.text
|
|
29980
|
+
})
|
|
29981
|
+
}, `head-${i}`);
|
|
29982
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
29983
|
+
color: theme.muted,
|
|
29984
|
+
wrap: "truncate-end",
|
|
29985
|
+
children: [" ", l.text]
|
|
29986
|
+
}, `note-${i}`);
|
|
29987
|
+
}),
|
|
29988
|
+
/* @__PURE__ */ jsx(Box, {
|
|
29989
|
+
marginTop: 1,
|
|
29990
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
29991
|
+
color: theme.muted,
|
|
29992
|
+
wrap: "truncate-end",
|
|
29993
|
+
children: "Only you see this · p plan · h hours · d off · v how it went · [ ] weeks"
|
|
29994
|
+
})
|
|
29995
|
+
})
|
|
29996
|
+
]
|
|
29997
|
+
});
|
|
29998
|
+
}
|
|
29999
|
+
|
|
29734
30000
|
//#endregion
|
|
29735
30001
|
//#region src/tui/app.tsx
|
|
29736
30002
|
/**
|
|
@@ -29784,10 +30050,14 @@ const NAV = [
|
|
|
29784
30050
|
{
|
|
29785
30051
|
screen: "crew",
|
|
29786
30052
|
label: "Crew"
|
|
30053
|
+
},
|
|
30054
|
+
{
|
|
30055
|
+
screen: "week",
|
|
30056
|
+
label: "Your week"
|
|
29787
30057
|
}
|
|
29788
30058
|
];
|
|
29789
|
-
/** The key that opens a sidebar screen: 1 to 9, 0 for the tenth, C for the crew. */
|
|
29790
|
-
const navKey = (i) => i < 10 ? String((i + 1) % 10) : "C";
|
|
30059
|
+
/** The key that opens a sidebar screen: 1 to 9, 0 for the tenth, C for the crew, W for your week. */
|
|
30060
|
+
const navKey = (i) => i < 10 ? String((i + 1) % 10) : i === 10 ? "C" : "W";
|
|
29791
30061
|
/** The sidebar's width: its longest line ("9 Calendar", ": Anything") with room either side. */
|
|
29792
30062
|
const NAV_WIDTH = 15;
|
|
29793
30063
|
/** Every open program, in the review's order: what a meeting started from the palette covers. */
|
|
@@ -30008,6 +30278,7 @@ function App({ ui, startIn }) {
|
|
|
30008
30278
|
if (input === "?") return setOverlay({ kind: "help" });
|
|
30009
30279
|
if (input === "H") return setOverlay({ kind: "harbors" });
|
|
30010
30280
|
if (input === "C") return goScreen("crew");
|
|
30281
|
+
if (input === "W") return goScreen("week");
|
|
30011
30282
|
if (input === "r" && page?.kind !== "bridge") {
|
|
30012
30283
|
sync.retryNow();
|
|
30013
30284
|
return notify("Saving now.");
|
|
@@ -30301,6 +30572,7 @@ function App({ ui, startIn }) {
|
|
|
30301
30572
|
else if (screen === "calendar") screenNode = /* @__PURE__ */ jsx(CalendarView, { ...viewProps });
|
|
30302
30573
|
else if (screen === "brief") screenNode = /* @__PURE__ */ jsx(BriefView, { ...viewProps });
|
|
30303
30574
|
else if (screen === "crew") screenNode = /* @__PURE__ */ jsx(CrewView, { ...viewProps });
|
|
30575
|
+
else if (screen === "week") screenNode = /* @__PURE__ */ jsx(WeekView, { ...viewProps });
|
|
30304
30576
|
else screenNode = /* @__PURE__ */ jsx(HistoryView, { ...viewProps });
|
|
30305
30577
|
return /* @__PURE__ */ jsxs(Box, {
|
|
30306
30578
|
flexDirection: "column",
|
package/lib/format.mjs
CHANGED
|
@@ -756,7 +756,8 @@ export function importText(r) {
|
|
|
756
756
|
if (r.alreadyThere) lines.push(`${n(r.alreadyThere, "task")} already there, left alone.`);
|
|
757
757
|
if (r.leftOut) lines.push(`${n(r.leftOut, "row")} left out (canceled, won't do or duplicate).`);
|
|
758
758
|
if (r.waitsOnLinks) lines.push(`${n(r.waitsOnLinks, "link")} between tasks became "waits on".`);
|
|
759
|
-
for (const p of r.problems ?? [])
|
|
759
|
+
for (const p of r.problems ?? [])
|
|
760
|
+
lines.push(`${p.card ? `Card “${p.card}”` : `Row ${p.row}`}: ${p.message}`);
|
|
760
761
|
return lines.join("\n");
|
|
761
762
|
}
|
|
762
763
|
|