import { Archive, Check, CloudUpload, MessageSquare } from 'lucide-react' import { useState } from 'react' import { Link, useSearchParams } from 'react-router' import { defaultWeek, isMonday, localNow, weekdayKicker, weekLabel } from '../plan/week.ts' import { ApiError } from '../api/errors.ts' import { formatClockTime, useHouseholdSettings } from '../settings/api.ts' import { useBreakpoint, type Breakpoint } from '../shell/useBreakpoint.ts' import { Button } from '../ui/Button.tsx' import buttonStyles from '../ui/Button.module.css' import { Checkbox } from '../ui/Checkbox.tsx' import { EmptyState } from '../ui/EmptyState.tsx' import { ErrorBanner } from '../ui/ErrorBanner.tsx' import { Icon } from '../ui/Icon.tsx' import { Skeleton } from '../ui/Skeleton.tsx' import { Tag } from '../ui/Tag.tsx' import { useGenerateSupplyRun, useSendToChat, useSupplyRun, useToggles, WEEK_PARAM, type SupplyRunGroup, type SupplyRunItem, type Toggles, } from './api.ts' import { keep, savedSchedule, useSavedProblem } from './offline.ts' import styles from './SupplyRunPage.module.css' // SUPPLY_ARCHIVE is the Supply archive screen. export const SUPPLY_ARCHIVE = '/plan/shopping/archive' // sentAt is when the Supply run was sent to Google Chat as its status line // says it, in the household's zone: "Sat 8:00 am". export function sentAt(at: Date, timezone: string): string { const day = new Intl.DateTimeFormat('en-US', { weekday: 'short', timeZone: timezone }).format(at) return `${day} ${formatClockTime(localNow(timezone, at).time)}` } // 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 // "have it" stands in for the quantity. A read-only Supply run shows the // same marks. An item toggled while the server cannot be reached // says it will sync. function ItemRow({ item, toggles }: { item: SupplyRunItem; toggles: Toggles | null }) { const name = ( <> {item.name} {toggles?.willSync.has(item.id) && ( will sync )} ) return (
  • {toggles === null ? ( {name} ) : ( toggles.toggle({ id: item.id, needed: !item.needed })} /> )} {item.needed ? item.qty : 'have it'}
  • ) } // GroupCard is one dinner's items: its name in Bagel over its day. function GroupCard({ group, toggles }: { group: SupplyRunGroup; toggles: Toggles | null }) { return (

    {group.dinner.name}

    {weekdayKicker(group.date)}
    ) } // SentStatus is the status line once the Supply run was sent to Google Chat. function SentStatus({ at, timezone }: { at: Date; timezone: string }) { return (

    Sent to Google Chat · {sentAt(at, timezone)}

    ) } // UnconfirmedStatus is the status line while a send to Google Chat whose // outcome is not known is recorded: it may or may not have been posted. function UnconfirmedStatus({ at, timezone }: { at: Date; timezone: string }) { return (

    Send to Google Chat not confirmed · {sentAt(at, timezone)} — check the space before sending again

    ) } // SendToChat posts the Supply run to Google Chat: "Send to Google Chat" // until it is sent, or a send is unconfirmed, then "Send again", which // posts it once more. function SendToChat({ sent, pending, onSend }: { sent: boolean; pending: boolean; onSend: () => void }) { return ( ) } // ArchiveLink opens the Supply archive: an outline action from tablet up, a // grape link on mobile. function ArchiveLink({ layout }: { layout: Breakpoint }) { const className = layout === 'mobile' ? styles.archiveLink : [buttonStyles.button, buttonStyles.outline, styles.action].join(' ') return ( {layout === 'mobile' ? 'Supply archive' : 'Archive'} ) } // SupplyRunList is a week's Supply run (mocks 1l, 2b, 2f): the kicker with // the week's dates, the title with the count still to buy of all items, and // 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 // header; a send that fails says why, and one whose outcome is not known // offers no retry and leaves the status line saying so. On a current Supply run each item toggles at once (useToggles); a // toggle refused puts the item back and says why. An archived one, or any // opened from the Supply archive (readOnly), is read-only and tagged // Archived, and a toggle it refused is not offered again. function SupplyRunList({ weekStart, timezone, layout, readOnly }: WeekProps) { const toggles = useToggles(weekStart) const send = useSendToChat(weekStart) const run = toggles.shown const sent = run.sent_to_chat_at !== null // A send whose outcome is not known is posted again only on request. const resend = sent || run.chat_send_unconfirmed_at !== null const unconfirmed = !send.isPending && run.chat_send_unconfirmed_at !== null && ( ) const sendUnconfirmed = send.error instanceof ApiError && send.error.code === 'chat_unconfirmed' const viewOnly = readOnly || run.status === 'archived' const wide = layout !== 'mobile' const failed = toggles.failed const failedItem = failed && run.groups.flatMap((group) => group.items).find((item) => item.id === failed.toggle.id) return (

    Supply run · {weekLabel(weekStart).dates}

    Shopping list

    {viewOnly && Archived}

    {run.needed_count} / {run.total_count}

    {run.status === 'archived' ? 'This list is in the Supply archive, as it was when the week ended.' : viewOnly ? "This week's list as it stands, read-only in the Supply archive." : "Uncheck anything you already have — it won't be counted."}

    {wide && (
    {!viewOnly && send.mutate(resend)} />}
    )}
    {failed !== null && ( toggles.toggle(failed.toggle)}> Couldn't mark {failedItem ? failedItem.name : 'an item'} {failed.toggle.needed ? 'to buy' : '“have it”'}.{' '} {failed.error.message} )} {send.isError && !viewOnly && ( send.mutate(resend)}> Couldn't send the shopping list to Google Chat. {send.error.message} )} {wide && run.sent_to_chat_at !== null && } {wide && unconfirmed} {run.groups.length === 0 ? ( None of this week's dinners has a recipe with ingredients yet. ) : (
    {run.groups.map((group) => ( ))}
    )} {!wide && (
    {run.sent_to_chat_at !== null && } {unconfirmed}
    )}
    ) } // SkeletonGroups stands in for the dinner cards while the Supply run loads. function SkeletonGroups({ layout }: { layout: Breakpoint }) { return (
    {Array.from({ length: 3 }, (_, i) => ( ))}
    ) } type WeekProps = { weekStart: string; timezone: string; layout: Breakpoint; readOnly: boolean } // 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 — // not offered read-only. function WeekSupplyRun({ weekStart, timezone, layout, readOnly }: WeekProps) { const run = useSupplyRun(weekStart) const generate = useGenerateSupplyRun(weekStart) // Offline, the Supply run saved on this device stays shown; the offline // pill says it is saved data. if (run.isError && !(run.data !== undefined && run.error instanceof TypeError)) { return void run.refetch()}>Couldn't load the shopping list. {run.error.message} } if (run.isPending || generate.isPending) return if (run.data === null) { return (
    {readOnly ? ( This week has no Supply run in the Supply archive. ) : ( generate.mutate() }}> I'll gather the ingredients of this week's planned dinners into a Supply run. )} {generate.isError && ( generate.mutate()}>Couldn't make the shopping list. {generate.error.message} )}
    ) } return } // 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 // plan_draft time has come, otherwise this week — or of the week chosen by // ?week=. Opened from the Supply archive it is readOnly: tagged Archived, // with no checkbox and nothing to make or send, whatever the week. Offline, // 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() return ( <> {problem !== null && ( Changes to the shopping list can't be kept on this device while offline. {problem.message} )} ) } function ChosenSupplyRun({ readOnly }: { readOnly: boolean }) { const layout = useBreakpoint() const settings = useHouseholdSettings() const [params] = useSearchParams() const chosen = params.get(WEEK_PARAM) // Read once, not on every render: a failed read reports a problem, which // renders this again. const [saved] = useState(() => keep(savedSchedule)) if (chosen !== null && !isMonday(chosen)) { return {`“${chosen}” is not the Monday of a week, as YYYY-MM-DD.`} } // fetch rejects with a TypeError only when the network fails. const schedule = settings.data ?? (settings.error instanceof TypeError ? saved : null) if (settings.isError && !schedule) { return void settings.refetch()}>Couldn't load the household settings. {settings.error.message} } if (!schedule) return const week = chosen ?? defaultWeek(schedule, new Date()) return }