import X from 'lucide-solid/icons/x'; import type { JSX } from 'solid-js'; import { createEffect, createResource, createUniqueId, For, Match, onCleanup, Show, Switch, } from 'solid-js'; import { api } from '@/api/client'; import { errorText } from '@/lib/errors'; import { statusLabel } from '@/lib/filterOpportunities'; import { formatDate, formatMoney } from '@/lib/format'; import { isFromFindATender } from '@/lib/opportunities'; import type { Opportunity } from '@/types/Opportunities'; import { ReleaseTabs } from './ReleaseTabs'; import { buttonPrimary, statusBadge } from './styles'; interface Props { /** The opportunity to show, from the list; undefined when the dialog is closed. */ opportunity: Opportunity | undefined; onClose: () => void; /** Called with a message when the opportunity's releases didn't load. */ onError: (message: string) => void; } const detailList = 'grid grid-cols-[max-content_1fr] gap-x-6 gap-y-1.5 text-sm'; const sectionHeading = 'mb-2 font-semibold'; const yesNo = (value: boolean): string => (value ? 'Yes' : 'No'); /** * A modal dialog with an opportunity's details, and its releases as tabs. * The details come from the list, so they show at once; the releases aren't * in the list, so the opportunity is fetched with them when the dialog * opens. The native keeps focus inside, closes on Escape and * returns focus to the button that opened it. */ export function OpportunityDialog(props: Props): JSX.Element { let dialog!: HTMLDialogElement; const titleId = createUniqueId(); let controller: AbortController | undefined; onCleanup(() => controller?.abort()); const [loaded, { refetch }] = createResource( () => props.opportunity?.uuid, (uuid) => { controller?.abort(); controller = new AbortController(); return api.getOpportunity(uuid, controller.signal); }, ); // Reading an errored resource throws, so check its state first. const releases = () => (loaded.state === 'ready' ? (loaded().releases ?? []) : []); const loadError = () => (loaded.state === 'errored' ? errorText(loaded.error) : undefined); createEffect(() => { if (props.opportunity && !dialog.open) dialog.showModal(); else if (!props.opportunity && dialog.open) dialog.close(); }); createEffect(() => { const err = loadError(); if (err !== undefined) props.onError(`The opportunity's releases didn't load: ${err}`); }); return ( props.onClose()} // A click on the backdrop lands on the dialog itself, outside its content. onClick={(e) => { if (e.target === dialog) dialog.close(); }} > {(opportunity) => (

{opportunity().title || 'Untitled opportunity'}

{opportunity().buyer_name}

Status
{statusLabel(opportunity().status)}
Published
{formatDate(opportunity().latest_release_date)}
Start
{formatDate(opportunity().start_date)}
End
{formatDate(opportunity().end_date)}
Value
{formatMoney(opportunity().gross_value)}
Procurement method
{opportunity().procurement_method || '—'}
Category
{opportunity().main_category || '—'}
Multiple lots
{yesNo(opportunity().multi_lot)}
SME suitable
{yesNo(opportunity().sme_suitable)}
VCSE suitable
{yesNo(opportunity().vcse_suitable)}
Documents
{yesNo(opportunity().has_documents)}
Source
{opportunity().source}
OCID
{opportunity().uuid}

Description

{opportunity().description}

0}>

Classifications

    {(code) => (
  • {code.code}{' '} {code.classification}
  • )}

Releases

Loading releases…

{(err) => ( )}
)}
); }