parity(admin): D17 — SalesExpertProfile, AdminUser name parts, agent grid fields
Restores the sales-expert fields that were lost when a sales expert became
an AdminUser instead of a type=1 Customer row.
New SalesExpertProfile holds cityId, gramLevel, phone and nationalCode. A
separate table rather than columns on AdminUser, because these mean
something only for role = SALES_EXPERT. gramLevel is DECIMAL(14,4), matched
to Customer.goldCreditLimitGrams so the same credit limit has identical
precision on both tables. phone and nationalCode copy the reference exactly:
nullable, NOT unique, no format rule — createCustomer validates only mobile
and percent, and both fields are required:false on the reference form.
Inventing a uniqueness constraint would reject data the reference accepts.
reagent_id is deliberately NOT ported (D17). Every use of it in the
reference reads a CUSTOMER's reagent_id, which the target already models as
Customer.referredByAgentId. On a sales expert's own form it was a side
effect of the shared customers form: filling it removed that expert from
the selectable-agent list, because queryReagent filters on
whereNull('reagent_id'). The target's equivalent filters on role and active
only, so no agent can be dropped — strictly better than the reference.
AdminUser gains firstName/lastName with the same additive migration,
backfill and sync trigger as Customer, for all admins rather than only
sales experts. This closes D0 requirement 7, and every remaining
`fname: fullName, lname: ''` was fixed — six further sites across
admin-users, admin-comments, admin-documents, admin-orders and
admin-preorders.
listReagents now returns real city_id, city, gram_level, phone and ncode,
and left-joins so an expert without a profile still lists with empty cells
rather than vanishing. The two grid filters map through the relation. The
service upserts a profile on create and on a role change to SALES_EXPERT,
and the migration backfilled one for every existing expert.
ReagentCustomer.vue gains the four fields plus the city -> commission
autofill, ported verbatim including the reference's falsy test so a city
percent of 0 also falls through to the aux lookup (replicated, not fixed).
Tests: new sales-expert round trip 33/33, covering create with every field,
an edit form with each field populated separately, a byte-identical no-op
save, single-field edits, both grid filters with negative cases, the
left-join case and the referrer dropdown. «محمد علی» + «احمدی» survives
intact. Plus jest 32/32, auth-smoke 53/53, customer trigger 9/9, typecheck
clean on api and admin-panel. Migration dry-run twice in a rolled-back
transaction before applying; drift is zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3af31948e
commit
2194b3d4d9
@@ -322,10 +322,16 @@ export class AdminCustomersService {
|
||||
|
||||
const aliases: Record<string, string> = {
|
||||
name: 'fullName',
|
||||
lname: 'fullName',
|
||||
// D0/D17 — real columns on AdminUser now, as on Customer.
|
||||
fname: 'firstName',
|
||||
lname: 'lastName',
|
||||
caj: 'createdAt',
|
||||
active: 'isActive',
|
||||
percent: 'wagePercent',
|
||||
// D17 — these live on the related SalesExpertProfile, so the grid's two
|
||||
// filters have to reach through the relation.
|
||||
city_id: 'salesExpertProfile.cityId',
|
||||
gram_level: 'salesExpertProfile.gramLevel',
|
||||
};
|
||||
const mapField = (f?: string) => (f ? (aliases[f] ?? f) : f);
|
||||
|
||||
@@ -344,8 +350,21 @@ export class AdminCustomersService {
|
||||
};
|
||||
|
||||
const { where, orderBy, skip, take, page, limit } = this.table.build(aliased, {
|
||||
searchable: ['fullName', 'mobile'],
|
||||
allowed: ['id', 'fullName', 'mobile', 'isActive', 'createdAt', 'wagePercent', 'role'],
|
||||
searchable: ['fullName', 'firstName', 'lastName', 'mobile'],
|
||||
allowed: [
|
||||
'id',
|
||||
'fullName',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'mobile',
|
||||
'isActive',
|
||||
'createdAt',
|
||||
'wagePercent',
|
||||
'role',
|
||||
// D17 — the grid's «شهر» and «سطح اعتبار» filters.
|
||||
'salesExpertProfile.cityId',
|
||||
'salesExpertProfile.gramLevel',
|
||||
],
|
||||
defaultOrderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
@@ -359,7 +378,14 @@ export class AdminCustomersService {
|
||||
orderBy: orderBy as Prisma.AdminUserOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: { branch: true, _count: { select: { referredCustomers: true } } },
|
||||
// D17 — a LEFT JOIN by nature in Prisma: an expert without a profile
|
||||
// still comes back, with `salesExpertProfile` null, so a missing row
|
||||
// degrades to empty cells rather than dropping the agent from the grid.
|
||||
include: {
|
||||
branch: true,
|
||||
salesExpertProfile: { include: { city: true } },
|
||||
_count: { select: { referredCustomers: true } },
|
||||
},
|
||||
}),
|
||||
this.prisma.adminUser.count({ where: scoped }),
|
||||
]);
|
||||
@@ -374,11 +400,17 @@ export class AdminCustomersService {
|
||||
return {
|
||||
id: r.id,
|
||||
name: r.fullName,
|
||||
fname: r.fullName,
|
||||
lname: '',
|
||||
// D0 requirement 7 — the real parts, as on Customer. This used to be
|
||||
// `fname: r.fullName, lname: ''`, the same defect the customer forms had.
|
||||
fname: r.firstName ?? r.fullName,
|
||||
lname: r.lastName ?? '',
|
||||
mobile: r.mobile,
|
||||
city_id: null,
|
||||
city: null,
|
||||
// D17 — real values now, so the grid's city column and its filter work.
|
||||
// Null-safe: an expert with no profile row simply shows empty.
|
||||
city_id: r.salesExpertProfile?.cityId ?? null,
|
||||
city: r.salesExpertProfile?.city
|
||||
? { id: r.salesExpertProfile.city.id, title: r.salesExpertProfile.city.name }
|
||||
: null,
|
||||
percent: r.wagePercent,
|
||||
grams_sell: agg._sum.totalGoldWeightGrams ?? 0,
|
||||
child: r._count.referredCustomers,
|
||||
@@ -388,12 +420,15 @@ export class AdminCustomersService {
|
||||
caj: r.createdAt,
|
||||
uaj: r.createdAt,
|
||||
branch: r.branch ? { id: r.branch.id, name: r.branch.name } : null,
|
||||
// Legacy columns an AdminUser has no counterpart for.
|
||||
gram_level: null,
|
||||
// D17 — from SalesExpertProfile. Null-safe for an expert with no row.
|
||||
gram_level: r.salesExpertProfile?.gramLevel ?? null,
|
||||
phone: r.salesExpertProfile?.phone ?? null,
|
||||
ncode: r.salesExpertProfile?.nationalCode ?? null,
|
||||
// Still genuinely absent: a sales expert is an AdminUser and has no
|
||||
// ledger of its own, so it carries no gram or rial debt (D10).
|
||||
remain: null,
|
||||
rial_remain: null,
|
||||
phone: null,
|
||||
address: null,
|
||||
address: r.address ?? null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user