Commit Graph
21 Commits
Author SHA1 Message Date
bwilliams 43f9ef3223 Labelled an opportunity's ID as an OCID only when it is one 2026-10-21 14:15:04 +01:00
bwilliams 64886a2f1e Added links to EU Supply tenders, as there are to Find a Tender notices 2026-10-21 14:14:47 +01:00
bwilliams 5c62376b6e Made the table's columns sortable
Every column's header is a button that sorts the table by it, and
clicking it again sorts the other way. A column starts with the most
useful first: titles A to Z, the latest dates and highest values, Yes
before No, hidden first, and marks in the menu's order. Statuses sort
in the filters' order rather than alphabetically, and titles by their
words and numbers, ignoring case and punctuation. Rows with no value in
the column (no date, no value, no marks) go last either way, and rows
that tie are newest release first.

The sort is remembered with the other preferences, and is newest
release first until another is chosen, as before. Sorting has moved
out of filterOpportunities, which now keeps the order it's given.
2026-10-21 12:47:25 +01:00
bwilliams ff2aa316b5 Made the classification picker a tree, where ticking a code selects its whole branch
The classifications are grouped by kind (CPV, then Other) and, within
CPV, nested by the CPV hierarchy: each code sits under its nearest
ancestor in the list, so one whose parent isn't in use isn't lost. The
other codes have no hierarchy, so they're listed flat. Branches start
closed; a search shows the matching codes with their ancestors, opened
when no more than 150 match.

Ticking a code selects it and every code beneath it, and ticking a
whole selected branch clears it. The checkboxes are tri-state, showing
a dash when only part of a branch is selected, and a closed branch that
is partly selected says how many codes beneath it are. The selection
above the tree shows a whole selected branch as one chip, with the
number of codes beneath it, and removing that chip clears the branch.

The filter is unchanged: a set of classification ids, so a branch is
the codes in the picker when it's ticked.
2026-10-21 12:39:22 +01:00
bwilliams 2e6c21f9dd Swapped the order of the details and "Find a Tender" link buttons 2026-10-19 10:40:27 +01:00
bwilliams f8648d765a Added a dialog with each opportunity's details and releases
Every row has a details icon under the Find a Tender link. It opens a
dialog with the opportunity's details from the list, and fetches the
opportunity with its releases, shown as tabs, newest first. Each tab
summarises the release's tender, awards and contracts, links to its
Find a Tender notice where it is one, and has the release's JSON,
pretty-printed and highlighted, in a section laid out only when it's
opened.

The JSON is highlighted by splitting it into tokens shown as text, not
HTML, as releases come from outside sources. The README's saved
preferences now include the filters.
2026-10-19 10:37:27 +01:00
bwilliams 6fbf7e56b2 The classification filter now offers only the loaded opportunities' classifications
It lists the classifications the opportunities in the days window have,
not every one stored, plus any already chosen, so a selection stays
visible and can be removed when nothing in the window has it. The other
filters don't narrow it, so choosing a status doesn't make
classifications vanish.
2026-10-19 10:19:34 +01:00
bwilliams 0819fb442b The filters are now remembered in localStorage
Every filter, the days and title included, is stored under
job_search.filters when it changes and read back when the page loads.
A stored field that isn't valid (missing, from an older version or
edited by hand) falls back to its default, keeping the rest. Clear
filters still goes back to the defaults.
2026-10-19 10:11:19 +01:00
bwilliams 746fa0a9c1 Added a "Published in the last X days" filter, applied by the backend
GET /api/opportunities takes released_from, an RFC 3339 time, and lists
only the opportunities whose latest release is at or after it. It works
with paging and with changed_after, which keeps the same batch either
way; an invalid value is 400.

The frontend's filter defaults to 7 days, from midnight UK time, and
sends it with every load and catch-up, so a year of opportunities isn't
downloaded to show a week's. Changing the days loads the list again;
Clear filters goes back to 7.
2026-10-19 10:05:45 +01:00
bwilliams b8244da2e5 Added a "Hide all visible" button
It hides the opportunities the filters show that aren't hidden already,
after asking for confirmation with their count. Those the filters leave
out keep their marks, so clearing the filters shows them as before.

