feat(apps): sales-expert screens in both apps + v2 admin home slider (D21, APP-02)

Both expert screens are built in user-app and user-app-v2, with byte-identical
template and script and a per-app style block only — verified by diffing
everything above <style>:

  ChildOrdersView  "سفارش مشتریان"  — legacy ChildOrders.vue. Keeps the
    reference's title, search, infinite paging, and per-card item count, total
    weight, customer name and the fixed «تحویل مسئول فروش» chip (the endpoint
    only returns that status, ApiMobileController:766).
  CustomerChoiceView "لیست مشتریان" — legacy customerChoice.vue. Keeps its
    title, per-search re-query and the reference's choice to LIST inactive
    customers (its active() scope is commented out at :187).

Reachable from inside the app, not just by URL: routes carry the reference's own
paths and names, the dashboard grows the two entries only for an expert (v1 as
tiles, v2 as its own rows), and a router guard sends an ordinary customer back
to their dashboard. None of that is relied on for safety — the server scopes
both endpoints on the AdminUser id in the token and answers 403 otherwise.

Not ported, both recorded in D21: the act-as api_token minting (:197-201) and
the child-order edit drawer, which recomputed an order's money from client-sent
values. The card opens a read-only summary instead.

APP-02 fixed: user-app-v2 gained getHomeSlider and now prefers admin-configured
slides, keeping its category-derived hero as the fallback and its own design.

Session stores stay byte-identical across the apps.
build 5/5, jest 45/45, expert login 36/36, apps smoke 43/43, typecheck clean.

[ref: henza_pwa/src/views/user/src/components/ChildOrders.vue, customerChoice.vue]
[ref: henza_pwa/src/router/index.ts:171-188]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-23 01:12:53 +03:30
co-authored by Claude Opus 5
parent 69f4ba2b29
commit f71fd5bfe1
99 changed files with 1991 additions and 99 deletions
+55
View File
@@ -189,6 +189,45 @@ export interface AdminSlide {
link: string;
}
/**
* D21 — a sales expert signed into the app. Ordinary customers get
* `isSalesExpert: false` and neither expert screen is shown to them.
*/
export interface ExpertIdentity {
isSalesExpert: boolean;
type: 'customer' | 'sales-expert';
}
/** One of the expert's referred customers ("انتخاب مشتری"). */
export interface ExpertCustomer {
id: string;
/** Legacy key the reference's card bound to. */
name: string;
fullName: string;
mobile: string;
phone: string | null;
store_name: string | null;
pasazh_name: string | null;
gram_level: string;
remain: string;
rial_remain: string;
active: number;
createdAt: string;
}
/** One row of "سفارش مشتریان". */
export interface ExpertChildOrder {
id: string;
orderNumber: string;
status: string;
/** Total order weight — the reference's `item.w` (ChildOrders.vue:55). */
w: string;
totalGoldWeightGrams: string;
createdAt: string;
items: OrderItemDetail[];
customer: { id: string; name: string; mobile: string } | null;
}
export const api = {
listProducts: () => request<ProductSummary[]>('/catalog/products'),
@@ -226,4 +265,20 @@ export const api = {
/** GET /home/slider — public, no auth. Empty array when the admin hasn't
* configured any slides (the caller falls back to a derived hero). */
getHomeSlider: () => request<AdminSlide[]>('/home/slider'),
// ── D21: sales-expert screens ────────────────────────────────────────
// All three scope themselves on the AdminUser id inside the token; no id
// ever travels from the client. An ordinary customer gets 403 on the first
// two, and `isSalesExpert: false` from whoami so the menu entries stay hidden.
getExpertIdentity: () => request<ExpertIdentity>('/expert/whoami'),
getExpertCustomers: (search?: string | null) =>
request<ExpertCustomer[]>('/expert/my-customers', {
method: 'POST',
body: JSON.stringify({ search: search ?? null }),
}),
getExpertChildOrders: (page = 1, search?: string | null) =>
request<{ rows: ExpertChildOrder[]; total: number; page: number; limit: number }>(
'/expert/child-orders',
{ method: 'POST', body: JSON.stringify({ page, search: search ?? null }) },
),
};