feat(admin): D16 (b)(c) — Excel order-item report, document-confirm SMS, %name in bulk SMS
Finished from a parallel session that expired (owner: «بساز»).
- RPT-01: GET admin/report/order-items — legacy report/order_item's
32-column Excel (order items, one row per unit, plus financial
documents), RTL, Jalali dates. The «گزارش تجمیعی» screen gets
«دریافت فایل اکسل». Review fixes:
- weight: computedWeightGrams is already one unit's weight; legacy
divided it by qty again, under-reporting «بدهکار» on multi-unit
lines. Not repeated.
- «درصد»: the final percent (wage − discount), as legacy.
- dates: the end date covers its whole day; a bad date is a 400.
- SMS-01: confirming or rejecting a financial document texts the customer,
as legacy CashController::toggleConfiremCash.
- SMS-02: bulk SMS substitutes %name per recipient and is actually sent
through the configured gateway (D26 relay), no longer only logged.
- Docs: 01-ADMIN-MATRIX and D16-BC-GAP-SUMMARY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fbe4f38e06
commit
2d74f53d0a
@@ -2,6 +2,7 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
||||
import { MessageAudience, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { TableService } from '../admin-table/table.service';
|
||||
import { SmsService } from '../sms/sms.service';
|
||||
import type { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { ACCOUNTING_SETTING_PREFIX } from '../accounting-sync/accounting-config';
|
||||
import { PRICING_SETTING_PREFIX } from '../pricing/wage.service';
|
||||
@@ -23,6 +24,7 @@ export class AdminMiscService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly table: TableService,
|
||||
private readonly sms: SmsService,
|
||||
) {}
|
||||
|
||||
// ── Cities ─────────────────────────────────────────────────────────
|
||||
@@ -245,12 +247,9 @@ export class AdminMiscService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy `POST sms` — records the send.
|
||||
*
|
||||
* NOTE no SMS gateway is wired up: legacy handed the message to an external
|
||||
* provider, and this port has no credentials for one. The message is stored
|
||||
* with the audience it resolved to, so the log is accurate, but nothing is
|
||||
* actually transmitted. Flagged in docs/ADMIN-PORT-PLAN.md.
|
||||
* Legacy `POST sms` — resolves the audience, substitutes `%name` per
|
||||
* recipient (legacy SmsController::batch + replaceName), delivers via the
|
||||
* configured gateway (D26 relay), and records the send.
|
||||
*/
|
||||
async sendSms(
|
||||
data: { type?: string | number; mobile?: string; text?: string },
|
||||
@@ -262,6 +261,18 @@ export class AdminMiscService {
|
||||
const audience = this.resolveAudience(data.type);
|
||||
const recipients = await this.resolveRecipients(audience, data.mobile);
|
||||
|
||||
const needsNameSub = body.includes('%name');
|
||||
const customersByMobile = needsNameSub
|
||||
? await this.customerNamesByMobile(recipients)
|
||||
: new Map<string, string>();
|
||||
|
||||
for (const mobile of recipients) {
|
||||
const text = needsNameSub
|
||||
? body.replace(/%name/g, customersByMobile.get(mobile) ?? 'مشتری عزیز')
|
||||
: body;
|
||||
this.sms.send(mobile, text, { audience });
|
||||
}
|
||||
|
||||
return this.prisma.smsMessage.create({
|
||||
data: {
|
||||
audience,
|
||||
@@ -273,6 +284,18 @@ export class AdminMiscService {
|
||||
});
|
||||
}
|
||||
|
||||
private async customerNamesByMobile(mobiles: string[]): Promise<Map<string, string>> {
|
||||
const rows = await this.prisma.customer.findMany({
|
||||
where: { mobile: { in: mobiles } },
|
||||
select: { mobile: true, fullName: true },
|
||||
});
|
||||
const map = new Map<string, string>();
|
||||
for (const r of rows) {
|
||||
if (r.fullName) map.set(r.mobile, r.fullName + ' عزیز');
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy's numeric audience codes, read off views/sms/create.vue where each
|
||||
* option is gated by its own rid:
|
||||
|
||||
Reference in New Issue
Block a user