Public: anyone can read this; the crew can change it.

An open deal nothing has touched in 14 days says so on the clock #313

Parked opened this
Closed without merging

mason/deal-clock:mason/310-an-open-deal-nothing-has-touched-in-14-d → main 4 commits fork

  • Closed without merging. Nothing was applied to main.

Review

Sign in to review this change.
38 files changed +208 −13
.bash_profile added
No content changes.
.bashrc added
No content changes.
.claude/agents added
No content changes.
.claude/commands added
No content changes.
.claude/hooks added
No content changes.
.claude/launch.json added
No content changes.
.claude/loop.md added
No content changes.
.claude/output-styles added
No content changes.
.claude/routines added
No content changes.
.claude/scheduled_tasks.json added
No content changes.
.claude/settings.json added
No content changes.
.claude/settings.local.json added
No content changes.
.claude/skills added
No content changes.
.claude/workflows added
No content changes.
.gitconfig added
No content changes.
.gitmodules added
No content changes.
.idea added
No content changes.
.mcp.json added
No content changes.
.profile added
No content changes.
.ripgreprc added
No content changes.
.vscode added
No content changes.
.zprofile added
No content changes.
.zshrc added
No content changes.
README.md +1 −1
@@ -34,7 +34,7 @@ with a link to the page it names. In the terminal press `G` for the same steps.
3434 adds up your book, like "4 open · $1,693,400 under contract · 2 close in
3535 November · 3 closed this year, $1,375,000 · $45,328 commission pending ·
3636 $37,395 earned this year" (the November count follows the month you open it
37 in). Below the clock, **This week** lists every open date that is overdue or
37 in). A lane that says **Quiet 16 days** beside its address (2210 Juniper Court) is an open deal nothing has touched for two weeks and with no date due in the next three; the deal page says when it was last touched. Below the clock, **This week** lists every open date that is overdue or
3838 due in the next seven days, with who owes it.
3939 3. **Open a deal.** Tap the red inspection marker on 77 Harbor View Dr. A
4040 sheet opens with the date, who owes it (the buyer), what is owed, and the
docs/TECHNICAL.md +1
@@ -283,6 +283,7 @@ decide nothing themselves. `AGENTS.md` says how to add to it.
283283 | `src/money.ts` | the sale price and earnest amount in whole dollars: `amount` validates input, `dollars` formats it one way; called by deals, book, reminders, MCP, routes and the pages |
284284 | `src/profile.ts` | the agent's saved details (one `profile` row, the environment as defaults): `agentOf`, `zoneOf`, `getProfile`, `setProfile`, `useDefaults`, with `permit` on the writes (the `voice`, warm or brief, is one of its fields and the drafting in `src/reminders.ts` reads it and the two voices' words live in `src/wording.ts`; `src/voice.ts` renders the Profile page's sample of each); called by deals, MCP, routes, sample, the terminal and the Profile page |
285285 | `src/progress.ts` | how far along a deal is: a deal and a day in, dates done of total, overdue count and days to closing out; called by routes and the clock, deal and progress pages |
286 | `src/quiet.ts` | which open deals have gone quiet: a deal and a day in, days since the last touch out, or null when closed, recently touched, or a date is due within 3 days; the last touch (`DealView.touched`) is derived in `src/deals.ts` from dates done, waived or moved, messages sent or declined, and the audit trail, so there is no migration; called by `src/pages/quiet.ts` for the clock lane badge and the deal page line |
286287 | `src/settled.ts` | what the agent settled today, counted from the records: a database, zone and day in, sent, declined and held counts out; called by routes and the review page |
287288 | `src/shift.ts` | "Closing moved": a deal and a new closing date in, the open non-custom dates that would shift by the same days out, rolled to business days; confirming goes through `amendDeal` |
288289 | `static/app.css`, `static/live.js`, `static/sheets.js` | the look in the tokens' words, the clock that stays current, and a date's sheet fetched when tapped |
docs/design/areas/clock.md +2
@@ -7,3 +7,5 @@ New classes:
77 - `.progress` — the paragraph that holds both at the top of a deal's page.
88
99 - `.closing-clash` — one amber line (`--color-soon`) under a closing on This week, the print sheet and the deal page: "Also closing at 2:30 pm: 2210 Juniper Court", the address linked to its deal.
10
11 - `.quiet-badge` — the word pill "Quiet 16 days" (amber border and text, `--color-soon`) beside an open deal's address on its lane and above its dates on its page: shown when nothing has touched the deal for 14 days and no date is due within 3. A word, so colour is never the only signal.
src/db.ts +2
@@ -590,6 +590,8 @@ export const migrations: string[] = [
590590 `ALTER TABLE deal ADD COLUMN note TEXT NOT NULL DEFAULT '' CHECK (length(note) <= 500)`,
591591 // How new drafts sound: warm (the original wording) or brief (src/reminders.ts).
592592 `ALTER TABLE profile ADD COLUMN voice TEXT NOT NULL DEFAULT 'warm' CHECK (voice IN ('warm', 'brief'))`,
593 // Reading when a deal was last touched asks the trail by target (src/quiet.ts); without this every read of the clock scans the whole trail.
594 `CREATE INDEX IF NOT EXISTS audit_target ON audit (resource, target, at)`,
593595 ];
594596
595597 export function openDb(dir = dataDir): Database {
src/deals.ts +38 −3
@@ -227,6 +227,8 @@ export interface DealView extends Deal {
227227 readonly conflicts: readonly Conflict[];
228228 /** Other open deals of this agent closing the same day less than two hours from this one (`overlap.ts`). */
229229 readonly closingConflicts: readonly ClosingConflict[];
230 /** The day anything last happened on the deal: opened, a date done, waived or moved, a message sent or declined, or any change the audit trail records (`quiet.ts`). */
231 readonly touched: string;
230232 }
231233
232234 export type Moment = "before" | "day_of" | "late" | "moved" | "status" | "checkin" | "anniversary";
@@ -320,8 +322,22 @@ function withHref(m: SentRow, parties: readonly Party[], zone: string): Sent {
320322 const NO_PAPERS: readonly Paper[] = [];
321323
322324 /** One deal assembled from its rows. */
323 function assemble(deal: Deal, rows: Children, zone: string): DealView {
325 function assemble(deal: Deal, rows: Children, zone: string, audited: string | null): DealView {
324326 const moves = rows.moves;
327 // Stamps are ISO and in UTC, so the latest sorts last: only that one is turned into a day in the agent's zone.
328 let latest = deal.created_at;
329 const later = (at: string | null): void => {
330 if (at !== null && at > latest) latest = at;
331 };
332 later(audited);
333 for (const d of rows.deadlines) {
334 later(d.done_at);
335 later(d.waived_at);
336 }
337 for (const m of moves) later(m.at);
338 for (const m of rows.sent) later(m.sent_at);
339 for (const m of rows.declined) later(m.at);
340 const touched = dayOfStamp(zone, latest);
325341 const papers = rows.papers.length === 0 ? undefined : byDeal(rows.papers, (p) => p.deadline_id);
326342 return {
327343 ...deal,
@@ -346,6 +362,7 @@ function assemble(deal: Deal, rows: Children, zone: string): DealView {
346362 held: rows.held,
347363 conflicts: conflicts(rows.deadlines),
348364 closingConflicts: [],
365 touched,
349366 };
350367 }
351368
@@ -397,7 +414,19 @@ function view(db: Database, deal: Deal): DealView {
397414 )
398415 .all(deal.id),
399416 held: db.query<Held, [number]>("SELECT * FROM reminder_hold WHERE deal_id = ?1 ORDER BY id").all(deal.id),
400 }, zoneOf(db));
417 }, zoneOf(db), lastAudited(db, deal.id));
418 }
419
420 /** The audit rows that are a change made to a deal: allowed, and done. */
421 const AUDITED = "resource = 'deal' AND allowed = 1 AND outcome = 'ok'";
422
423 /** When the audit trail last recorded a change to this deal; null when it recorded none, or cannot be read (a read never fails for the trail). */
424 function lastAudited(db: Database, dealId: number): string | null {
425 try {
426 return db.query<{ at: string | null }, [number]>(`SELECT max(at) AS at FROM audit WHERE ${AUDITED} AND target = ?1`).get(dealId)?.at ?? null;
427 } catch {
428 return null;
429 }
401430 }
402431
403432 /** Rows grouped by the deal they hang off, in the order read. */
@@ -460,6 +489,12 @@ function viewMany(db: Database, where: string, params: string[]): DealView[] {
460489 (r) => r.deal_id,
461490 );
462491 const zone = zoneOf(db);
492 const audited = new Map(
493 db
494 .query<{ target: string; at: string }, string[]>(`SELECT target, max(at) AS at FROM audit WHERE ${AUDITED} AND target IN (SELECT CAST(id AS TEXT) FROM deal${where}) GROUP BY target`)
495 .all(...params)
496 .map((r) => [Number(r.target), r.at] as const),
497 );
463498 return deals.map((deal) =>
464499 assemble(deal, {
465500 deadlines: deadlines.get(deal.id) ?? [],
@@ -469,7 +504,7 @@ function viewMany(db: Database, where: string, params: string[]): DealView[] {
469504 sent: sent.get(deal.id) ?? [],
470505 declined: declined.get(deal.id) ?? [],
471506 held: held.get(deal.id) ?? [],
472 }, zone),
507 }, zone, audited.get(deal.id) ?? null),
473508 );
474509 }
475510
src/migrations.pin.json +3 −1
@@ -62,5 +62,7 @@
6262 "8d0b71567ca7fc8e0927bfb1465c84a30b6702a8460145f7640e612b829530f5",
6363 "c82fe9b3aea33d99a2c65ffb4179901d5aecde6b2ef759531b94ff12035a719a",
6464 "d7c7a79baf6dcd397af5c223839a821db0f0b744f6f02358ea4ba38532ae148b",
65 "9d29fe2875d6dff02bf7b79a6afbea525f86bca59756a28378e979916a8cfc27"
65 "9d29fe2875d6dff02bf7b79a6afbea525f86bca59756a28378e979916a8cfc27",
66 "6c8473e560e240dcc74e0e01f198acd6fc1f33a1fc19e80c17fbfae097ee1caf",
67 "73ce6502f90acf67bfdb43e658f1ea0fdd15d29ca378c6d470743c571e83bdbc"
6668 ]
src/migrations.test.ts +1 −1
@@ -13,7 +13,7 @@ test("every migration is pinned by position; a pull may only append", () => {
1313 });
1414
1515 test("a database made before the Profile line gets the column and reads the agent", () => {
16 const before = migrations.filter((m) => !/profile ADD COLUMN line|commission_rate|profile ADD COLUMN zone|ADD COLUMN cash|'anniversary'|deadline ADD COLUMN (time|place)|party ADD COLUMN reach|reminder_hold ADD COLUMN back_on|TABLE IF NOT EXISTS paper|deal ADD COLUMN note|deadline_move ADD COLUMN undo|profile ADD COLUMN voice/.test(m));
16 const before = migrations.filter((m) => !/profile ADD COLUMN line|commission_rate|profile ADD COLUMN zone|ADD COLUMN cash|'anniversary'|deadline ADD COLUMN (time|place)|party ADD COLUMN reach|reminder_hold ADD COLUMN back_on|TABLE IF NOT EXISTS paper|deal ADD COLUMN note|deadline_move ADD COLUMN undo|profile ADD COLUMN voice|INDEX IF NOT EXISTS audit_target/.test(m));
1717 const db = new Database(":memory:");
1818 db.exec("CREATE TABLE schema_version (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))");
1919 before.forEach((sql, i) => {
src/pages/clock.ts +2 −1
@@ -26,6 +26,7 @@ import { bookLine, tally } from "../book.ts";
2626 import { closedInLine, daysToClose, paceLine, paceOf } from "../pace.ts";
2727 import { dollars } from "../money.ts";
2828 import { progressLine } from "./progress.ts";
29 import { quietBadge } from "./quiet.ts";
2930
3031 /** The days the clock shows: a week behind today (further, if something open is older), to a few days past the last closing. */
3132 export function span(deals: readonly DealView[], on: string): { start: string; days: number } {
@@ -80,7 +81,7 @@ function drawLane(deal: DealView, start: string, on: string): string {
8081 })
8182 .join("");
8283 return `<li class="lane">
83 <div class="lane-head"><a href="/deals/${deal.id}">${escape(deal.address)}</a> <small class="muted">${dealSideLabel(deal)} · ${deal.conflicts.length === 0 ? "" : `<b class="u-overdue">${deal.conflicts.length} out of order</b> · `}${progressLine(deal, on)}</small></div>
84 <div class="lane-head"><a href="/deals/${deal.id}">${escape(deal.address)}</a> ${quietBadge(deal, on)}<small class="muted">${dealSideLabel(deal)} · ${deal.conflicts.length === 0 ? "" : `<b class="u-overdue">${deal.conflicts.length} out of order</b> · `}${progressLine(deal, on)}</small></div>
8485 <div class="track" style="height:calc(${height} * 3rem)">${markers}</div>
8586 </li>`;
8687 }
src/pages/deal.ts +2
@@ -24,6 +24,7 @@ import { sideOf } from "../side.ts";
2424 import { dealSideLabel, sideLabel, sideSelect } from "./side.ts";
2525 import { papersLine } from "../papers.ts";
2626 import { progressLine } from "./progress.ts";
27 import { touchedNote } from "./quiet.ts";
2728 import { appointmentForm, appointmentText, type AppointmentProblem } from "./appointment.ts";
2829 import { badge, conflictLines, nonBusinessLine, noteLine, owesChangedLine, rollForm, undoMoveForm, draftItem, dueLine, outboxLine, owedBy, releaseForm, sheet, sheetId } from "./sheet.ts";
2930
@@ -398,6 +399,7 @@ export function dealPage(input: {
398399 </section>
399400 <div id="deal" data-live="deal.changed">
400401 <p class="progress">${progressLine(deal, on)}</p>
402 ${touchedNote(deal, on)}
401403 <section class="card">
402404 <h2>Dates</h2>
403405 ${conflictLines(deal)}
src/pages/quiet.ts added +17
@@ -0,0 +1,17 @@
1 /** The quiet badge on a deal's lane, and the line on its page that says when it was last touched. */
2
3 import { type DealView } from "../deals.ts";
4 import { escape } from "../http.ts";
5 import { quietDays, quietLabel, touchedLine } from "../quiet.ts";
6
7 /** "Quiet 16 days" in a pill when the open deal has gone quiet, then a space; nothing otherwise. */
8 export function quietBadge(deal: DealView, on: string): string {
9 const days = quietDays(deal, on);
10 return days === null ? "" : `<b class="quiet-badge">${escape(quietLabel(days))}</b> `;
11 }
12
13 /** When an open deal was last touched, with the badge if it is quiet; nothing for a closed or fallen-through deal. */
14 export function touchedNote(deal: DealView, on: string): string {
15 if (deal.closed || deal.fell_through) return "";
16 return `<p class="small muted">${quietBadge(deal, on)}${escape(touchedLine(deal.touched, on))}</p>`;
17 }
src/quiet.test.ts added +80
@@ -0,0 +1,80 @@
1 /** An open deal nothing has touched in 14 days says so on the clock; a date due soon, or a closed deal, never does. */
2 import { expect, test } from "bun:test";
3 import { mkdtempSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { addDays, DEFAULT_ZONE, today } from "./dates.ts";
7 import { openDb } from "./db.ts";
8 import { createDeal, getDeal, listDeals, markDone, setDealNote } from "./deals.ts";
9 import { clock } from "./pages/clock.ts";
10 import { dealPage } from "./pages/deal.ts";
11 import { quietDays, QUIET_DAYS } from "./quiet.ts";
12 import type { Who } from "./who.ts";
13
14 const on = today(DEFAULT_ZONE);
15 const alice: Who = { role: "user", id: "u1", name: "alice" };
16 const dl = (due: string, open = true) => ({ done_at: open ? null : "x", waived_at: null, due }) as never;
17 const deal = (touched: string, deadlines: unknown[] = [dl(addDays(on, 20))], over: object = {}) => ({ closed: false, fell_through: false, touched, deadlines, ...over }) as never;
18
19 test("quiet at 14 days and not at 13", () => {
20 expect(QUIET_DAYS).toBe(14);
21 expect(quietDays(deal(addDays(on, -14)), on)).toBe(14);
22 expect(quietDays(deal(addDays(on, -16)), on)).toBe(16);
23 expect(quietDays(deal(addDays(on, -13)), on)).toBeNull();
24 });
25
26 test("a date due inside 3 days exempts the deal; a date 4 days out, or a done one, does not", () => {
27 const old = addDays(on, -30);
28 expect(quietDays(deal(old, [dl(addDays(on, 3))]), on)).toBeNull();
29 expect(quietDays(deal(old, [dl(on)]), on)).toBeNull();
30 expect(quietDays(deal(old, [dl(addDays(on, 4))]), on)).toBe(30);
31 expect(quietDays(deal(old, [dl(addDays(on, 1), false)]), on)).toBe(30);
32 });
33
34 test("a closed or fallen-through deal is never quiet", () => {
35 const old = addDays(on, -30);
36 expect(quietDays(deal(old, [], { closed: true }), on)).toBeNull();
37 expect(quietDays(deal(old, [], { fell_through: true }), on)).toBeNull();
38 });
39
40 test("the last touch is read from the records: a deal opened long ago goes quiet, a note or a done date wakes it", () => {
41 const db = openDb(mkdtempSync(join(tmpdir(), "app-quiet-")));
42 const made = createDeal(db, alice, { openAnyway: true, address: "5 Quiet Way", buyer: "A Buyer", seller: "A Seller", contractDate: addDays(on, -20), closingDate: addDays(on, 60) });
43 if (!made.ok) throw new Error(made.message);
44 const id = made.value.id;
45 const view = () => { const g = getDeal(db, alice, { id }); if (!g.ok) throw new Error(g.message); return g.value; };
46 expect(view().touched).toBe(on);
47 const old = `${addDays(on, -16)}T09:00:00.000Z`;
48 db.query("UPDATE deal SET created_at = ?1 WHERE id = ?2").run(old, id);
49 db.query("DELETE FROM audit").run();
50 db.query("UPDATE deadline SET due = ?1 WHERE deal_id = ?2 AND kind <> 'closing'").run(addDays(on, 30), id);
51 expect(view().touched).toBe(addDays(on, -16));
52 const html = (v = view()) => clock({ who: alice, deals: [v], on });
53 expect(html()).toContain("Quiet 16 days");
54 expect(dealPage({ who: alice, deal: view(), on } as never)).toContain("Last touched 16 days ago");
55 // A private note on the deal is an audited change: the deal is touched today.
56 expect(setDealNote(db, alice, { id, note: "called the lender" }).ok).toBe(true);
57 expect(view().touched).toBe(on);
58 expect(html()).not.toContain("quiet-badge");
59 // A date marked done is a touch too.
60 db.query("DELETE FROM audit").run();
61 expect(view().touched).toBe(addDays(on, -16));
62 const first = view().deadlines.find((d) => d.kind === "earnest")!;
63 expect(markDone(db, alice, { id: first.id, done: true }).ok).toBe(true);
64 db.query("DELETE FROM audit").run();
65 expect(view().touched).toBe(on);
66 });
67
68 test("the sample deals on a fresh boot show one quiet deal on the clock, and none of the closed ones", () => {
69 const db = openDb(mkdtempSync(join(tmpdir(), "app-quiet-sample-")));
70 const l = listDeals(db, alice);
71 if (!l.ok) throw new Error(l.message);
72 const html = clock({ who: alice, deals: l.value, on });
73 expect(html.match(/class="quiet-badge"/g)?.length).toBe(1);
74 expect(html).toMatch(/2210 Juniper Court<\/a> <b class="quiet-badge">Quiet 1[5-7] days<\/b>/);
75 const juniper = l.value.find((d) => d.address === "2210 Juniper Court")!;
76 expect(quietDays(juniper, on)).toBeGreaterThanOrEqual(14);
77 expect(dealPage({ who: alice, deal: juniper, on } as never)).toContain("Last touched 1");
78 const closed = l.value.find((d) => d.closed)!;
79 expect(dealPage({ who: alice, deal: closed, on } as never)).not.toContain("Last touched");
80 });
src/quiet.ts added +35
@@ -0,0 +1,35 @@
1 /**
2 * Which open deals have gone quiet: nothing has touched them for a while and
3 * no date is about to force the matter. Plain data in, plain data out; the
4 * clock and the deal page read it from here.
5 */
6
7 import { daysBetween } from "./dates.ts";
8 import { isOpen } from "./deadlines.ts";
9 import type { DealView } from "./deals.ts";
10
11 /** Whole days without a touch after which an open deal is quiet. */
12 export const QUIET_DAYS = 14;
13 /** A deal with an open date due today or within this many days is never quiet: the date is the nudge. An overdue date is not an exemption: the clock already flags it, and a deal nobody has touched since is the one a client will ask about. */
14 export const SOON_DAYS = 3;
15
16 /** How many days an open deal has been quiet for, or null when it is not quiet (closed, fell through, recently touched, or a date is due soon). */
17 export function quietDays(
18 deal: Pick<DealView, "closed" | "fell_through" | "touched" | "deadlines">,
19 on: string,
20 ): number | null {
21 if (deal.closed || deal.fell_through) return null;
22 const days = daysBetween(deal.touched, on);
23 if (days < QUIET_DAYS) return null;
24 if (deal.deadlines.some((d) => isOpen(d) && daysBetween(on, d.due) >= 0 && daysBetween(on, d.due) <= SOON_DAYS)) return null;
25 return days;
26 }
27
28 /** "Quiet 16 days": a word, so colour is never the only signal. */
29 export const quietLabel = (days: number): string => `Quiet ${days} days`;
30
31 /** "Last touched 16 days ago", "today" or "yesterday", for the deal page. */
32 export function touchedLine(touched: string, on: string): string {
33 const days = daysBetween(touched, on);
34 return days <= 0 ? "Last touched today" : days === 1 ? "Last touched yesterday" : `Last touched ${days} days ago`;
35 }
src/sample.ts +11 −5
@@ -37,6 +37,8 @@ interface Sample {
3737 /** Closed a year ago to the day, so its home anniversary note waits today; the buyer's email. */
3838 readonly anniversary?: string;
3939 readonly lengths?: Partial<typeof BRIEF_LENGTHS>;
40 /** Days since anything touched the deal: it was opened then and its waived dates stamped then, so it wears the quiet badge on every boot. */
41 readonly quiet?: number;
4042 /** When and where it closes, for the sample that closes this week. */
4143 readonly closingAt?: readonly [string, string];
4244 /** Role, name, email or phone, and a phone besides when the first is an email. */
@@ -138,12 +140,14 @@ const SAMPLES: readonly Sample[] = [
138140 address: "2210 Juniper Court",
139141 buyer: "Dev & Anika Rao",
140142 seller: "Carla Mendes",
141 contract: -3,
142 closing: 40,
143 contract: -17,
144 closing: 60,
143145 price: 389_900,
144146 rate: 250,
145147 earnest: 8_000,
146 lengths: { earnest: 5 },
148 // Nothing is due for a week and nobody has touched it for sixteen days: the quiet deal.
149 quiet: 16,
150 lengths: { earnest: 29, inspection: 33, title: 37, appraisal: 41, financing: 47 },
147151 parties: [
148152 ["lender", "Owen Brandt, First Keel Bank", "[email protected]"],
149153 ["other_agent", "Jordan Ellis, Crestline Homes", "(555) 014-2290"],
@@ -306,7 +310,7 @@ export interface SampleState {
306310 export function isUntouched(state: SampleState, anchor: string): boolean {
307311 const s = SAMPLES.find((x) => x.address === state.sample);
308312 if (s === undefined) return false;
309 const marker = `${addDays(anchor, -1)}${DONE_TIME}`;
313 const marker = `${addDays(anchor, -(s.quiet ?? 1))}${DONE_TIME}`;
310314 const stamped = (d: string | null, i: number) => d === null || (d.endsWith(DONE_TIME) && d.slice(0, 10) < state.dues[i]!) || d === marker;
311315 return (
312316 state.parties === s.parties.length &&
@@ -392,6 +396,7 @@ export function refresh(db: Database, on = today(zoneOf(db))): void {
392396 for (const m of shifted.moves) db.query("UPDATE deadline_move SET from_due = ?1, to_due = ?2 WHERE id = ?3").run(rolled(m.fromDue), rolled(m.toDue), m.id);
393397 // The sample's sent reminder follows its date, so it stays "yesterday's".
394398 const days = `+${daysBetween(anchor, on)} days`;
399 db.query("UPDATE deal SET created_at = strftime('%Y-%m-%dT%H:%M:%fZ', created_at, ?1) WHERE id = ?2").run(days, deal.id);
395400 db.query("UPDATE deadline_move SET at = strftime('%Y-%m-%dT%H:%M:%fZ', at, ?1) WHERE deadline_id IN (SELECT id FROM deadline WHERE deal_id = ?2)").run(days, deal.id);
396401 db.query("UPDATE reminder SET due = date(due, ?1), sent_at = strftime('%Y-%m-%dT%H:%M:%fZ', sent_at, ?1) WHERE deal_id = ?2 AND sample = 1").run(days, deal.id);
397402 db.query("UPDATE reminder SET due = (SELECT due FROM deadline WHERE id = reminder.deadline_id) WHERE deal_id = ?1 AND sample = 1 AND moment <> 'checkin'").run(deal.id);
@@ -425,6 +430,7 @@ function writeSamples(db: Database, on: string): void {
425430 commissionRate: s.rate,
426431 side: s.side ?? "buyer",
427432 });
433 if (s.quiet !== undefined) db.query("UPDATE deal SET created_at = ?1 WHERE id = ?2").run(`${addDays(on, -s.quiet)}T09:00:00.000Z`, deal.id);
428434 if (s.closingAt !== undefined)
429435 db.query("UPDATE deal SET closing_time = ?1, closing_place = ?2 WHERE id = ?3").run(s.closingAt[0], s.closingAt[1], deal.id);
430436 for (const kind of APPOINTMENT_KINDS) {
@@ -453,7 +459,7 @@ function writeSamples(db: Database, on: string): void {
453459 else db.query("UPDATE deadline SET done_at = min(date(due, '-1 day'), ?1) || ?2 WHERE deal_id = ?3 AND kind = ?4").run(finished, DONE_TIME, deal.id, kind);
454460 }
455461 for (const kind of s.waived ?? [])
456 db.query("UPDATE deadline SET waived_at = ?1, waived_by = ?2 WHERE deal_id = ?3 AND kind = ?4").run(`${addDays(on, -1)}${DONE_TIME}`, SAMPLE.name, deal.id, kind);
462 db.query("UPDATE deadline SET waived_at = ?1, waived_by = ?2 WHERE deal_id = ?3 AND kind = ?4").run(`${addDays(on, -(s.quiet ?? 1))}${DONE_TIME}`, SAMPLE.name, deal.id, kind);
457463 for (const [kind, note] of s.notes ?? [])
458464 db.query("UPDATE deadline SET note = ?1 WHERE deal_id = ?2 AND kind = ?3").run(note, deal.id, kind);
459465 for (const [kind, name, isIn] of s.papers ?? []) {
src/tour.ts +1 −1
@@ -28,7 +28,7 @@ export const STEPS: readonly Step[] = [
2828 },
2929 {
3030 title: "Read the clock",
31 text: "One lane per open deal. Time runs left to right and today is the dark line down the middle. Every date in a contract is a marker: red is overdue (the inspection on 77 Harbor View Dr), amber is due within three days, teal is coming up, grey is done (the earlier dates on 1418 Alder Lane, which closes in a few days). Under the heading, one line adds up your book, like \"4 open · $1,693,400 under contract · 2 close in November · 3 closed this year, $1,375,000 · $45,328 commission pending · $37,395 earned this year\" (the November count follows the month you open it in). Below the clock, **This week** lists every open date that is overdue or due in the next seven days, with who owes it.",
31 text: "One lane per open deal. Time runs left to right and today is the dark line down the middle. Every date in a contract is a marker: red is overdue (the inspection on 77 Harbor View Dr), amber is due within three days, teal is coming up, grey is done (the earlier dates on 1418 Alder Lane, which closes in a few days). Under the heading, one line adds up your book, like \"4 open · $1,693,400 under contract · 2 close in November · 3 closed this year, $1,375,000 · $45,328 commission pending · $37,395 earned this year\" (the November count follows the month you open it in). A lane that says **Quiet 16 days** beside its address (2210 Juniper Court) is an open deal nothing has touched for two weeks and with no date due in the next three; the deal page says when it was last touched. Below the clock, **This week** lists every open date that is overdue or due in the next seven days, with who owes it.",
3232 go: { label: "the clock", page: "/" },
3333 },
3434 {
static/app.css +12
@@ -1043,6 +1043,18 @@ a.marker {
10431043 max-width: 24rem;
10441044 }
10451045
1046 /* An open deal nothing has touched in two weeks: a word in a pill, never colour alone. */
1047 .quiet-badge {
1048 display: inline-block;
1049 padding: 0 var(--space-2);
1050 border: 1px solid var(--color-soon);
1051 border-radius: var(--radius-pill);
1052 color: var(--color-soon);
1053 font-size: var(--text-sm);
1054 font-weight: 600;
1055 white-space: nowrap;
1056 }
1057
10461058 /* The header's Review link: the count has a slot of its own, so it appearing moves nothing. */
10471059 .review-count {
10481060 display: inline-block;

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs