95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import type { Opportunity, OpportunityRelease } from '@/types/Opportunities';
|
|
|
|
/**
|
|
* The opportunities with changed merged in: each replaces the opportunity
|
|
* with its UUID, in its place, or is added at the end if there's none.
|
|
*/
|
|
export function mergeOpportunities(
|
|
opportunities: readonly Opportunity[],
|
|
changed: readonly Opportunity[],
|
|
): Opportunity[] {
|
|
const byUuid = new Map(opportunities.map((opportunity) => [opportunity.uuid, opportunity]));
|
|
for (const opportunity of changed) byUuid.set(opportunity.uuid, opportunity);
|
|
return [...byUuid.values()];
|
|
}
|
|
|
|
/** The opportunity's newest release, or undefined if it has none. */
|
|
export function latestRelease(opportunity: Opportunity): OpportunityRelease | undefined {
|
|
let latest: OpportunityRelease | undefined;
|
|
for (const release of opportunity.releases ?? []) {
|
|
if (!latest || Date.parse(release.release_date) > Date.parse(latest.release_date)) {
|
|
latest = release;
|
|
}
|
|
}
|
|
return latest;
|
|
}
|
|
|
|
/** The page of a Find a Tender notice; a Find a Tender release's ID is its notice's. */
|
|
export function findATenderNoticeUrl(releaseId: string): string {
|
|
return `https://www.find-tender.service.gov.uk/notice/${encodeURIComponent(releaseId)}`;
|
|
}
|
|
|
|
// EU Supply's releases are the portal's tenders, as the backend stores them
|
|
// (internal/eusupply's Tender), with the address of the tender's page.
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === 'object' && value !== null;
|
|
|
|
const isWebAddress = (address: string): boolean => {
|
|
try {
|
|
const { protocol } = new URL(address);
|
|
return protocol === 'https:' || protocol === 'http:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The page of an EU Supply tender: the url in its release's data, as it is,
|
|
* or undefined if there's none. Only http and https addresses are used, so
|
|
* a malformed or javascript: one is never opened.
|
|
*/
|
|
export function euSupplyTenderUrl(data: string): string | undefined {
|
|
let tender: unknown;
|
|
try {
|
|
tender = JSON.parse(data);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
if (!isRecord(tender) || typeof tender.url !== 'string') return undefined;
|
|
return isWebAddress(tender.url) ? tender.url : undefined;
|
|
}
|
|
|
|
/** A site an opportunity's releases can be opened on. */
|
|
export interface NoticeSite {
|
|
/** The site's name, for labels: "Open the latest notice on <name>". */
|
|
name: string;
|
|
/** The page of release on the site, or undefined if it has none. */
|
|
urlFor: (release: OpportunityRelease) => string | undefined;
|
|
}
|
|
|
|
// By the source the backend gives the opportunities fetched from each site.
|
|
const NOTICE_SITES: Record<string, NoticeSite> = {
|
|
FindATender: {
|
|
name: 'Find a Tender',
|
|
urlFor: (release) => findATenderNoticeUrl(release.release_id),
|
|
},
|
|
'EU Supply - UK': {
|
|
name: 'EU Supply',
|
|
urlFor: (release) => euSupplyTenderUrl(release.data),
|
|
},
|
|
};
|
|
|
|
/** The site the opportunity's releases can be opened on, or undefined for sources without one. */
|
|
export function noticeSite(opportunity: Opportunity): NoticeSite | undefined {
|
|
return NOTICE_SITES[opportunity.source];
|
|
}
|
|
|
|
/**
|
|
* What the opportunity's UUID is called on screen: "OCID" for an OCDS
|
|
* contracting process's id, and "ID" for the other kinds, such as
|
|
* eusupply-uk-<tender id>, which the app makes up itself.
|
|
*/
|
|
export function uuidLabel(opportunity: Opportunity): string {
|
|
return opportunity.uuid_type === 'ocid' ? 'OCID' : 'ID';
|
|
}
|