Conversation
…pared invoice The wizard's first step needs to know who is worth opening before it asks for anyone's entries, so `billing/customers` rolls the unbilled rule up over the whole company, one row per customer and currency. Its review step needs the work behind the totals, so `billing/unbilled` now also answers with a row per entry carrying the task, project, member and day. `prepare` gains the keys the host's own invoice writer reads: a line now carries its zeroed discount and tax fields, so DocumentItemService never reaches for a missing index, and `notes`, `template_name` and `taxes` arrive as placeholders the browser fills in from the company defaults.
New files rather than additions to the shared ones, so the wizard never edits a line another slice is editing. The host endpoints sit here beside the module ones because the wizard reads both: the invoice write itself goes to the host's own endpoint through the session client, and the number, the templates, the exchange rate and the company defaults all come from the endpoints the host's own invoice form reads, so a company that numbers by hand, defaults to a custom template or bills in a second currency gets the same answer here.
Four steps on one page: pick a customer with time waiting, tick the entries off, check what the invoice will say, create it. The steps are one screen's state rather than four routes, because a half-finished selection is not something to leave in the address bar. The write path keeps the module out of the host's invoice tables. The browser posts the prepared body to the host's own endpoint with the session client, so it gets the host's permissions, validation and numbering, and hands the created line ids back to `billing/confirm`, which stamps the entries. A stamp that does not land leaves a live invoice and unbilled time, so that case gets its own banner with a retry: confirming is idempotent. A card names a customer and a currency, and the entries step narrows to it. Money in two denominations cannot be added up and an invoice is written in one, which is also why `prepare` refuses a mixed selection. Two ways in: a button on the time page header, and the unbilled figure on a project's overview, which carries the customer through as a query parameter. The query is read off the injected router, because a module page is registered with `props: true`, which carries route params and not the query. The entry table shows every column at every width rather than hiding two on a narrow screen: the host's stylesheet is loaded after the module's and carries a plain `.hidden`, which beats a `sm:table-cell` from here, so a hidden column would never come back. It scrolls sideways instead.
The package ships compiled, so `dist/` travels with the source change and the module installs without a build step on the target system.
Keeps both registrations in init.ts and rebuilds dist/ from the union. Also lets the time page's project filter keep name order now that the list defaults to newest first, and drops the hidden/sm:flex pattern the host's late stylesheet defeats in favour of max-sm:hidden.
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.
M6 of the Tasks and Projects module: the screen that turns unbilled hours into a draft invoice, plus the two backend pieces it needed.
Based on
feat/ui-time.Backend
GET billing/customerslists who has billable time waiting, one row per customer and currency (customer_id,entries,minutes,amount,currency_id), over exactly the ruleunbilled()already applies to one customer. Both now shareunbilledEntriesForTasks(), so the internal-project exclusion, the date range and the vanished-invoice rule are written once.GET billing/unbilledadditionally answers with a row per entry (task, project, member, day, minutes, amount, rate, description), which is what the review step ticks off. Additive; nothing in the existing shape moved.POST billing/preparenow returns every key the host's own invoice writer reads. A line carriesdiscount_type,discount,discount_val,taxandtaxessoDocumentItemService::createItemsnever reaches for a missing index, andnotes,template_nameandtaxesarrive as placeholders the browser fills in from the company defaults.groups[i]still lines up withitems[i].Frontend
pages/BillingPage.vue, registered asbillingbehindtasks-projects:invoice-tasks, four steps on one page:billing/customers, names resolved through the hostGET /api/v1/customers, with an optional date range.billing/prepare, the lines and totals, invoice date, a due date defaulted frominvoice_due_date_days, the template list, the number fromGET /api/v1/next-number(editable when the company does not auto-generate, and unlocked by a host refusal), and the exchange rate fromGET /api/v1/currencies/{id}/exchange-ratewhen the contact settles in another currency. Host 422s land here as field errors.POST /api/v1/invoicesthrough the injected client, thenbilling/confirmwith the line ids zipped positionally ontogroups. A stamp that does not land gets a recovery banner with a retry, because confirming is idempotent.Two ways in: an "Invoice time" button on the time page header, and the unbilled figure on a project's overview, which carries
?customer_id=through.New files only for the slice (
registrations/billing.ts,messages/billing.ts,api/billing.ts,types/billing.ts);init.tsgains one import and one call.Notes
preparerefuses such a selection, so this keeps every selection something it will accept. The demo data has exactly this shape, since some entries predate the project's currency..hidden, which beats asm:table-cellfrom here, so a hidden column would never come back. It scrolls sideways instead. The same trap applies to the existinghidden ... sm:flexon the time page header chip, which is out of scope here.round(hours to 2dp x rate), so a very short entry can differ by a cent or two (1 minute at 90.00/h rolls up as 1.50 and bills as 1.80). That rounding isprepare's existing contract: the line total isquantity * priceso the invoice the host builds matches the preview exactly.Verified
pnpm run lint,pnpm exec tsc --noEmit,pnpm run build(onewindow.__invoiceshelf_vue, nofrom "vue"),composer run lint,composer run test(240 green, was 229), and both module validators.End to end in the devenv stack: invoiced Acme Corp's unbilled time as INV-000038 ($135.00, one "Wireframes" line at 1.5h x $90.00), both entries stamped and read-only in the time editor, then the remaining entry as INV-000039. The wizard's first step then reports nothing to invoice. Clean browser console.
https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E