Conversation
`support/invoicing.ts` is the whole of invoicing: prepare the selection, fill in the host's own defaults, post the invoice with the session's client, stamp the entries with the line ids that come back, and land on the draft. Every entry point runs it and nothing else, so "invoice this" means the same four steps from a task row, a bulk bar, a task page, a project header or the unbilled time page. `prepareInvoice` now takes the selection rather than a list of entry ids, so a caller names whichever of the three shapes its screen knows. The contact is read for the currency it settles in, because the host compares that with the company setting when it decides whether an exchange rate is required. Three facts outlive the screen that caused them, so they live in `stores/invoicing.ts`: - one module-wide lock, because the sequence is four requests long and a second click elsewhere would race the same hours onto two invoices; - an invoice the host created whose entries were not stamped, which is the one failure that leaves work behind: the time still reads as unbilled, so `InvoiceRetryBanner` offers the idempotent confirm again and the module refuses to start another invoice until it lands; - whether the caller may invoice at all, learned from the first 403, since the settings endpoint does not say. `InvoiceNumberModal` is mounted once rather than by each screen that can invoice: a company that numbers its invoices by hand is the only one ever asked, and backing out of that question cancels the invoice rather than creating one with a blank number.
The placeholders the UI slice left behind now do the thing they promised. A task row and the task page invoice one task, the bulk bar invoices the selection as one invoice, and the project overview card and the project header invoice everything unbilled on a project. Each of them is a call to the same sequence, so none of them has an opinion of its own: the selection travels as task ids or a project id and the server decides what is still billable. Sending the bulk selection together rather than looping is the point of it, because that is what turns two customers into one clear message instead of two invoices. The actions show themselves only when they would mean something. A task with nothing billable left, or one already on an invoice, keeps the entry in the menu but refuses it with a title saying which of the two it is, so the menu never changes shape as work is logged. Everything hides once the server has refused the ability once. The retry banner rides on the Tasks screen, the task page and the project page, which is where someone will be standing when a stamp fails, and the task list and board refresh themselves through the shared task version.
The wizard asked its question backwards. Picking a customer and then their entries is four steps to reach something the task screens now do in one click, so it is gone, and what is left is the one question those screens cannot answer: who owes money for what, across every project, and which single entry should be left off. `UnbilledTimePage` keeps the `billing` id, path and ability the wizard had, so a bookmark and the registered ability both still resolve. It opens on the customers with time waiting, drops into their entries with the grouping picker and the checkboxes, and hands the ticked ids to the same sequence every other screen runs. The date window survives from the wizard, because month-end billing is the reason to be on this page at all. It is linked from the Reports and Projects headers and from nowhere in the sidebar: it is a report with a button, not a place to live. Amounts render in the company's format, as they already do on the task list and the project cards, rather than carrying a per-contact currency the page would have to fetch to label correctly. The customer and currency split stays in the data, because `prepare` refuses a selection that mixes either.
One `window.__invoiceshelf_vue` and no import from "vue", so the bundle still runs on the host's own Vue instance.
The task list carried eleven columns at the host table's `px-6`, which made it some three hundred pixels wider than the content area on a 1280 screen: the row menu sat past the right edge, behind a horizontal scrollbar nobody goes looking for, so editing or deleting a task from the list was unreachable at the width most people run the app at. The due date moves under the task name, where it is a detail of the task rather than a column that is empty on most rows, and the remaining cells use half the host's horizontal padding. The table now fits the content area at 1280 and 1440, and the row menu is where the eye expects it.
The three "Invoice task" and "Invoice project" placeholders are disabled until the invoicing slice lands, but the host button renders a disabled button exactly like a live one: same border, same colour, full opacity. A person reading the task page, the bulk bar or a project's Unbilled card saw an ordinary action and got nothing when they clicked it. The wrapper that already carries the "coming with invoicing" tooltip now dims the button and shows the not-allowed cursor, so the affordance reads as the promise it is.
The task form's Customer field printed the raw contact id, so editing a task showed "#22" where every other screen in the module shows "Acme Corp". The company's contact map already exists for exactly this, and the form is now the screen that asks for it when a task carries a customer: opening the form from the list, where nothing had loaded the map yet, fills the field in rather than leaving a number behind.
At `w-72` the board needs 1200 pixels for the four statuses a company starts with, which is eighty more than the content area offers on a 1440 screen: the Done column arrived sliced down the middle, its badge cut in half, which reads as a broken screen rather than as a board to scroll. Columns are `w-64` now, so the default board fits the page and a company that adds statuses still gets the sideways scroll the layout was built for.
…voicing-ui The verification pass had dimmed the disabled Invoice placeholders; the real actions from the invoicing UI replace them, so those hunks keep the invoicing side.
Contributor
Author
|
Superseded by #14, which carries this stack consolidated into three commits on top of main. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The frontend half of PR 3. The backend it talks to (
billing/preparetakingtask ids and a project,
InvoiceLineComposer, the line-note settings) is onthe base branch.
What changes
Invoicing stops being a place you go to and becomes something you do where the
work is.
support/invoicing.tsis the whole of it:billing/preparefor theselection, then the host's own defaults (due date, template, number, and an
exchange rate when the contact does not settle in the company currency), then
POST /api/v1/invoiceswith the session's client, thenbilling/confirm,then
/admin/invoices/{id}/edit, falling back to/viewwhen the host'sedit-invoiceguard refuses the push. The module still never writes to thehost invoice tables.
row menu, the bulk bar (the selection goes as one invoice, so two customers
are one clear message rather than two invoices), the task page header, the
project overview card and the project page header.
pages/UnbilledTimePage.vuetakes itsbillingid,path and ability and answers the one question the task screens cannot: who
owes money for what across every project, and which single entry to leave
off. Linked from the Reports and Projects headers, and from nowhere in the
sidebar.
The three things that can go wrong
mixed_billing_selectionbecomes"Select tasks of one customer", naming how many it spanned. The same refusal
also covers two currencies, which carries no ids, so the counted sentence is
used only when it is true.
nothing_to_invoicebecomes a plain notice,not an error against a field the user filled in correctly.
failure that leaves work behind: the invoice exists and the hours still read
as unbilled. It is parked in
stores/invoicing.ts,InvoiceRetryBanneroffers the idempotent confirm again on the Tasks screen, the task page, the
project page and the unbilled time page, and the module refuses to start
another invoice until it lands, so a second click can never produce a
duplicate.
Notes
GET /api/v1/next-numberunless thecompany has
invoice_auto_generate = NO, or the endpoint could not answer,in which case
InvoiceNumberModalasks. That dialog is mounted once as acompany layout overlay rather than by each screen that can invoice; backing
out of it cancels the invoice rather than creating one with a blank number.
prepare, since the modulesettings endpoint does not report it. After one refusal the actions stop
offering themselves for the session.
already do on the task list and the project cards, rather than fetching a
per-contact currency to label them with. The customer and currency split
stays in the data, because
preparerefuses a selection that mixes either.the host, which would otherwise lock the module out of invoicing for the
session. It confirms, and says the time stays unbilled.
Verified
pnpm run lint,pnpm exec tsc --noEmit,pnpm run build(onewindow.__invoiceshelf_vue, nofrom "vue",dist/reproducible),composer run lint,composer run test312 green and unchanged. No browserpass from this worktree; the orchestrator runs the live invoicing pass after
the merge.
https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E