feat: move brand name to one config and redesign the accounting card
اپ: نام برند در همهی صفحهها و پیامها یکسان شده و از یک تنظیم مرکزی خوانده میشود. اپ: کارت حسابداری در صفحهی مشتری بازطراحی شد؛ ماندهها و وضعیت لینک واضحتر نمایش داده میشوند. پنل: کارت حسابداری در پروندهی مشتری بازطراحی شد؛ ماندهها، آخرین ارسال و فرم لینک دستی واضحتر شدهاند. پنل: ستون «کد» در جدولها بهطور پیشفرض پنهان است و از انتخاب ستونها قابل نمایش است. پنل: تغییر نمایش ستونها در جدولها بلافاصله اعمال میشود. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
5d2a1efb0c
commit
532ceee189
@@ -9,6 +9,8 @@ import { OTP_ONLY_PASSWORD_HASH } from '../auth/password-hash';
|
||||
import { agentSalesWhere, customerSalesWhere } from '../sales-agent/agent-orders';
|
||||
import { SmsService } from '../sms/sms.service';
|
||||
import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service';
|
||||
import { AccountingConfigService } from '../accounting-sync/accounting-config.service';
|
||||
import { brandName, brandSiteUrl } from '../../common/brand';
|
||||
|
||||
/**
|
||||
* Wholesale ("عمده") customer lists — legacy views/customer/omde/*.
|
||||
@@ -137,12 +139,13 @@ export class AdminCustomersService {
|
||||
private readonly table: TableService,
|
||||
private readonly sms: SmsService,
|
||||
private readonly accounting: AccountingHooksService,
|
||||
private readonly accountingConfig: AccountingConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The welcome SMS the reference sends after every new customer is created
|
||||
* (AdminController.php:378-387), with the brand and the PWA link changed to
|
||||
* منتزر; same line order. The commented-out Android/iOS lines are not sent
|
||||
* BRAND_NAME / BRAND_SITE_URL (common/brand.ts); same line order. The commented-out Android/iOS lines are not sent
|
||||
* there and are not sent here.
|
||||
*
|
||||
* D20 — awaiting it would let a slow or unconfigured gateway hold up the
|
||||
@@ -153,11 +156,11 @@ export class AdminCustomersService {
|
||||
*/
|
||||
private sendWelcomeSms(mobile: string, firstName: string, lastName: string) {
|
||||
const body =
|
||||
'منتزر\n' +
|
||||
`${brandName()}\n` +
|
||||
`همکار گرامی ${firstName} ${lastName}\n` +
|
||||
`نام کاربری : ${mobile}\n` +
|
||||
'لینک pwa \n' +
|
||||
'https://bnk.montzar.ir';
|
||||
brandSiteUrl();
|
||||
|
||||
void this.sms.send(mobile, body).catch((e: unknown) => {
|
||||
// send() is documented never to throw; this is belt-and-braces so an
|
||||
@@ -182,6 +185,8 @@ export class AdminCustomersService {
|
||||
'goldCreditLimitGrams',
|
||||
'goldDebtGrams',
|
||||
'fiatDebtRial',
|
||||
'accountingGoldBalance',
|
||||
'accountingRialBalance',
|
||||
'referredByAgent.fullName',
|
||||
// legacy filter keys, accepted so the ported table models work unchanged
|
||||
'name',
|
||||
@@ -205,6 +210,8 @@ export class AdminCustomersService {
|
||||
gram_level: 'goldCreditLimitGrams',
|
||||
remain: 'goldDebtGrams',
|
||||
rial_remain: 'fiatDebtRial',
|
||||
acc_remain: 'accountingGoldBalance',
|
||||
acc_rial_remain: 'accountingRialBalance',
|
||||
caj: 'createdAt',
|
||||
uaj: 'updatedAt',
|
||||
'reagent.name': 'referredByAgent.fullName',
|
||||
@@ -289,7 +296,7 @@ export class AdminCustomersService {
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.customer.findMany({
|
||||
where: scoped,
|
||||
orderBy: orderBy as Prisma.CustomerOrderByWithRelationInput,
|
||||
orderBy: AdminCustomersService.nullsLast(orderBy) as Prisma.CustomerOrderByWithRelationInput,
|
||||
skip,
|
||||
take,
|
||||
include: { city: true, referredByAgent: true, avatarFile: true },
|
||||
@@ -297,7 +304,61 @@ export class AdminCustomersService {
|
||||
this.prisma.customer.count({ where: scoped }),
|
||||
]);
|
||||
|
||||
return this.table.respond(rows.map((r) => this.toRow(r)), total, query, page, limit);
|
||||
const linked = await this.accountingLinked(rows.map((r) => r.id));
|
||||
return this.table.respond(
|
||||
rows.map((r) => ({ ...this.toRow(r), ...AdminCustomersService.accountingCells(r, linked) })),
|
||||
total,
|
||||
query,
|
||||
page,
|
||||
limit,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The accounting balance is null for a customer not linked to the software,
|
||||
* and Postgres puts nulls FIRST on a descending sort — the biggest debtors
|
||||
* would sit under every unlinked customer.
|
||||
*/
|
||||
private static nullsLast(orderBy: Record<string, unknown>) {
|
||||
const [field, dir] = Object.entries(orderBy)[0] ?? [];
|
||||
return field === 'accountingGoldBalance' || field === 'accountingRialBalance'
|
||||
? { [field]: { sort: dir, nulls: 'last' } }
|
||||
: orderBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* D27 — which of these customers are linked to the CONNECTED accounting
|
||||
* software; null when none is connected (the list then hides its columns).
|
||||
* The provider is checked because a balance read from a previous provider
|
||||
* stays on the customer row.
|
||||
*/
|
||||
private async accountingLinked(ids: string[]): Promise<Set<string> | null> {
|
||||
const config = await this.accountingConfig.get();
|
||||
if (config.provider === 'none' || this.accountingConfig.inactiveReason(config)) return null;
|
||||
const links = await this.prisma.accountingLink.findMany({
|
||||
where: { provider: config.provider, entityType: 'CUSTOMER', entityId: { in: ids } },
|
||||
select: { entityId: true },
|
||||
});
|
||||
return new Set(links.map((l) => l.entityId));
|
||||
}
|
||||
|
||||
/** The balance in the accounting software (app convention: positive = owes). */
|
||||
private static accountingCells(
|
||||
c: {
|
||||
id: string;
|
||||
accountingGoldBalance: Prisma.Decimal | null;
|
||||
accountingRialBalance: Prisma.Decimal | null;
|
||||
accountingBalanceAt: Date | null;
|
||||
},
|
||||
linked: Set<string> | null,
|
||||
) {
|
||||
if (!linked) return {};
|
||||
const on = linked.has(c.id);
|
||||
return {
|
||||
acc_remain: on ? c.accountingGoldBalance : null,
|
||||
acc_rial_remain: on ? c.accountingRialBalance : null,
|
||||
acc_at: on ? c.accountingBalanceAt : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Legacy field names preserved so the ported table models bind unchanged. */
|
||||
@@ -1022,6 +1083,10 @@ export class AdminCustomersService {
|
||||
: VerificationStep.VERIFIED,
|
||||
verificationRejectionReason: null,
|
||||
verificationRejectedAt: null,
|
||||
// Reference MajorCustomerController.php:108-110 — accepting step one
|
||||
// activates the account. An app self-registration is created inactive
|
||||
// and can sign in only from here on.
|
||||
...(payload.sts_verify_step_one !== undefined ? { isActive: true } : {}),
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
|
||||
Reference in New Issue
Block a user