diff --git a/apps/admin-panel/src/views/report/order_item.vue b/apps/admin-panel/src/views/report/order_item.vue
index 8df7b42..829b088 100644
--- a/apps/admin-panel/src/views/report/order_item.vue
+++ b/apps/admin-panel/src/views/report/order_item.vue
@@ -14,7 +14,11 @@
- اعمال
+
+ اعمال
+
+ دریافت فایل اکسل
+
@@ -46,6 +50,7 @@
diff --git a/apps/api/package.json b/apps/api/package.json
index 8dec4f2..5e7d526 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -27,6 +27,7 @@
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
+ "exceljs": "^4.4.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.2.2",
diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts
index 1366eb3..712ee87 100644
--- a/apps/api/src/app.module.ts
+++ b/apps/api/src/app.module.ts
@@ -32,6 +32,7 @@ import { AdminInvoicesModule } from './modules/admin-invoices/admin-invoices.mod
import { AdminMiscModule } from './modules/admin-misc/admin-misc.module';
import { AdminUploadsModule } from './modules/admin-uploads/admin-uploads.module';
import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module';
+import { AdminReportModule } from './modules/admin-report/admin-report.module';
import { SalesAgentModule } from './modules/sales-agent/sales-agent.module';
@Module({
@@ -58,6 +59,7 @@ import { SalesAgentModule } from './modules/sales-agent/sales-agent.module';
AdminMiscModule,
AdminUploadsModule,
AdminDashboardModule,
+ AdminReportModule,
CatalogModule,
CustomersModule,
SmsModule,
diff --git a/apps/api/src/modules/admin-documents/admin-documents.service.ts b/apps/api/src/modules/admin-documents/admin-documents.service.ts
index 2bd7911..160809f 100644
--- a/apps/api/src/modules/admin-documents/admin-documents.service.ts
+++ b/apps/api/src/modules/admin-documents/admin-documents.service.ts
@@ -3,6 +3,7 @@ import { LedgerCurrency, LedgerDirection, LedgerSourceType, Prisma } from '@pris
import { PrismaService } from '../../prisma/prisma.service';
import { TableService } from '../admin-table/table.service';
import { LedgerService } from '../accounting/ledger.service';
+import { SmsService } from '../sms/sms.service';
import type { TableQueryDto } from '../admin-table/table-query.dto';
/**
@@ -62,6 +63,7 @@ export class AdminDocumentsService {
private readonly prisma: PrismaService,
private readonly table: TableService,
private readonly ledger: LedgerService,
+ private readonly sms: SmsService,
) {}
private static readonly ALLOWED = [
@@ -313,6 +315,7 @@ export class AdminDocumentsService {
if (!e.confirmed) {
await this.ledger.confirmEntry(id, adminUserId);
+ this.notifyDocumentConfirm(e.customerId, true);
return this.findOne(id);
}
@@ -335,9 +338,24 @@ export class AdminDocumentsService {
});
});
+ this.notifyDocumentConfirm(e.customerId, false);
return this.findOne(id);
}
+ // Legacy CashController::toggleConfiremCash sent an SMS on both outcomes.
+ private notifyDocumentConfirm(customerId: string, confirmed: boolean) {
+ const text = confirmed
+ ? 'هنزا گلد\nسند مالی شما توسط مدیر سیستم تایید شد.'
+ : 'هنزا گلد\nسند مالی شما توسط مدیر سیستم رد شد.';
+
+ this.prisma.customer
+ .findUnique({ where: { id: customerId }, select: { mobile: true } })
+ .then((c) => {
+ if (c) this.sms.send(c.mobile, text);
+ })
+ .catch(() => {});
+ }
+
/**
* Legacy `DELETE cash/{id}`.
*
diff --git a/apps/api/src/modules/admin-misc/admin-misc.cities.spec.ts b/apps/api/src/modules/admin-misc/admin-misc.cities.spec.ts
index 8f28a58..5f85cde 100644
--- a/apps/api/src/modules/admin-misc/admin-misc.cities.spec.ts
+++ b/apps/api/src/modules/admin-misc/admin-misc.cities.spec.ts
@@ -27,7 +27,7 @@ describe('AdminMiscService — city tree', () => {
update,
},
};
- service = new AdminMiscService(prisma as never, {} as never);
+ service = new AdminMiscService(prisma as never, {} as never, {} as never);
});
it('a city without its own percent takes the nearest ancestor’s (legacy _getP)', async () => {
diff --git a/apps/api/src/modules/admin-misc/admin-misc.service.ts b/apps/api/src/modules/admin-misc/admin-misc.service.ts
index dd185c3..d2de577 100644
--- a/apps/api/src/modules/admin-misc/admin-misc.service.ts
+++ b/apps/api/src/modules/admin-misc/admin-misc.service.ts
@@ -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();
+
+ 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