Build the Supply archive screen at /plan/shopping/archive (Feature 17) #139
@@ -35,10 +35,18 @@ const WEEK_40 = post('2026-09-28', 'Week 40 · Sep 28 – Oct 4', { title: 'Pizz
|
|||||||
const WEEK_39 = post('2026-09-21', 'Week 39 · Sep 21 – 27', { title: 'Chili season opens', dinnerCount: 1, itemCount: 1, onHandCount: 0, dinners: ['Chili'] })
|
const WEEK_39 = post('2026-09-21', 'Week 39 · Sep 21 – 27', { title: 'Chili season opens', dinnerCount: 1, itemCount: 1, onHandCount: 0, dinners: ['Chili'] })
|
||||||
const WEEK_1 = post('2025-12-29', 'Week 1 · Dec 29 – Jan 4')
|
const WEEK_1 = post('2025-12-29', 'Week 1 · Dec 29 – Jan 4')
|
||||||
|
|
||||||
// The archive as the server groups it: by month and year a week belongs to
|
// The archive as the server groups it: by week each period is one ISO week
|
||||||
// the period of its Monday, so week 40 is September's and week 1 is 2025's.
|
// headed by its label; by month and year a week belongs to the period of its
|
||||||
|
// Monday, so week 40 is September's and week 1 is 2025's.
|
||||||
const BY = {
|
const BY = {
|
||||||
week: [WEEK_41, WEEK_40, WEEK_39, WEEK_1].map((p) => ({ key: p.weekStart, heading: p.label, posts: [p] })),
|
week: (
|
||||||
|
[
|
||||||
|
['2026-W41', WEEK_41],
|
||||||
|
['2026-W40', WEEK_40],
|
||||||
|
['2026-W39', WEEK_39],
|
||||||
|
['2026-W01', WEEK_1],
|
||||||
|
] as const
|
||||||
|
).map(([key, p]) => ({ key, heading: p.label, posts: [p] })),
|
||||||
month: [
|
month: [
|
||||||
{ key: '2026-10', heading: 'October 2026', posts: [WEEK_41] },
|
{ key: '2026-10', heading: 'October 2026', posts: [WEEK_41] },
|
||||||
{ key: '2026-09', heading: 'September 2026', posts: [WEEK_40, WEEK_39] },
|
{ key: '2026-09', heading: 'September 2026', posts: [WEEK_40, WEEK_39] },
|
||||||
@@ -111,7 +119,7 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('SupplyArchivePage', () => {
|
describe('SupplyArchivePage', () => {
|
||||||
it('renders mock 1m by week: one post per week with its kicker, title, counts and dinner chips, under no heading', async () => {
|
it('renders mock 1m by week: one post per week with its kicker, title, counts and dinner chips', async () => {
|
||||||
stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week') })
|
stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week') })
|
||||||
await renderPage()
|
await renderPage()
|
||||||
|
|
||||||
@@ -134,13 +142,28 @@ describe('SupplyArchivePage', () => {
|
|||||||
'/plan/shopping/archive?week=2026-09-21',
|
'/plan/shopping/archive?week=2026-09-21',
|
||||||
'/plan/shopping/archive?week=2025-12-29',
|
'/plan/shopping/archive?week=2025-12-29',
|
||||||
])
|
])
|
||||||
expect(screen.queryAllByRole('heading', { level: 2 })).toEqual([])
|
})
|
||||||
|
|
||||||
|
it('renders by week the periods and headings the server sends, as sent', async () => {
|
||||||
|
stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week') })
|
||||||
|
await renderPage()
|
||||||
|
await screen.findByText('Chili season opens')
|
||||||
|
|
||||||
|
expect(screen.getAllByRole('heading', { level: 2 }).map((h) => h.textContent)).toEqual(BY.week.map((period) => period.heading))
|
||||||
|
expect(screen.getByRole('heading', { level: 2, name: WEEK_40.label })).toHaveClass(styles.heading!)
|
||||||
|
for (const period of BY.week) {
|
||||||
|
const section = screen.getByRole('region', { name: period.heading })
|
||||||
|
expect(within(section).getAllByRole('link').map((link) => link.getAttribute('href'))).toEqual(
|
||||||
|
period.posts.map((p) => `/plan/shopping/archive?week=${p.weekStart}`),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('capitalises the week label kicker in CSS from the label as the API writes it', async () => {
|
it('capitalises the week label kicker in CSS from the label as the API writes it', async () => {
|
||||||
stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week') })
|
stubServer({ [`GET ${ARCHIVE('week')}`]: archive('week') })
|
||||||
await renderPage()
|
await renderPage()
|
||||||
expect(await screen.findByText('Week 39 · Sep 21 – 27')).toHaveClass(styles.kicker!)
|
await screen.findByText('Chili season opens')
|
||||||
|
expect(within(postCard('Chili season opens')).getByText('Week 39 · Sep 21 – 27')).toHaveClass(styles.kicker!)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -299,6 +322,26 @@ const FROZEN = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The current week's Supply run, week 41, still current: the API's archive
|
||||||
|
// includes it, highlighted.
|
||||||
|
const CURRENT = {
|
||||||
|
...FROZEN,
|
||||||
|
weekStart: '2026-10-05',
|
||||||
|
label: 'Week 41 · Oct 5 – 11',
|
||||||
|
title: 'Tacos, tikka & a salmon finale',
|
||||||
|
status: 'current',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
dinner: { id: 'd2', name: 'Beef tacos' },
|
||||||
|
date: '2026-10-05',
|
||||||
|
items: [
|
||||||
|
{ id: 'i3', name: 'Corn tortillas', qty: '12', needed: true },
|
||||||
|
{ id: 'i4', name: 'Limes', qty: '3', needed: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
describe('SupplyArchivePage frozen post', () => {
|
describe('SupplyArchivePage frozen post', () => {
|
||||||
it('opens a tapped post’s frozen list read-only, tagged Archived, sending no PATCH', async () => {
|
it('opens a tapped post’s frozen list read-only, tagged Archived, sending no PATCH', async () => {
|
||||||
const fetchMock = stubServer({
|
const fetchMock = stubServer({
|
||||||
@@ -322,6 +365,45 @@ describe('SupplyArchivePage frozen post', () => {
|
|||||||
expect(await screen.findByRole('heading', { level: 1, name: 'Supply archive' })).toBeInTheDocument()
|
expect(await screen.findByRole('heading', { level: 1, name: 'Supply archive' })).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens the current week’s post read-only too, tagged Archived, with no checkbox, nothing to send and no PATCH', async () => {
|
||||||
|
const fetchMock = stubServer({
|
||||||
|
[`GET ${ARCHIVE('week')}`]: archive('week'),
|
||||||
|
'GET /api/v1/shopping/2026-10-05': () => jsonResponse(200, CURRENT),
|
||||||
|
})
|
||||||
|
await renderPage()
|
||||||
|
|
||||||
|
fireEvent.click(await screen.findByText('Tacos, tikka & a salmon finale'))
|
||||||
|
|
||||||
|
expect(await screen.findByText('Archived')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText("This week's list as it stands, read-only in the Supply archive.")).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Corn tortillas')).toBeInTheDocument()
|
||||||
|
expect(screen.queryAllByRole('checkbox')).toEqual([])
|
||||||
|
fireEvent.click(screen.getByText('Corn tortillas'))
|
||||||
|
fireEvent.click(screen.getByText('Limes'))
|
||||||
|
expect(fetchMock.mock.calls.filter(([, init]) => init?.method !== undefined && init.method !== 'GET')).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('on desktop opens the current week’s post read-only, and with no post open shows this week’s Supply run to change', async () => {
|
||||||
|
mockViewport(1440)
|
||||||
|
const fetchMock = stubServer({
|
||||||
|
[`GET ${ARCHIVE('week')}`]: archive('week'),
|
||||||
|
'GET /api/v1/shopping/2026-10-05': () => jsonResponse(200, CURRENT),
|
||||||
|
})
|
||||||
|
const view = await renderPage('/plan/shopping/archive?week=2026-10-05')
|
||||||
|
|
||||||
|
expect(await screen.findByText('Archived')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Corn tortillas')).toBeInTheDocument()
|
||||||
|
expect(screen.queryAllByRole('checkbox')).toEqual([])
|
||||||
|
expect(screen.queryByText('Send to Google Chat')).not.toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByText('Corn tortillas'))
|
||||||
|
expect(fetchMock.mock.calls.filter(([, init]) => init?.method !== undefined && init.method !== 'GET')).toEqual([])
|
||||||
|
view.unmount()
|
||||||
|
|
||||||
|
await renderPage()
|
||||||
|
expect(await screen.findByRole('checkbox', { name: 'Corn tortillas' })).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Archived')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('on desktop (mock 2f) shows the archive as a panel beside the open post’s list', async () => {
|
it('on desktop (mock 2f) shows the archive as a panel beside the open post’s list', async () => {
|
||||||
mockViewport(1440)
|
mockViewport(1440)
|
||||||
stubServer({
|
stubServer({
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function plural(n: number, one: string, many: string): string {
|
|||||||
// PostCard is one week's Supply run (mock 1m): the week label as its kicker,
|
// PostCard is one week's Supply run (mock 1m): the week label as its kicker,
|
||||||
// the AI's title — the week label while it has none — the counts and its
|
// the AI's title — the week label while it has none — the counts and its
|
||||||
// first dinners as chips. The current week's post is ringed in grape. It
|
// first dinners as chips. The current week's post is ringed in grape. It
|
||||||
// opens the week's list, read-only once frozen into the Supply archive.
|
// opens the week's list, read-only.
|
||||||
function PostCard({ post }: { post: ArchivePost }) {
|
function PostCard({ post }: { post: ArchivePost }) {
|
||||||
const rest = post.dinners.length - CHIPS
|
const rest = post.dinners.length - CHIPS
|
||||||
return (
|
return (
|
||||||
@@ -94,8 +94,9 @@ function shownGroups(groups: ArchiveGroup[], shown: number): ArchiveGroup[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ArchivePosts is the Supply archive as the server groups it by by: one post
|
// ArchivePosts is the Supply archive as the server groups it by by: one post
|
||||||
// per week, under Bagel headings of their month or year. It shows PAGE posts
|
// per week, under the Bagel heading of each period the server sends — by
|
||||||
// and "Load older" shows PAGE more.
|
// week, the week's label. It shows PAGE posts and "Load older" shows PAGE
|
||||||
|
// more.
|
||||||
function ArchivePosts({ by }: { by: ArchivePeriod }) {
|
function ArchivePosts({ by }: { by: ArchivePeriod }) {
|
||||||
const archive = useSupplyArchive(by)
|
const archive = useSupplyArchive(by)
|
||||||
const [shown, setShown] = useState(PAGE)
|
const [shown, setShown] = useState(PAGE)
|
||||||
@@ -119,16 +120,12 @@ function ArchivePosts({ by }: { by: ArchivePeriod }) {
|
|||||||
const groups = shownGroups(archive.data, shown)
|
const groups = shownGroups(archive.data, shown)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{by === 'week' ? (
|
{groups.map((group) => (
|
||||||
<Posts posts={groups.flatMap((group) => group.posts)} />
|
<section key={group.key} aria-label={group.heading} className={styles.period}>
|
||||||
) : (
|
<h2 className={styles.heading}>{group.heading}</h2>
|
||||||
groups.map((group) => (
|
<Posts posts={group.posts} />
|
||||||
<section key={group.key} aria-label={group.heading} className={styles.period}>
|
</section>
|
||||||
<h2 className={styles.heading}>{group.heading}</h2>
|
))}
|
||||||
<Posts posts={group.posts} />
|
|
||||||
</section>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{shown < total && (
|
{shown < total && (
|
||||||
<Button variant="outline" className={styles.older} onClick={() => setShown(shown + PAGE)}>
|
<Button variant="outline" className={styles.older} onClick={() => setShown(shown + PAGE)}>
|
||||||
Load older
|
Load older
|
||||||
@@ -161,10 +158,12 @@ function SupplyArchive({ panel }: { panel: boolean }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SupplyArchivePage is /plan/shopping/archive. A post opens it on its week
|
// SupplyArchivePage is /plan/shopping/archive. A post opens it on its week
|
||||||
// (?week=), whose list the Supply run screen shows — read-only once frozen
|
// (?week=), whose list the Supply run screen shows read-only and tagged
|
||||||
// into the Supply archive. On desktop (mock 2f) the archive is a panel at the
|
// Archived, whatever its status: even the current week's post is only
|
||||||
// right of that list, this week's when no post is open; on mobile and tablet
|
// viewed here, never changed. On desktop (mock 2f) the archive is a panel at
|
||||||
// it is the archive or, with a post open, the post's list.
|
// the right of that list, this week's editable Supply run when no post is
|
||||||
|
// open; on mobile and tablet it is the archive or, with a post open, the
|
||||||
|
// post's list.
|
||||||
export function SupplyArchivePage() {
|
export function SupplyArchivePage() {
|
||||||
const layout = useBreakpoint()
|
const layout = useBreakpoint()
|
||||||
const [params] = useSearchParams()
|
const [params] = useSearchParams()
|
||||||
@@ -172,7 +171,7 @@ export function SupplyArchivePage() {
|
|||||||
return (
|
return (
|
||||||
<div className={styles.split}>
|
<div className={styles.split}>
|
||||||
<div className={styles.list}>
|
<div className={styles.list}>
|
||||||
<SupplyRunPage />
|
<SupplyRunPage readOnly={params.has(WEEK_PARAM)} />
|
||||||
</div>
|
</div>
|
||||||
<aside className={styles.panel} aria-label="Supply archive">
|
<aside className={styles.panel} aria-label="Supply archive">
|
||||||
<SupplyArchive panel />
|
<SupplyArchive panel />
|
||||||
@@ -180,5 +179,5 @@ export function SupplyArchivePage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return params.has(WEEK_PARAM) ? <SupplyRunPage /> : <SupplyArchive panel={false} />
|
return params.has(WEEK_PARAM) ? <SupplyRunPage readOnly /> : <SupplyArchive panel={false} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export function sentAt(at: Date, timezone: string): string {
|
|||||||
|
|
||||||
// ItemRow is one ingredient: on a current Supply run a checkbox, checked
|
// ItemRow is one ingredient: on a current Supply run a checkbox, checked
|
||||||
// (grape) while it is to buy; unchecked, the name is struck through and
|
// (grape) while it is to buy; unchecked, the name is struck through and
|
||||||
// "have it" stands in for the quantity. An archived Supply run shows the
|
// "have it" stands in for the quantity. A read-only Supply run shows the
|
||||||
// same marks, read-only. An item toggled while the server cannot be reached
|
// same marks. An item toggled while the server cannot be reached
|
||||||
// says it will sync.
|
// says it will sync.
|
||||||
function ItemRow({ item, toggles }: { item: SupplyRunItem; toggles: Toggles | null }) {
|
function ItemRow({ item, toggles }: { item: SupplyRunItem; toggles: Toggles | null }) {
|
||||||
const name = (
|
const name = (
|
||||||
@@ -121,12 +121,13 @@ function ArchiveLink({ layout }: { layout: Breakpoint }) {
|
|||||||
// one card per dinner, stacked on mobile and in three masonry columns from
|
// one card per dinner, stacked on mobile and in three masonry columns from
|
||||||
// tablet up, where Archive and Send to Google Chat are actions in the
|
// tablet up, where Archive and Send to Google Chat are actions in the
|
||||||
// header. On a current Supply run each item toggles at once (useToggles); a
|
// header. On a current Supply run each item toggles at once (useToggles); a
|
||||||
// toggle refused puts the item back and says why. An archived one is
|
// toggle refused puts the item back and says why. An archived one, or any
|
||||||
// read-only, and a toggle it refused is not offered again.
|
// opened from the Supply archive (readOnly), is read-only and tagged
|
||||||
function SupplyRunList({ weekStart, timezone, layout }: { weekStart: string; timezone: string; layout: Breakpoint }) {
|
// Archived, and a toggle it refused is not offered again.
|
||||||
|
function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
||||||
const toggles = useToggles(weekStart)
|
const toggles = useToggles(weekStart)
|
||||||
const run = toggles.shown
|
const run = toggles.shown
|
||||||
const archived = run.status === 'archived'
|
const viewOnly = readOnly || run.status === 'archived'
|
||||||
const wide = layout !== 'mobile'
|
const wide = layout !== 'mobile'
|
||||||
const failed = toggles.failed
|
const failed = toggles.failed
|
||||||
const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id)
|
const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id)
|
||||||
@@ -138,21 +139,23 @@ function SupplyRunList({ weekStart, timezone, layout }: { weekStart: string; tim
|
|||||||
<p className={styles.kicker}>Supply run · {weekLabel(weekStart).dates}</p>
|
<p className={styles.kicker}>Supply run · {weekLabel(weekStart).dates}</p>
|
||||||
<div className={styles.titleRow}>
|
<div className={styles.titleRow}>
|
||||||
<h1 className={styles.title}>Shopping list</h1>
|
<h1 className={styles.title}>Shopping list</h1>
|
||||||
{archived && <Tag tone="grape">Archived</Tag>}
|
{viewOnly && <Tag tone="grape">Archived</Tag>}
|
||||||
<p className={styles.count}>
|
<p className={styles.count}>
|
||||||
{run.needed_count} / {run.total_count}
|
{run.needed_count} / {run.total_count}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.helper}>
|
<p className={styles.helper}>
|
||||||
{archived
|
{run.status === 'archived'
|
||||||
? 'This list is in the Supply archive, as it was when the week ended.'
|
? 'This list is in the Supply archive, as it was when the week ended.'
|
||||||
: "Uncheck anything you already have — it won't be counted."}
|
: viewOnly
|
||||||
|
? "This week's list as it stands, read-only in the Supply archive."
|
||||||
|
: "Uncheck anything you already have — it won't be counted."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{wide && (
|
{wide && (
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<ArchiveLink layout={layout} />
|
<ArchiveLink layout={layout} />
|
||||||
{!archived && (
|
{!viewOnly && (
|
||||||
<Unavailable hint={CHAT_HINT} below className={styles.action}>
|
<Unavailable hint={CHAT_HINT} below className={styles.action}>
|
||||||
<Icon icon={MessageSquare} size="inline" />
|
<Icon icon={MessageSquare} size="inline" />
|
||||||
Send to Google Chat
|
Send to Google Chat
|
||||||
@@ -162,7 +165,7 @@ function SupplyRunList({ weekStart, timezone, layout }: { weekStart: string; tim
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
{failed !== null && (
|
{failed !== null && (
|
||||||
<ErrorBanner onRetry={archived ? undefined : () => toggles.toggle(failed.toggle)}>
|
<ErrorBanner onRetry={viewOnly ? undefined : () => toggles.toggle(failed.toggle)}>
|
||||||
Couldn't mark {failedItem ? failedItem.name : 'an item'} {failed.toggle.needed ? 'to buy' : '“have it”'}.{' '}
|
Couldn't mark {failedItem ? failedItem.name : 'an item'} {failed.toggle.needed ? 'to buy' : '“have it”'}.{' '}
|
||||||
{failed.error.message}
|
{failed.error.message}
|
||||||
</ErrorBanner>
|
</ErrorBanner>
|
||||||
@@ -173,7 +176,7 @@ function SupplyRunList({ weekStart, timezone, layout }: { weekStart: string; tim
|
|||||||
) : (
|
) : (
|
||||||
<div className={styles.groups} data-testid="supply-run-groups">
|
<div className={styles.groups} data-testid="supply-run-groups">
|
||||||
{run.groups.map((group) => (
|
{run.groups.map((group) => (
|
||||||
<GroupCard key={`${group.date} ${group.dinner.id}`} group={group} toggles={archived ? null : toggles} />
|
<GroupCard key={`${group.date} ${group.dinner.id}`} group={group} toggles={viewOnly ? null : toggles} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -201,9 +204,12 @@ function SkeletonGroups({ layout }: { layout: Breakpoint }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WeekProps = { weekStart: string; timezone: string; layout: Breakpoint; readOnly: boolean }
|
||||||
|
|
||||||
// WeekSupplyRun is the Supply run of the week starting on weekStart, or,
|
// WeekSupplyRun is the Supply run of the week starting on weekStart, or,
|
||||||
// when the week has none yet, the offer to make it from the week's plan.
|
// when the week has none yet, the offer to make it from the week's plan —
|
||||||
function WeekSupplyRun({ weekStart, timezone, layout }: { weekStart: string; timezone: string; layout: Breakpoint }) {
|
// not offered read-only.
|
||||||
|
function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) {
|
||||||
const run = useSupplyRun(weekStart)
|
const run = useSupplyRun(weekStart)
|
||||||
const generate = useGenerateSupplyRun(weekStart)
|
const generate = useGenerateSupplyRun(weekStart)
|
||||||
|
|
||||||
@@ -216,36 +222,42 @@ function WeekSupplyRun({ weekStart, timezone, layout }: { weekStart: string; tim
|
|||||||
if (run.data === null) {
|
if (run.data === null) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.page} ${styles[layout]}`}>
|
<div className={`${styles.page} ${styles[layout]}`}>
|
||||||
<EmptyState title="No shopping list yet" action={{ label: 'Make the shopping list', onClick: () => generate.mutate() }}>
|
{readOnly ? (
|
||||||
I'll gather the ingredients of this week's planned dinners into a Supply run.
|
<EmptyState title="No shopping list">This week has no Supply run in the Supply archive.</EmptyState>
|
||||||
</EmptyState>
|
) : (
|
||||||
|
<EmptyState title="No shopping list yet" action={{ label: 'Make the shopping list', onClick: () => generate.mutate() }}>
|
||||||
|
I'll gather the ingredients of this week's planned dinners into a Supply run.
|
||||||
|
</EmptyState>
|
||||||
|
)}
|
||||||
{generate.isError && (
|
{generate.isError && (
|
||||||
<ErrorBanner onRetry={() => generate.mutate()}>Couldn't make the shopping list. {generate.error.message}</ErrorBanner>
|
<ErrorBanner onRetry={() => generate.mutate()}>Couldn't make the shopping list. {generate.error.message}</ErrorBanner>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <SupplyRunList weekStart={weekStart} timezone={timezone} layout={layout} />
|
return <SupplyRunList weekStart={weekStart} timezone={timezone} layout={layout} readOnly={readOnly} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupplyRunPage is the Supply run (mock 1l on mobile, 2b on tablet, 2f on
|
// SupplyRunPage is the Supply run (mock 1l on mobile, 2b on tablet, 2f on
|
||||||
// desktop) of the week the Plan opens on — next week once this week's
|
// desktop) of the week the Plan opens on — next week once this week's
|
||||||
// plan_draft time has come, otherwise this week — or of the week chosen by
|
// plan_draft time has come, otherwise this week — or of the week chosen by
|
||||||
// ?week=, such as a week in the Supply archive. Offline, where the settings
|
// ?week=. Opened from the Supply archive it is readOnly: tagged Archived,
|
||||||
// cannot be read, it chooses the week by the schedule saved on this device.
|
// with no checkbox and nothing to make or send, whatever the week. Offline,
|
||||||
export function SupplyRunPage() {
|
// where the settings cannot be read, it chooses the week by the schedule
|
||||||
|
// saved on this device.
|
||||||
|
export function SupplyRunPage({ readOnly = false }: { readOnly?: boolean }) {
|
||||||
const problem = useSavedProblem()
|
const problem = useSavedProblem()
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{problem !== null && (
|
{problem !== null && (
|
||||||
<ErrorBanner>Changes to the shopping list can't be kept on this device while offline. {problem.message}</ErrorBanner>
|
<ErrorBanner>Changes to the shopping list can't be kept on this device while offline. {problem.message}</ErrorBanner>
|
||||||
)}
|
)}
|
||||||
<ChosenSupplyRun />
|
<ChosenSupplyRun readOnly={readOnly} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChosenSupplyRun() {
|
function ChosenSupplyRun({ readOnly }: { readOnly: boolean }) {
|
||||||
const layout = useBreakpoint()
|
const layout = useBreakpoint()
|
||||||
const settings = useHouseholdSettings()
|
const settings = useHouseholdSettings()
|
||||||
const [params] = useSearchParams()
|
const [params] = useSearchParams()
|
||||||
@@ -264,7 +276,7 @@ function ChosenSupplyRun() {
|
|||||||
}
|
}
|
||||||
if (!schedule) return <SkeletonGroups layout={layout} />
|
if (!schedule) return <SkeletonGroups layout={layout} />
|
||||||
const week = chosen ?? defaultWeek(schedule, new Date())
|
const week = chosen ?? defaultWeek(schedule, new Date())
|
||||||
return <WeekSupplyRun key={week} weekStart={week} timezone={schedule.timezone} layout={layout} />
|
return <WeekSupplyRun key={week} weekStart={week} timezone={schedule.timezone} layout={layout} readOnly={readOnly} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// isMonday says whether date is a real YYYY-MM-DD Monday.
|
// isMonday says whether date is a real YYYY-MM-DD Monday.
|
||||||
|
|||||||
Reference in New Issue
Block a user