Adds the three proposal fields (proposed_target_date, proposed_by,
proposed_at) to the cards collection with a partial index that backs
the 'pending proposals' query, plus two new ActivityAction values
(date_proposed, date_proposal_resolved) and column_changed for
return-to-scheduled.
New server actions under src/app/actions/:
- client-proposal.ts: client-manager drag creates a proposal (not
a target_date write); 404 (not 403) on cross-tenant failures and
Done-column cards, matching the rest of /api/client/*.
- owner-plan.ts: accept and dismiss clear the three proposal
fields together and emit the audit rows; returnToScheduledAction
moves a Done card back to the first working column.
Server-side Done-column guards added to:
- src/app/actions/reschedule.ts (was UI-only)
- src/app/api/client/cards/[id]/route.ts (was UI-only)
Both pass through the new writeCardWithAudit helper at
src/lib/cards/write-with-audit.ts — single source of truth for
'patch card + emit activity rows', replacing the inline copies in
the PATCH route and rescheduleCardAction.
Adds src/lib/roadmap/slot-date.ts (slot <-> target_date mapping)
consumed by Pixel's puzzle UI; named SlotId API rather than the
plan's numeric indices for self-documenting callers.
CLIENT_EDITABLE_CARD_FIELDS stays at priority + target_date per
the brief; proposals go through the dedicated action so they get
their own audit row type.
86 lines
2.6 KiB
JavaScript
86 lines
2.6 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
/**
|
|
* Proposal fields on `cards`.
|
|
*
|
|
* Plan Puzzle v1: clients drag a card into a month slot and we record their
|
|
* intent on three nullable fields rather than overwriting `target_date`
|
|
* (which the owner controls). The owner sees the chip, accepts (which copies
|
|
* the proposal into `target_date`), or dismisses (which clears it).
|
|
*
|
|
* The three fields are written and cleared together. A row with one set and
|
|
* the others null is logically incoherent; nothing in the application writes
|
|
* one without the others. See src/app/actions/plan/client-proposal.ts and
|
|
* src/app/actions/plan/owner-plan.ts for the only writers.
|
|
*
|
|
* The partial index supports the owner-side "show me cards with pending
|
|
* proposals" query without indexing the millions of empty proposals on the
|
|
* rest of the table.
|
|
*
|
|
* REFERENCE ONLY — see pb_migrations/README.md. scripts/setup-remote.ts is
|
|
* what runs.
|
|
*/
|
|
migrate(
|
|
(app) => {
|
|
const cards = app.findCollectionByNameOrId("cards");
|
|
const users = app.findCollectionByNameOrId("users");
|
|
|
|
cards.fields.add([
|
|
{
|
|
name: "proposed_target_date",
|
|
type: "date",
|
|
required: false,
|
|
presentable: false,
|
|
hidden: false,
|
|
system: false,
|
|
},
|
|
{
|
|
name: "proposed_by",
|
|
type: "relation",
|
|
required: false,
|
|
collectionId: users.id,
|
|
cascadeDelete: false,
|
|
minSelect: 0,
|
|
maxSelect: 1,
|
|
presentable: false,
|
|
hidden: false,
|
|
system: false,
|
|
},
|
|
{
|
|
name: "proposed_at",
|
|
type: "date",
|
|
required: false,
|
|
presentable: false,
|
|
hidden: false,
|
|
system: false,
|
|
},
|
|
]);
|
|
|
|
const existing = new Set((cards.indexes as string[]) ?? []);
|
|
const proposalIndex =
|
|
"CREATE INDEX idx_cards_proposal ON cards (proposed_target_date) WHERE proposed_target_date IS NOT NULL";
|
|
if (!existing.has(proposalIndex)) {
|
|
cards.indexes.push(proposalIndex);
|
|
}
|
|
|
|
return app.save(cards);
|
|
},
|
|
(app) => {
|
|
const cards = app.findCollectionByNameOrId("cards");
|
|
|
|
for (const name of [
|
|
"proposed_target_date",
|
|
"proposed_by",
|
|
"proposed_at",
|
|
]) {
|
|
const field = cards.fields.find((f) => f.name === name);
|
|
if (field) cards.fields.removeById(field.id);
|
|
}
|
|
|
|
const indexes = (cards.indexes as string[]) ?? [];
|
|
const proposalIndex =
|
|
"CREATE INDEX idx_cards_proposal ON cards (proposed_target_date) WHERE proposed_target_date IS NOT NULL";
|
|
cards.indexes = indexes.filter((i) => i !== proposalIndex);
|
|
|
|
return app.save(cards);
|
|
}
|
|
);
|