Dial popover: show the PBX extension name instead of the state text — server/utils/telephony/extensions.ts reads each extension's FreePBX display name from the astdb (AMI DBGet AMPUSER/<ext>/cidname, covered by the reporting class already granted to the logship manager user; cached 10 min per extension, fetched on the same socket as DeviceStateList, ERP user name stays the fallback), DialButton rows now read '401 Younes' with the state carried by the coloured dot + hover tooltip only, DirectoryExtension type gains pbxName (details)
Bank fetch page: /accounting/bank-transactions becomes the dedicated "Bankabruf" page (fetch from the bank), replacing the duplicate staged-statements/import tables — coverage table per connection (data from/to, counts, last fetch, 12-month strip green = staged / red = never fetched, click = preselect), backfill runner for past periods (months from a coverage-aware grid with "Select missing", everything since a date, free range, single day; chunks of max 31 days so PayPal's 30-day window and the proxy timeout are respected; sequential per connection + chunk, progress bar, per-chunk log, abort, retry failed, details via the fetch-result modal, dry-run toggle, hand-off to Banking → import), connection cards kept in a collapsible section; new proxy route bank-transactions/coverage.get.ts (Laravel banking/statements/coverage, fail-soft notice when not deployed); shared bk-* styles moved from the Banking page into app/assets/css/banking.css (loaded by both pages); Banking fetch modal links to the backfill page; menu labels "Bankabruf"/"Bank Fetch"; bankfetch_* translations en/de/es (details)
Dashboard calls card: '+' on unmatched numbers → quick-create contact modal — new components/dashboard/QuickContactModal.vue (teleported; name required + autofocused, phone preset from the call row incl. PBX caller-id name, optional e-mail and 'als Lead anlegen' flag; creates a plain ad_user via /api/admin/users/store, not a system user, current org); RecentCallsCard remembers checked-but-unmatched numbers (null entries) so the '+' only shows after the ES match finished, and stores the created user as the number's contact locally so the row flips to the contact menu without waiting for the ad_user index sync; rcc_add_contact/qcm_* translations en/de/es (details)
DPD labels: carry address2 (Adresszusatz) like DHL — dpd/index + dpd/multi now map body.address2 into the recipient: name2 when no company/c/o is set, otherwise the company keeps name2 and address2 goes to contact (the only other printed DPD line, 35 chars), plus a data-only comment mirror (70); <contact> emitted after <city> and <comment> after <email> per the DPD XSD order. DHL index/multi cap consignee name2/name3 at 50 chars so over-long values are truncated instead of rejected. Frontend unchanged (commission.vue already sends both fields to both carriers). (details)
Call recordings: async whisper transcription — transcribe.post.ts starts the job, waits max 20 s and answers 202 pending for anything longer (the edge nginx cuts requests at 60 s, long calls used to 504 while whisper kept running); new transcribe-status.get.ts polls the disk state (pending marker <file>.pending.json shared across PM2 instances, cache file = done, marker error = failed); shared statusResponse() in transcriptionResponse.ts; CallRecordingsModal polls every 3 s up to 25 min and shows the elapsed time (details)
Amazon Ads invoices → Eingangsrechnungen: new page /accounting/amazon-ads-invoices lists Sponsored-Ads invoices (Amazon Ads API when connected — LWA consent flow, profile picker, GET /invoices + /invoices/{id} + /billing/documents PDF — or console PDFs dropped on the page and parsed server-side), lets the user tick which ones to import and books each as a completed AP invoice on the fixed vendor 1043801 (org *): one charge line per campaign (default charge Werbekosten), tax by rate so GrandTotal = Amazon gross, DocumentNo/POReference = Amazon invoice no., duplicate guard, original Amazon PDF attached under PO_Invoice; Storno action voids + renames the number (<no>-VOID-<id>) so the same invoice can be re-imported; booked/Lexoffice status comes from iDempiere (vendor AP invoices matched by number). Lexoffice upload: purchase invoices now upload the attached vendor original instead of the Jasper print (no ZUGFeRD on foreign documents) and requireOriginal:true skips invoices without one (used by the Ads page). Store data/amazon-ads.db + PDF cache data/amazon-ads/ (git-ignored); AMAZON_ADS_* env keys (empty = PDF-upload mode until the Ads API approval exists); AgGrid wrapper registers RowGroupingPanelModule (error #200 on every grid page); menu + en/de/es labels; parser dev script reference/test-scripts/amazon-ads-parse.mjs + sample invoice (details)
Sales order ship-to name + dashboard calls card improvements (details)
Dial popover: show the PBX extension name instead of the state text — server/utils/telephony/extensions.ts reads each extension's FreePBX display name from the astdb (AMI DBGet AMPUSER/<ext>/cidname, covered by the reporting class already granted to the logship manager user; cached 10 min per extension, fetched on the same socket as DeviceStateList, ERP user name stays the fallback), DialButton rows now read '401 Younes' with the state carried by the coloured dot + hover tooltip only, DirectoryExtension type gains pbxName
Bank fetch page: /accounting/bank-transactions becomes the dedicated "Bankabruf" page (fetch from the bank), replacing the duplicate staged-statements/import tables — coverage table per connection (data from/to, counts, last fetch, 12-month strip green = staged / red = never fetched, click = preselect), backfill runner for past periods (months from a coverage-aware grid with "Select missing", everything since a date, free range, single day; chunks of max 31 days so PayPal's 30-day window and the proxy timeout are respected; sequential per connection + chunk, progress bar, per-chunk log, abort, retry failed, details via the fetch-result modal, dry-run toggle, hand-off to Banking → import), connection cards kept in a collapsible section; new proxy route bank-transactions/coverage.get.ts (Laravel banking/statements/coverage, fail-soft notice when not deployed); shared bk-* styles moved from the Banking page into app/assets/css/banking.css (loaded by both pages); Banking fetch modal links to the backfill page; menu labels "Bankabruf"/"Bank Fetch"; bankfetch_* translations en/de/es
Dashboard calls card: '+' on unmatched numbers → quick-create contact modal — new components/dashboard/QuickContactModal.vue (teleported; name required + autofocused, phone preset from the call row incl. PBX caller-id name, optional e-mail and 'als Lead anlegen' flag; creates a plain ad_user via /api/admin/users/store, not a system user, current org); RecentCallsCard remembers checked-but-unmatched numbers (null entries) so the '+' only shows after the ES match finished, and stores the created user as the number's contact locally so the row flips to the contact menu without waiting for the ad_user index sync; rcc_add_contact/qcm_* translations en/de/es
DPD labels: carry address2 (Adresszusatz) like DHL — dpd/index + dpd/multi now map body.address2 into the recipient: name2 when no company/c/o is set, otherwise the company keeps name2 and address2 goes to contact (the only other printed DPD line, 35 chars), plus a data-only comment mirror (70); <contact> emitted after <city> and <comment> after <email> per the DPD XSD order. DHL index/multi cap consignee name2/name3 at 50 chars so over-long values are truncated instead of rejected. Frontend unchanged (commission.vue already sends both fields to both carriers).
Call recordings: async whisper transcription — transcribe.post.ts starts the job, waits max 20 s and answers 202 pending for anything longer (the edge nginx cuts requests at 60 s, long calls used to 504 while whisper kept running); new transcribe-status.get.ts polls the disk state (pending marker <file>.pending.json shared across PM2 instances, cache file = done, marker error = failed); shared statusResponse() in transcriptionResponse.ts; CallRecordingsModal polls every 3 s up to 25 min and shows the elapsed time
Amazon Ads invoices → Eingangsrechnungen: new page /accounting/amazon-ads-invoices lists Sponsored-Ads invoices (Amazon Ads API when connected — LWA consent flow, profile picker, GET /invoices + /invoices/{id} + /billing/documents PDF — or console PDFs dropped on the page and parsed server-side), lets the user tick which ones to import and books each as a completed AP invoice on the fixed vendor 1043801 (org *): one charge line per campaign (default charge Werbekosten), tax by rate so GrandTotal = Amazon gross, DocumentNo/POReference = Amazon invoice no., duplicate guard, original Amazon PDF attached under PO_Invoice; Storno action voids + renames the number (<no>-VOID-<id>) so the same invoice can be re-imported; booked/Lexoffice status comes from iDempiere (vendor AP invoices matched by number). Lexoffice upload: purchase invoices now upload the attached vendor original instead of the Jasper print (no ZUGFeRD on foreign documents) and requireOriginal:true skips invoices without one (used by the Ads page). Store data/amazon-ads.db + PDF cache data/amazon-ads/ (git-ignored); AMAZON_ADS_* env keys (empty = PDF-upload mode until the Ads API approval exists); AgGrid wrapper registers RowGroupingPanelModule (error #200 on every grid page); menu + en/de/es labels; parser dev script reference/test-scripts/amazon-ads-parse.mjs + sample invoice
Sales order ship-to name + dashboard calls card improvements
Sales orders: edit.vue only fetched order data in onMounted, so switching between order tabs via the in-app tab bar (router.push into the same keep-alive'd component, only :id changes) kept showing the previously loaded order's data — including its business partner — until a hard reload. Added a route.params.id watcher (loadOrderData()) mirroring the existing fix on sales/invoices and sales/customer-rma edit pages. Also added the ship-to recipient name (C_BPartner_Location_ID.Name — the same field the orders grid shows as "Name", which can legitimately differ from the business partner's own Name) to the read-only order view.vue page, which was missing it entirely; edit.vue's OrderForm.vue already displayed it correctly.
Dashboard "Letzte Anrufe" card: the extension badge and duration/missed label were side-by-side in a fixed-width column that could overflow into neighboring text. Stacked them vertically instead and gave rows a bit more height. Added server-side search (number/name) and pagination — cdr.ts's recentCalls() now takes offset/search/direction and returns total + missedTotal (so the missed-count badge stays accurate regardless of the active direction tab or page).
Quick-contact modal (dashboard "+" on unmatched calls): added a "search existing contact" panel above the create-new form, so a call can be linked to an existing ad_user/lead instead of always creating a duplicate. Scoped to the caller's own organization by default ("search all organizations" to widen), with a type filter (All/Lead/External/Internal) mirroring the site's existing Users/Leads/External Users split. New GET /api/admin/users/search endpoint backs it; picking a result only PUTs the call's phone number onto the existing contact when it isn't already on file, to avoid clobbering an existing different number.
server/api/invoices/so.get.ts dropped its IsSOTrx-only filter (kept the existing DocStatus CO/CL/DR restriction) so /sales/invoices now returns both sales and purchase invoices — including the Amazon Ads AP invoices booked via /accounting/amazon-ads-invoices, which land in c_invoice like any other vendor invoice. Route name/URL kept as-is to avoid touching every caller.
New "Type" grid column (AgInvoiceKind.vue) tags each row AR/AR-CM/AP/AP-CM from C_Invoice.DocBaseType (the same field lexoffice-upload.post.ts already keys its AR/AP branching on), right after the document-number column.
InvoiceListFilters.vue gained two segmented filters: Receivables/Payables (side, derived from IsSOTrx / the C_DocTypeTarget_ID 1000006 "AP CreditMemo used as a customer credit note" exception — same convention as accounting/opos) and Lexoffice upload status (uploaded/not uploaded), both applied client-side alongside the existing fulfillment/payment/amount filters so the grand-total row and counts stay consistent.
The existing bulk Lexoffice upload button needed no changes — the shared lexoffice-upload.post.ts route already branches on DocBaseType per invoice (original vendor PDF for AP, Jasper print for AR) and processes mixed AR/AP id batches correctly; it just never had AP rows to select from before.
Fixed InvoiceAction.vue's row-menu edit/duplicate handlers, which always navigated to the sales-invoice pages — now they branch on isSOTrx to /procurements/invoices/{id}/{edit,duplicate} for AP rows, matching the branching AgLink.vue's document-number link already had.
Note: this route has no $top cap (pre-existing), so removing the AR-only restriction roughly doubles the worst-case row count for orgs with a long purchase-invoice history — worth watching if the page gets sluggish.
Lead page: move Aufnahmen button out of the offer/contract CTA group
The "Aufnahmen" (call recordings) button sat in the top-right CTA cluster alongside Angebot/Vertrag/SEPA/Onboarding, but it isn't part of that offer/contract workflow — it's phone-call history. Moved it next to the phone number / DialButton in the contact-chips row instead, gated on the lead having a phone number (nothing to match recordings against otherwise). The compact top-toolbar icon button (MobileActionsMenu) already had its own "Aufnahmen" entry and is unchanged.
DialButton: opt-in Recordings toggle (play-only) in the dial popover
Adds a showRecordings prop that puts a headset icon in the popover header next to Close. Toggling it switches the popover body to a compact list of FreePBX recordings matched to `number` (GET /api/telephony/recordings, same endpoint CallRecordingsModal.vue uses on contact pages, called here with just `numbers` — no source/recordId, so it works for numbers with no known contact too) — date, direction, duration and a play button only, no transcription/download clutter. Off by default everywhere; enabled on both DialButton usages in the dashboard's "Letzte Anrufe" card, so clicking a call's number now offers a quick way to listen to past recordings for that number right from the dial popover.
Recent calls card: stack date above number, give the name its own full line
The date used to be its own fixed 62px grid column, squeezing how much width the number/name column got. Moved it to sit above the number instead (inside the same column), so the name — which can run considerably longer than the number — gets the full row width on its own line rather than sharing it with a separate date column.
Locator priorities: exclude locators not allowed for shipping
M_LocatorType.IsAvailableForShipping=false locators (return bins, quarantine, reconditioning input, ...) are dropped before building pick-order conflicts/proposals, since shipment generation never picks from them anyway.
DialButton: fix recordings that appeared to play but stayed silent
Chrome's <audio> element does not behave like a normal failed request when the recordings route errors out (e.g. the PBX recordings service returns a JSON error body instead of audio): no 'error' event ever fires and .play()'s promise never settles either — it just sits at networkState=LOADING/readyState=HAVE_NOTHING with paused=false forever. Since the row's play/stop icon was driven only by a manually-toggled ref, a failed load looked exactly like "still playing" with no sound and no way to tell it failed. Add a stall-timeout guard (6s) that aborts the hung request, resets the button and shows an inline error message when playable data never arrives; canplay/playing clear the timer on success.
AgInvoiceKind: fix AP/AR tags never actually getting their colors
The tag colors never rendered: this component is used as an AG Grid cellRenderer, and its <style scoped> block never reached the rendered elements (they carried no data-v-* attribute at all), so every tag silently fell back to Bulma's plain grey .tag look regardless of AR/AP/ credit-memo type — the same gap already documented for AG Grid image cell renderers. Move the styling to a non-scoped block keyed off a dedicated .ag-ikind-tag marker class (so it can't leak onto the many other Bulma .tag elements elsewhere in the app), and switch from thin outlined tags to solid-filled ones so AP vs AR reads at a glance while scanning a long invoice grid.
Lexoffice upload: default AP invoices into Ausgaben, not the general inbox
Without an explicit category, uploads went through Lexoffice's plain Files-drop, which auto-creates its own uncategorized draft voucher — it never lands as a filed Einnahmen/Ausgaben entry, just a general/pending item in Lexoffice's own inbox. AR invoices only looked correctly filed under Einnahmen because someone picked a category for them every time, which goes through the Vouchers API instead.
Add a third modal option, "Mit Standardkategorie hochladen": books a real voucher via the same Vouchers API as an explicit category, using a generic catch-all per direction (Einnahmen for AR/AR-CM, Sonstige Ausgaben for AP/AP-CM) resolved per invoice so a mixed bulk upload still files each one correctly. The true "Ohne Kategorie hochladen" (plain, uncategorized Files-drop) option is unchanged and still available.
Also fixes APC (AP credit memo) incorrectly booking as voucher type purchaseinvoice instead of purchasecreditnote.
Bank fetch coverage: show import progress per month, not just staged
Coverage cells only reflected whether any statement was staged for a month; add the imported/open counts the coverage endpoint already returns so a month reads green only once fully imported, amber while partially imported, with the count shown. Also surface the imported count as a pill on the connection row, next to the existing pending one.
Squashed to drop an accidentally committed 134MB HAR capture file that was blocking the push (GitHub's 100MB file size limit); also gitignore *.har so this can't recur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G7aDXCyYUwGgTG81QTP8E
Quote: configurable grading pricing (time/fixed + description) and save-draft button
Grading is now priced by time/effort or a fixed price with a freetext description printed as a subtext, instead of a flat by-effort checkbox. Also adds a Save button on the quote modal to persist offer_conditions without sending the quote.
Add a "Kundenanforderungen" section to the quote modal — freetext bullet points each answered possible/not possible (green check / red X) or with a freetext answer, printed as a checklist in the PDF.
Also add "create lead" and "open all leads" icon buttons to the dashboard's latest-leads card header.
Amazon: switch central OAuth app to Published, fix test-connection + credential UI
Order source 1000026 stopped importing because the previous central Amazon app ("logship", Draft) hit its mandatory 180-day LWA Client Secret rotation deadline (2026-09-12) — LWA still authenticated fine, but every SP-API call was denied. Reconnecting the seller's own authorization doesn't rotate the app's secret, so that never fixed it. Standardized the central app on "LogShip App" (Published) instead, matching what order source 1000000 already used successfully.
Also: - test-connection.post.ts: report failure (with Amazon's real error text) when every SP-API endpoint is denied, instead of always defaulting to a misleading "missing Tax Invoicing role" message. - OrderSourceForm.vue: hide the raw Client ID / Client Secret / Refresh Token fields for Amazon by default (Connect/Reconnect manages them automatically); only reveal them behind an explicit "uses its own separate Amazon Developer app" toggle.
SIS: new Amazon pricing control page — compare internal vs Amazon price/qty, Buy Box, fees, push corrected prices
Admin-only page under Sales comparing internal price/stock against what's live on Amazon for each Amazon order source, with a one-click price push back to Amazon. New Amazon SP-API surface in amazonSpApi.ts: bulk merchant listings report (Reports API, the source of truth for "what's on Amazon"), Listings Items API read/patch, Product Pricing API (Buy Box), Fees Estimate API. Product matching is SKU-first (Value/SKU) with ASIN as fallback, via sisMatching.ts. Verified end-to-end against live production data, including a real price push (Amazon's Listings Items PATCH turned out to be async despite returning immediately, so pushes poll briefly then trust the accepted submission, with a short-lived overlay so a subsequent bulk report refresh can't revert a just-pushed price before Amazon's own report catches up).