Dashboard: add call-blacklist button to Recent Calls card
Admin-only "Sperren" action per row, writes to FreePBX's astdb-based Blacklist via AMI DBPut (no MySQL table involved) so future calls from that number are rejected immediately.
Dashboard: always show Queue-routed calls on Recent Calls card
Previously the card only showed missed inbound + answered outbound, so support-line calls that came in via the Queue and got answered (possibly after a missed ring on another agent) never appeared. Queue-routed inbound calls now always show regardless of outcome; direct-dial inbound calls keep the missed-only behavior.
Amazon-side errors (report timeout, 429, ...) were silently triggering a full refresh-and-retry of the whole route (report poll + matching, or a full price push run twice) because event.context.lastIdempiereError is only ever set by iDempiere calls — tag Amazon-origin errors and skip the retry for them. Also: timeouts + retry-after-aware backoff on every raw SP-API/LWA call (none had any before), parallelize the previously serial SKU/ASIN/price chunk lookups in sisMatching.ts, a real loading spinner/elapsed-time + client-side fetch timeout instead of a static "Loading listings..." line, and a per-order-source push queue so rapid per-field price pushes never burst past Amazon's ~1 req/sec limit.
Also adds the requested "knight rider" glowing border animation on a price field while its push is in flight, handing off to the existing success/error flash once it resolves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G7aDXCyYUwGgTG81QTP8E
Telephony: fix call recording playback for .gsm files
Recordings served as .gsm played inconsistently in the dashboard phone card and the contact-page recordings modal ("play does nothing" for most, "player shows up" only for .wav/.mp3) — no browser ships a native GSM 06.10 decoder, so the <audio> element just sits at readyState=HAVE_NOTHING forever with no error event. ffmpeg (already used to decode these same .gsm files for whisper transcription) transcodes .gsm to mp3 server-side before streaming; .wav/.mp3 pass through unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G7aDXCyYUwGgTG81QTP8E
/sales/returns now fetches ?withLines=1 (the M_InOutLine + nested M_Product_ID expand already used by the dashboard "Letzte Retouren" card) and shows the returned products with their quantities inline in a new grid column, with its own text filter — no master-detail.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G7aDXCyYUwGgTG81QTP8E
Inventory Desktop: BOM tags + exclude-BOM filter on article search
Ports the same excludeBom checkbox + BOM/JTL-BOM tags mobile/inventory.vue already has on its article text search (server already supports both via /api/materials/products/search?excludeBom=), but defaults the checkbox to checked here — a physical stock count is about individually-located articles, not kit/BOM headers, unlike the mobile default (unchecked).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013G7aDXCyYUwGgTG81QTP8E
Return label: enlarge header-band text for readability
Company name and date/time in the dark top band were too small against the fill, especially on the common 74x52/56x75/102x152 label sizes. Bumps bar height and font sizes on both sides and raises the minimum shrink-to-fit floor for long company names.
Return labels no longer print per-article, so the legacy per-product printer field (m_product.labelprinter_rma) no longer fits how they're used. Replaces it with a printer picker in the top navbar (desktop) / returns page header (mobile), admin-only (IsClientAdministrator), that staff switch per working place — remembered per device via localStorage so it survives reloads. Falls back to the org's CUST_Printer marked "Default printer (for its type)" (printerType=lab), else the lab printer with the smallest id.
Also fixes a bug in /api/print/return-label where an explicit body.queue override was never actually consulted (labelPrinterId always defaulted to a truthy 'b', winning the || before queue was checked), and consolidates return-label printing to one request per return instead of grouping lines by printer — a return is one label listing every article, so the old per-product grouping could silently split one return across multiple partial-article labels.
Sales order edit: emphasize ship-to name over partner
On the completed-order shipping summary, the partner name was bold and the actual ship-to location (Name/Name2) was plain text, making it easy to misread which one the order ships to. Indents the partner row and drops its bold, and bolds the Name/Name2 values instead.
My-Tickets: fix staff replies stamped with wrong org, hiding them from limited-role customers
sendMessage() stamped new r_requestupdate rows with the replying staff member's currently active org (logship_organization_id cookie) instead of the ticket's own org. A limited-role customer's access is scoped to only their own org (IsAccessAllOrgs=false), so any reply written while staff had a different org active became invisible to them via iDempiere's row-level security — even though the ticket itself and their own messages stayed visible. Every other send path in this file (accounting-flag note, assign-to-merchant, status actions) already preferred the ticket's own org with this exact fallback; only the main composer's sendMessage() was missing it.
Confirmed against production data: customer "stoyan" (Wonderhome) was missing 4 of 9 staff replies on ticket 1000426, all sent while support had a different org active.