The backend has no bulk route, so each opportunity is hidden with its
own request. The rows go at once; every mark change is now saved at
most six at a time (lib/concurrency.ts), so hundreds of requests aren't
sent together. Any that fail are shown again, with one toast for them
all.
2026-10-19 09:47:49 +01:00
bwilliams b1cc9745ba Code Conventions change: A new package now goes in the same change as the code that needs it 2026-10-19 09:47:21 +01:00
bwilliams e98f18b790 Added a link from each opportunity to its latest Find a Tender notice
Opportunities from Find a Tender have a link icon before their title.
Clicking it fetches the opportunity with its releases (the list leaves
them out) and opens its newest release's notice,
www.find-tender.service.gov.uk/notice/<release_id>, in a new tab. The
tab is opened on the click and pointed at the notice when the request
returns, so it isn't blocked as a popup; if the request fails, or there
are no releases, it's closed and a toast says why. Opportunities from
other sources have no link for now, but keep its space so the titles
line up.

The icon is lucide-solid's, added for it at 1.52.0.
2026-10-19 09:38:14 +01:00
bwilliams a6630f112f Added tests for format and storage, run in UK time 2026-10-16 16:46:08 +01:00
bwilliams 13232bb15c Added tests for the opportunities' catch-up and the toasts 2026-10-16 16:41:10 +01:00
bwilliams f9064511b0 Added tests for web/src/api 2026-10-16 16:37:58 +01:00
bwilliams 60643546f6 Added tests for web/src/lib 2026-10-16 16:29:08 +01:00
bwilliams 67e2c96ca7 Added an SME suitable filter 2026-10-16 16:26:32 +01:00
bwilliams 49dd11be56 Errors now show as toasts in the bottom right corner 2026-10-16 16:22:19 +01:00
bwilliams 115ee29a85 Added Vitest to web/, ready for tests 2026-10-16 16:16:27 +01:00
bwilliams 5e6c011ecb Embedded the frontend in the binary, and built it in the build script
`npm run sync` in web/ copies the build output (web/dist, from npm run build)
to internal/frontend/dist, which the binary embeds. Go can't embed from
outside the package's directory (web/ is also outside the Go module), hence
the copy. Only dist/.gitkeep is committed, so a fresh clone still compiles,
and a binary built without syncing first serves no frontend.

job-search serve serves server.frontend.dir if it is set, or else the
embedded build, or warns that there is no frontend. scripts/build.sh builds
and syncs the frontend before building, running npm ci first for a release
or when web/node_modules is missing.
2026-10-16 16:03:15 +01:00
bwilliams 1832ea7437 Added the frontend in web/, following its code conventions, with linting and generated backend types
web/CODE_CONVENTIONS.md covers the Vite, SolidJS, Tailwind and TypeScript
frontend: tooling, files, imports, exports, naming, TypeScript, formatting,
Solid's reactivity, styling, the backend's types, API calls, errors,
security, formatting values, comments, accessibility, storage,
dependencies and tests. npm run check (strict tsc -b, ESLint and Prettier)
passes, and npm run build builds it.

ESLint (typescript-eslint with type information, eslint-plugin-solid,
simple-import-sort, eslint-comments, and rules for the naming, type import,
export type and promise conventions) and Prettier (with the Tailwind class
plugin) check what they can. Tool versions are pinned exactly. Imports
between folders use @/, which works in Vite as well as TypeScript.

The backend's types are generated from its OpenAPI description into
src/types/api.ts by npm run generate:api, which turns swag's x-nullable
into null types first; src/types/Opportunities.ts names them for the app.

The app works against the backend's API: it fetches every page of
/api/opportunities with each opportunity's marks, the classifications to
filter by from /api/classifications, sets and clears marks (hidden among
them) with the CSRF token, and shows the OCDS tender statuses. Its lists'
requests are aborted when reloaded or no longer needed. The dev server
proxies /api and /ws to job-search serve, by default at localhost:9393.
web/go.mod keeps web/ out of the Go module, as an npm package in
node_modules ships Go code.

The page stays up to date over the backend's WebSocket (src/api/events.ts,
with hand-written event types in src/types/Events.ts), reconnecting with a
growing delay. ClassificationsUpdated changes the filter's classifications
in place and OpportunityMarksUpdated sets an opportunity's marks (unless
this page is still saving its own change). OpportunitiesUpdated catches the
list up: it asks for the opportunities changed after the last change batch
it has seen (changed_after) and merges them in, reloading the whole list
only when the backend doesn't know the batch. A reconnect catches up the
same way and reloads the classifications.
2026-10-16 16:01:38 +01:00