Feature 17: Shopping List and Supply Archive (#140)
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #140.
This commit is contained in:
+89
-17
@@ -7,8 +7,10 @@ import { QUEST_LOG_KEY } from '../discover/api.ts'
|
||||
import { Confirm } from '../settings/LeaveGuard.tsx'
|
||||
import { methodLabels } from '../settings/RegularsSettingsPage.tsx'
|
||||
import { formatClockTime, useHouseholdSettings } from '../settings/api.ts'
|
||||
import { supplyRunScreen, useGenerateSupplyRun, useSupplyRun } from '../shopping/api.ts'
|
||||
import { useBreakpoint, type Breakpoint } from '../shell/useBreakpoint.ts'
|
||||
import { Button, type ButtonVariant } from '../ui/Button.tsx'
|
||||
import buttonStyles from '../ui/Button.module.css'
|
||||
import { EmptyState } from '../ui/EmptyState.tsx'
|
||||
import { ErrorBanner } from '../ui/ErrorBanner.tsx'
|
||||
import { Icon } from '../ui/Icon.tsx'
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
type PlanDay,
|
||||
type PlanDinner,
|
||||
type PlanStatus,
|
||||
type PushedSupplyRun,
|
||||
type Swap,
|
||||
} from './api.ts'
|
||||
import { DragAnnouncer, DROP_DATE, DropBoard, PlanDragProvider, useDropTarget } from './drag.tsx'
|
||||
@@ -79,10 +82,6 @@ const settingsFailures: Record<string, { reason: string; fix: string }> = {
|
||||
},
|
||||
}
|
||||
|
||||
// SHOPPING_HINT is why the shopping list is unavailable.
|
||||
// TODO(Feature 17): open the Supply run, with its count, once it is built.
|
||||
const SHOPPING_HINT = 'The shopping list arrives once the Supply run is built.'
|
||||
|
||||
// syncStatuses is the plan manager's line on whether the plan is on Calendar.
|
||||
const syncStatuses: Record<PlanStatus, string> = {
|
||||
draft: 'Not pushed to Google Calendar yet',
|
||||
@@ -90,6 +89,13 @@ const syncStatuses: Record<PlanStatus, string> = {
|
||||
changed: 'Changes not pushed to Google Calendar',
|
||||
}
|
||||
|
||||
// supplyRunOutcomes says what became of the week's Supply run after a push
|
||||
// that made it or left it as it was.
|
||||
const supplyRunOutcomes: Record<Exclude<PushedSupplyRun['status'], 'failed'>, string> = {
|
||||
generated: "Made this week's Supply run from the plan.",
|
||||
archived: "This week's Supply run is in the Supply archive, so it was left as it was.",
|
||||
}
|
||||
|
||||
// duration is minutes as the plan manager's cards say it: "45 min", "6 hr",
|
||||
// "1 hr 30 min", with no-break spaces so a narrow card wraps the line
|
||||
// between the method and the duration, never inside the duration.
|
||||
@@ -195,6 +201,22 @@ function DayColumn({ day, timezone, swapping, onSwap }: { day: PlanDay; timezone
|
||||
)
|
||||
}
|
||||
|
||||
// ShoppingList opens the week's Supply run: an outline action in the plan
|
||||
// manager's header, a grape link under the Draft board on mobile. Once the
|
||||
// week has a Supply run it says how many items are still to buy, "Shopping
|
||||
// list · 28"; until then, while that is read, or when reading it failed, it
|
||||
// says no count, and the Supply run screen offers to make the list or shows
|
||||
// the failure.
|
||||
function ShoppingList({ weekStart, neededCount, layout }: { weekStart: string; neededCount: number | undefined; layout: Breakpoint }) {
|
||||
const className = layout === 'mobile' ? styles.shoppingLink : [buttonStyles.button, buttonStyles.outline, styles.action].join(' ')
|
||||
return (
|
||||
<Link to={supplyRunScreen(weekStart)} className={className}>
|
||||
<Icon icon={ShoppingCart} size="inline" />
|
||||
{neededCount !== undefined ? `Shopping list · ${neededCount}` : 'Shopping list'}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// SkeletonTiles stands in for the seven days while the plan loads or the AI
|
||||
// drafts it, as two columns of tiles on mobile and seven columns wider.
|
||||
function SkeletonTiles({ label, layout, children }: { label: string; layout: Breakpoint; children?: string }) {
|
||||
@@ -217,13 +239,16 @@ function SkeletonTiles({ label, layout, children }: { label: string; layout: Bre
|
||||
// the plan is read again. Rebuilding a plan pushed to Calendar asks first,
|
||||
// and sends confirm. A failed draft is shown with Retry; no plan is made up.
|
||||
//
|
||||
// From tablet up it is the plan manager (mocks 2c, 2d): Rebuild week, Push to
|
||||
// Calendar and the unavailable Shopping list are actions in the header,
|
||||
// over the plan's sync status and the 7-column grid.
|
||||
// From tablet up it is the plan manager (mocks 2c, 2d): Shopping list,
|
||||
// Rebuild week and Push to Calendar are actions in the header, over the
|
||||
// plan's sync status and the 7-column grid. On mobile the Shopping list
|
||||
// link sits under the push button.
|
||||
//
|
||||
// Push to Calendar, on either layout, sends the plan shown and shows it as
|
||||
// synced once the server answers; a failure says why, linking to the Google
|
||||
// Calendar settings when only they can fix it.
|
||||
// Calendar settings when only they can fix it. What became of the week's
|
||||
// Supply run is said too: made, left as it was in the Supply archive, or
|
||||
// not made, with the way to make it.
|
||||
function WeekBoard({
|
||||
weekStart,
|
||||
timezone,
|
||||
@@ -243,6 +268,9 @@ function WeekBoard({
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const swap = useSwapDay(weekStart)
|
||||
const push = usePushToCalendar(weekStart)
|
||||
const run = useSupplyRun(weekStart)
|
||||
// Makes the week's Supply run again after a push could not make it.
|
||||
const generate = useGenerateSupplyRun(weekStart)
|
||||
const pushing = push.pending
|
||||
// The date of the day being swapped, while the swap sheet is open.
|
||||
const [swappingDate, setSwappingDate] = useState<string | null>(null)
|
||||
@@ -312,6 +340,7 @@ function WeekBoard({
|
||||
if (!canPush) return
|
||||
swap.dismiss()
|
||||
push.reset()
|
||||
generate.reset()
|
||||
push.mutate()
|
||||
}
|
||||
|
||||
@@ -323,6 +352,7 @@ function WeekBoard({
|
||||
setJobId(null)
|
||||
swap.dismiss()
|
||||
push.reset()
|
||||
generate.reset()
|
||||
mutate(
|
||||
{ weekStart, confirm },
|
||||
{
|
||||
@@ -341,8 +371,33 @@ function WeekBoard({
|
||||
else start(false)
|
||||
}
|
||||
|
||||
const retryPush = cached ? () => void plan.refetch() : pushToCalendar
|
||||
// makeShoppingList makes the week's Supply run again after a push could
|
||||
// not make it. Once made, here or on the Supply run screen, the push is
|
||||
// settled (useGenerateSupplyRun). A failure is the mutation's state, which
|
||||
// generate.error reports.
|
||||
function makeShoppingList() {
|
||||
generate.mutate()
|
||||
}
|
||||
// A push's Supply run made again on the Plan stands for the push's, which
|
||||
// is settled.
|
||||
const supplyRun: PushedSupplyRun | null = generate.isSuccess ? { status: 'generated', error: null } : push.supplyRun
|
||||
const failure = push.error !== null ? (
|
||||
<PushFailure error={push.error} cached={cached} onRetry={cached ? () => void plan.refetch() : pushToCalendar} />
|
||||
<PushFailure error={push.error} cached={cached} onRetry={retryPush} />
|
||||
) : supplyRun?.status === 'failed' ? (
|
||||
supplyRun.error.code === 'plan_changed' ? (
|
||||
<ErrorBanner onRetry={retryPush}>
|
||||
Pushed to Calendar, but the plan changed before its Supply run was made, so no shopping list was made from it; push
|
||||
it again.
|
||||
</ErrorBanner>
|
||||
) : (
|
||||
<ErrorBanner onRetry={generate.isPending ? undefined : makeShoppingList} retryLabel="Make the shopping list">
|
||||
Pushed to Calendar, but this week's Supply run couldn't be made.
|
||||
{generate.isPending
|
||||
? ' Making the shopping list…'
|
||||
: generate.isError && ` Couldn't make the shopping list. ${generate.error.message}`}
|
||||
</ErrorBanner>
|
||||
)
|
||||
) : swap.failed !== null ? (
|
||||
<ErrorBanner onRetry={() => swapIn(swap.failed!.swap)}>
|
||||
Couldn't swap {weekdayName(swap.failed.swap.date)}'s dinner. {swap.failed.error.message}
|
||||
@@ -360,11 +415,27 @@ function WeekBoard({
|
||||
) : (
|
||||
<ErrorBanner onRetry={job.poll}>Couldn't check on the draft. {job.error.message}</ErrorBanner>
|
||||
)
|
||||
const pushed = supplyRun !== null && supplyRun.status !== 'failed' && (
|
||||
<p role="status" className={styles.pushed}>
|
||||
{supplyRunOutcomes[supplyRun.status]}
|
||||
</p>
|
||||
)
|
||||
// A failed read of the week's Supply run is shown, with Retry, after the
|
||||
// Draft board's Shopping list link or under the plan manager's board; the
|
||||
// Shopping list action still opens the Supply run. The count is that of a
|
||||
// successful read only: a failed refetch keeps the earlier Supply run in
|
||||
// run.data, and its count is then no longer current.
|
||||
const neededCount = run.isSuccess ? run.data?.needed_count : undefined
|
||||
const shoppingList = <ShoppingList weekStart={weekStart} neededCount={neededCount} layout={layout} />
|
||||
const supplyRunFailure = run.isError && (
|
||||
<ErrorBanner onRetry={() => void run.refetch()}>Couldn't load this week's shopping list. {run.error.message}</ErrorBanner>
|
||||
)
|
||||
|
||||
// swapIn sends the swap chosen in the sheet and closes it.
|
||||
function swapIn(chosen: Swap) {
|
||||
setSwappingDate(null)
|
||||
push.reset()
|
||||
generate.reset()
|
||||
swap.mutate(chosen)
|
||||
}
|
||||
// dropIn swaps in a dinner dragged onto a day. A drag is not picked up,
|
||||
@@ -462,6 +533,8 @@ function WeekBoard({
|
||||
{pushing ? 'Pushing to Calendar…' : 'Looks good — push to Calendar'}
|
||||
</Button>
|
||||
<SyncStatus plan={plan.data} className={styles.mobileStatus} />
|
||||
{shoppingList}
|
||||
{supplyRunFailure}
|
||||
{sheet}
|
||||
</>
|
||||
)
|
||||
@@ -493,10 +566,7 @@ function WeekBoard({
|
||||
<div className={styles.actionBar}>
|
||||
{plan.data ? <SyncStatus plan={plan.data} /> : <span className={styles.status} />}
|
||||
<div className={styles.actions}>
|
||||
<Unavailable hint={SHOPPING_HINT} variant="outline" below className={styles.action}>
|
||||
<Icon icon={ShoppingCart} size="inline" />
|
||||
Shopping list
|
||||
</Unavailable>
|
||||
{shoppingList}
|
||||
<Button
|
||||
variant="outline"
|
||||
className={styles.action}
|
||||
@@ -537,6 +607,8 @@ function WeekBoard({
|
||||
{header}
|
||||
{body}
|
||||
{failure}
|
||||
{pushed}
|
||||
{layout !== 'mobile' && supplyRunFailure}
|
||||
<Confirm
|
||||
open={confirming}
|
||||
title="Rebuild this week?"
|
||||
@@ -600,10 +672,10 @@ function PushFailure({ error, cached, onRetry }: { error: Error; cached: boolean
|
||||
)
|
||||
}
|
||||
|
||||
// Unavailable is an action with nothing behind it yet (the shopping list):
|
||||
// it stays focusable, so a tap or focus shows the tooltip saying why, above
|
||||
// it or, at the top of the page, below.
|
||||
function Unavailable({
|
||||
// Unavailable is an action with nothing behind it yet (Send to Google
|
||||
// Chat): it stays focusable, so a tap or focus shows the tooltip saying
|
||||
// why, above it or, at the top of the page, below.
|
||||
export function Unavailable({
|
||||
hint,
|
||||
variant = 'primary',
|
||||
below = false,
|
||||
|
||||
Reference in New Issue
Block a user