🚀 Elite Accountant Hub: Foundation & Trojan Horse deployment

This commit is contained in:
Hamza-Ayed
2026-04-22 01:05:25 +03:00
parent 2e2d76c0a8
commit 444097814d
23 changed files with 796 additions and 88 deletions

View File

@@ -32,9 +32,9 @@ JWT_REFRESH_EXPIRY=
ENCRYPTION_KEY=
# ── JoFotara ───────────────────────────────────────────────
JOFOTARA_SANDBOX_URL=
JOFOTARA_PROD_URL=
JOFOTARA_ENV=
JOFOTARA_SANDBOX_URL=https://sandbox.jofotara.gov.jo/core/invoices
JOFOTARA_PROD_URL=https://backend.jofotara.gov.jo/core/invoices
JOFOTARA_ENV=production
# ── Gemini AI ──────────────────────────────────────────────
GEMINI_API_KEY=

View File

@@ -52,6 +52,12 @@ export class Company {
@Column({ type: 'varchar', length: 50, nullable: true })
jofotara_income_source_sequence?: string;
@Column({ type: 'text', nullable: true })
certificate_path?: string;
@Column({ type: 'text', nullable: true, select: false })
certificate_password_encrypted?: string;
@Column({ type: 'boolean', default: true })
is_active!: boolean;

View File

@@ -12,4 +12,9 @@ export class DashboardController {
async getStats(@CurrentUser() user: any) {
return this.dashboardService.getStats(user.tenantId);
}
@Get('multi-entity')
async getMultiEntityStats(@CurrentUser() user: any) {
return this.dashboardService.getMultiEntityStats(user.tenantId);
}
}

View File

@@ -76,4 +76,50 @@ export class DashboardService {
})),
};
}
/**
* جلب إحصائيات مجمعة لكل الشركات (Elite Accountant View)
* Get summarized stats for all companies under a tenant
*/
async getMultiEntityStats(tenantId: string) {
const companies = await this.companyRepository.find({
where: { tenant_id: tenantId },
});
const companyStats = await Promise.all(
companies.map(async (company) => {
const stats = await this.invoiceRepository
.createQueryBuilder('invoice')
.select('COUNT(*)', 'total')
.addSelect('SUM(CASE WHEN status = :approved THEN tax_amount ELSE 0 END)', 'totalTax')
.addSelect('COUNT(CASE WHEN status = :failed THEN 1 END)', 'failedCount')
.where('invoice.company_id = :companyId', {
companyId: company.id,
approved: InvoiceStatus.APPROVED,
failed: InvoiceStatus.VALIDATION_FAILED
})
.getRawOne();
return {
id: company.id,
name: company.name,
taxId: company.tax_identification_number,
totalInvoices: parseInt(stats.total) || 0,
totalTax: parseFloat(stats.totalTax) || 0,
failedCount: parseInt(stats.failedCount) || 0,
riskScore: this.calculateMockRiskScore(stats), // Placeholder for AI Risk Score
};
}),
);
return companyStats;
}
private calculateMockRiskScore(stats: any) {
// Logic to be replaced by AI Anomaly Detection later
const failedRatio = stats.total > 0 ? (stats.failedCount / stats.total) : 0;
if (failedRatio > 0.2) return 85; // High Risk
if (failedRatio > 0.05) return 45; // Medium Risk
return 12; // Low Risk
}
}

View File

@@ -0,0 +1,34 @@
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bull';
import { Logger } from '@nestjs/common';
import { InjectRedis } from '@liaoliaots/nestjs-redis';
import Redis from 'ioredis';
import { InvoicesService } from './invoice.service';
@Processor('invoice-bulk-queue')
export class BulkUploadProcessor {
private readonly logger = new Logger(BulkUploadProcessor.name);
constructor(
private readonly invoicesService: InvoicesService,
@InjectRedis() private readonly redis: Redis,
) {}
@Process('process-zip')
async handleBulkZip(job: Job<{ filePath: string, companyId: string, tenantId: string }>) {
const { filePath, companyId, tenantId } = job.data;
this.logger.log(`Processing bulk ZIP: ${filePath} for Company: ${companyId}`);
// TODO: Implement ZIP extraction (adm-zip or unzipper)
// TODO: For each file in ZIP:
// 1. Calculate Hash (MD5/SHA256)
// 2. Check Redis SMEMBERS to prevent duplicate processing
// const hash = 'calculated_file_hash';
// const exists = await this.redis.sismember(`company:${companyId}:invoice-hashes`, hash);
// if (exists) return;
// 3. Save hash and trigger individual processing
// await this.redis.sadd(`company:${companyId}:invoice-hashes`, hash);
// await this.invoicesService.processSingleFile(fileInZip, tenantId, companyId);
}
}

View File

@@ -113,6 +113,9 @@ export class Invoice {
@Column({ type: 'jsonb', nullable: true })
validation_errors?: string[];
@Column({ type: 'text', nullable: true })
qr_code?: string;
@Column({ type: 'decimal', precision: 4, scale: 3, nullable: true })
ai_confidence_score?: number;

View File

@@ -1,8 +1,10 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Gemini AI Extraction Service
* مُصادَق (Musadaq) — Gemini AI Extraction Service / خدمة استخراج البيانات
* ════════════════════════════════════════════════════════════
* يقوم باستخراج البيانات من صور/ملفات الفواتير باستخدام Gemini.
* This service extracts financial data from invoice images/PDFs using Gemini AI.
* It ensures unstructured data is converted into UBL 2.1 compliant JSON.
* يقوم باستخراج البيانات المالية من صور/ملفات الفواتير باستخدام ذكاء Gemini الاصطناعي.
* يضمن تحويل البيانات غير المهيكلة إلى JSON مطابق لمعايير UBL 2.1.
* ════════════════════════════════════════════════════════════
*/
@@ -29,6 +31,7 @@ export class GeminiExtractorService {
/**
* استخراج البيانات من صورة الفاتورة (يدعم فواتير متعددة في ملف واحد)
* Extract accounting data from an invoice file (supports multiple invoices per file)
*/
async extractInvoiceData(filePath: string, storageRoot: string): Promise<any> {
try {
@@ -36,9 +39,26 @@ export class GeminiExtractorService {
const fileData = fs.readFileSync(fullPath);
const prompt = `
You are a Jordanian tax expert. Extract all details from this file (image or PDF).
The file may contain ONE or MULTIPLE distinct invoices.
Extract EACH invoice separately.
أنت الآن "مدقق ضريبي أردني خبير" (Expert Jordanian Tax Auditor).
مهمتك هي استخراج البيانات المحاسبية من هذا الملف بدقة متناهية لضمان الامتثال لنظام الفوترة الوطني (JoFotara).
قواعد الاستخراج الاستراتيجية:
1. الشخصية: تعامل مع الملف كمدقق يبحث عن أدق التفاصيل المالية والقانونية.
2. العملة: جميع المبالغ بالدينار الأردني (JOD). التزم بدقة 3 خانات عشرية (مثال: 1.250).
3. الضرائب: حدد نسب الضريبة لكل بند بناءً على القوائم المعتمدة:
- (16%): النسبة العامة (أي سلع غير مذكورة أدناه).
- (10%): حلاوة طحينة، طحينة، أجبان محضرة، أسماك محفوظة، سجق.
- (5%): عبوات الألبان والعلب المعدنية والكرتون المطبوع المخصص للتغليف.
- (4%): كرتون أطباق البيض، القرطاسية، الزي المدرسي، مدافئ الكاز والغاز، البوتاس والفوسفات.
- (2%): الملفوف، الباميا، البازلاء (طازجة أو مبردة).
- (0%): اللحوم، الأسماك، لوازم شبكات الري، آلات الزراعة، نباتات الهندباء.
- (معفى - Exempt): العدس، الشاي، الحليب، السكر، الأرز، الهواتف الخلوية، الكهرباء.
4. الأطراف: استخرج الرقم الضريبي للمورد (Supplier TIN) والرقم الضريبي أو الوطني للمشتري (Buyer TIN/National ID).
5. التصنيف:
- "simplified": إذا كان المشتري فرداً (بدون رقم ضريبي).
- "standard": إذا كان المشتري شركة أو منشأة (يوجد رقم ضريبي).
6. الفواتير المتعددة: إذا احتوى الملف على أكثر من فاتورة منفصلة، استخرج بيانات كل واحدة في عنصر مستقل في مصفوفة "invoices".
7. التجاهل: تجاهل الأختام اليدوية التي تغطي النصوص، وحاول استنتاج النص تحتها برمجياً.
The output MUST be a strict JSON object with this schema:
{
@@ -47,15 +67,18 @@ export class GeminiExtractorService {
"invoice_number": "string",
"invoice_date": "YYYY-MM-DD",
"invoice_type": "cash" | "credit",
"ubl_type_code": "388" | "381",
"payment_method_code": "013" | "023",
"invoice_category": "standard" | "simplified",
"supplier_name": "string",
"supplier_tin": "string (10 digits)",
"buyer_name": "string (optional)",
"buyer_tin": "string (optional)",
"subtotal": number (before discount and tax),
"discount_total": number (total discount),
"tax_amount": number (total tax),
"grand_total": number (final amount),
"buyer_national_id": "string (10 digits, optional)",
"subtotal": number,
"discount_total": number,
"tax_amount": number,
"grand_total": number,
"currency_code": "JOD",
"lines": [
{
@@ -64,21 +87,14 @@ export class GeminiExtractorService {
"quantity": number,
"unit_price": number,
"discount": number,
"tax_rate": number (e.g. 0.16 for 16%),
"line_total": number (quantity * unit_price - discount)
"tax_rate": number (e.g. 0.16),
"line_total": number
}
]
}
]
}
JoFotara Specific Rules:
- invoice_category: "simplified" if the buyer is a regular person (no TIN), "standard" if B2B (buyer has TIN).
- Prices in Jordan (JOD) often have 3 decimal places (e.g. 2.800).
- Standard VAT Rate: 0.16 (16%)
- The formula MUST hold: Subtotal - Discount + Tax = Grand Total.
- If multiple invoices are in the PDF, ensure the "invoices" array contains all of them.
Return ONLY the JSON. No markdown formatting.
`;

View File

@@ -1,6 +1,9 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Invoices Controller
* مُصادَق (Musadaq) — Invoices Controller / متحكم الفواتير
* ════════════════════════════════════════════════════════════
* Handles HTTP requests related to invoice management.
* يتعامل مع طلبات HTTP المتعلقة بإدارة الفواتير.
* ════════════════════════════════════════════════════════════
*/
@@ -31,6 +34,7 @@ export class InvoicesController {
/**
* قائمة جميع الفواتير للمكتب
* List all invoices for the entire accounting office (tenant)
*/
@Get()
async findAllByTenant(@CurrentUser() user: any) {
@@ -39,6 +43,7 @@ export class InvoicesController {
/**
* رفع فاتورة لشركة محددة
* Upload an invoice file for a specific company
*/
@Post('upload/:companyId')
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
@@ -53,6 +58,7 @@ export class InvoicesController {
/**
* قائمة الفواتير لشركة محددة
* List all invoices for a specific company
*/
@Get('company/:companyId')
async findAll(
@@ -64,6 +70,7 @@ export class InvoicesController {
/**
* تفاصيل فاتورة محددة
* Get details of a specific invoice
*/
@Get(':id')
async findOne(
@@ -78,6 +85,7 @@ export class InvoicesController {
*/
/**
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية
* Submit an invoice to the official JoFotara portal
*/
@Post(':id/submit')
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
@@ -87,6 +95,7 @@ export class InvoicesController {
/**
* حذف الفاتورة نهائياً
* Permanently delete an invoice
*/
@Post(':id/delete') // Using POST for delete to match frontend request style or use standard DELETE
@Roles(UserRole.ADMIN, UserRole.ACCOUNTANT)
@@ -96,6 +105,7 @@ export class InvoicesController {
/**
* الحصول على الملف الأصلي للفاتورة
* Download/Stream the original invoice file
*/
@Get(':id/file')
async getFile(

View File

@@ -9,6 +9,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bull';
import { InvoicesService } from './invoice.service';
import { InvoicesController } from './invoice.controller';
import { PublicInvoiceController } from './public-invoice.controller';
import { Invoice } from './entities/invoice.entity';
import { InvoiceLine } from './entities/invoice-line.entity';
import { InvoiceProcessor } from './invoice.processor';
@@ -23,9 +24,10 @@ import { CompaniesModule } from '../companies/company.module';
@Module({
imports: [
TypeOrmModule.forFeature([Invoice, InvoiceLine]),
BullModule.registerQueue({
name: 'invoice-processing',
}),
BullModule.registerQueue(
{ name: 'invoice-processing' },
{ name: 'invoice-bulk-queue' }
),
forwardRef(() => SubscriptionsModule),
forwardRef(() => CompaniesModule),
TaxValidationModule,
@@ -38,7 +40,7 @@ import { CompaniesModule } from '../companies/company.module';
JoFotaraGatewayService,
LocalStorageService,
],
controllers: [InvoicesController],
controllers: [InvoicesController, PublicInvoiceController],
exports: [InvoicesService],
})
export class InvoicesModule {}

View File

@@ -1,9 +1,11 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Invoice Processor (Queue Consumer)
* مُصادَق (Musadaq) — Invoice Processor (Queue Consumer) / معالج الفواتير
* ════════════════════════════════════════════════════════════
* This is the main consumer for the invoice processing queue (Bull).
* It orchestrates AI extraction, tax validation, and database storage.
* المستهلك الرئيسي لطابور معالجة الفواتير (Bull Queue).
* يربط بين الذكاء الاصطناعي، التحقق الضريبي، وتوليد الـ XML.
* يقوم بالتنسيق بين استخراج البيانات بالذكاء الاصطناعي، التحقق الضريبي، وحفظ البيانات.
* ════════════════════════════════════════════════════════════
*/
@@ -50,6 +52,7 @@ export class InvoiceProcessor {
/**
* الخطوة الأولى: استخراج البيانات باستخدام AI
* Step 1: Extract data from the file using Gemini Multimodal AI
*/
@Process('extract-data')
async handleExtraction(job: Job<{ invoiceId: string; filePath: string; tenantId: string; companyId: string }>) {
@@ -97,6 +100,7 @@ export class InvoiceProcessor {
/**
* حفظ البيانات المستخرجة في قاعدة البيانات
* Save the JSON data extracted by AI into the SQL database
*/
private async saveExtractedData(invoiceId: string, data: any) {
const queryRunner = this.dataSource.createQueryRunner();
@@ -109,11 +113,14 @@ export class InvoiceProcessor {
invoice_number: data.invoice_number,
invoice_date: data.invoice_date,
invoice_type: data.invoice_type || 'cash',
ubl_type_code: data.ubl_type_code || '388',
payment_method_code: data.payment_method_code || '013',
invoice_category: data.invoice_category || 'simplified',
supplier_name: data.supplier_name,
supplier_tin: data.supplier_tin,
buyer_name: data.buyer_name,
buyer_tin: data.buyer_tin,
buyer_national_id: data.buyer_national_id,
subtotal: data.subtotal,
discount_total: data.discount_total,
tax_amount: data.tax_amount,
@@ -157,6 +164,7 @@ export class InvoiceProcessor {
/**
* الخطوة الثانية: التحقق الضريبي التلقائي
* Step 2: Perform automatic tax rules validation (ISTD Standards)
*/
private async autoValidate(invoiceId: string) {
const invoice = await this.invoiceRepository.findOne({

View File

@@ -1,6 +1,11 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Invoices Service
* مُصادَق (Musadaq) — Invoices Service / خدمة إدارة الفواتير
* ════════════════════════════════════════════════════════════
* This service handles the core logic for invoice management,
* including uploading, retrieval, and integration with JoFotara.
* تقوم هذه الخدمة بمعالجة العمليات الأساسية لإدارة الفواتير،
* بما في ذلك الرفع، الاسترجاع، والربط مع نظام جو فوترة.
* ════════════════════════════════════════════════════════════
*/
@@ -44,6 +49,7 @@ export class InvoicesService {
/**
* رفع فاتورة جديدة وبدء المعالجة
* Upload a new invoice and initiate extraction/validation
*/
async upload(
tenantId: string,
@@ -55,10 +61,12 @@ export class InvoicesService {
throw new ForbiddenException('Monthly invoice limit reached for your current plan');
}
const company = await this.companiesService.findOne(tenantId, companyId);
const fileName = `${Date.now()}-${file.originalname}`;
const filePath = await this.localStorageService.saveFile(
tenantId,
companyId,
company.name,
fileName,
file.buffer,
);
@@ -85,6 +93,7 @@ export class InvoicesService {
/**
* قائمة الفواتير لشركة محددة
* Find all invoices for a specific company
*/
async findAll(tenantId: string, companyId: string): Promise<Invoice[]> {
return this.invoiceRepository.find({
@@ -94,7 +103,8 @@ export class InvoicesService {
}
/**
* قائمة جميع الفواتير للمكتب
* قائمة جميع الفواتير للمكتب (المستأجر)
* Find all invoices for the entire tenant (accounting office)
*/
async findAllByTenant(tenantId: string): Promise<Invoice[]> {
return this.invoiceRepository.find({
@@ -105,7 +115,8 @@ export class InvoicesService {
}
/**
* تفاصيل فاتورة
* تفاصيل فاتورة محددة مع بنودها
* Get detailed information for a specific invoice including its lines
*/
async findOne(tenantId: string, id: string): Promise<Invoice> {
const invoice = await this.invoiceRepository.findOne({
@@ -118,6 +129,7 @@ export class InvoicesService {
/**
* تحديث بيانات الفاتورة يدوياً
* Manually update invoice data (only if not already submitted)
*/
async update(tenantId: string, id: string, updateData: any): Promise<Invoice> {
const invoice = await this.findOne(tenantId, id);
@@ -131,7 +143,8 @@ export class InvoicesService {
}
/**
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية
* إرسال الفاتورة إلى بوابة جو فوترة الحكومية (ISTD)
* Submit the validated invoice to the official JoFotara portal
*/
async submitToJoFotara(tenantId: string, id: string): Promise<any> {
const invoice = await this.findOne(tenantId, id);
@@ -154,7 +167,10 @@ export class InvoicesService {
credentials.secretKey,
);
await this.invoiceRepository.update(id, { status: InvoiceStatus.APPROVED });
await this.invoiceRepository.update(id, {
status: InvoiceStatus.APPROVED,
qr_code: response.qrCode || response.qr_code || null,
});
return response;
} catch (error) {
await this.invoiceRepository.update(id, { status: InvoiceStatus.VALIDATION_FAILED });
@@ -163,7 +179,8 @@ export class InvoicesService {
}
/**
* حذف الفاتورة والملف التابع لها
* حذف الفاتورة والملف التابع لها نهائياً
* Permanently delete an invoice and its associated file from storage
*/
async remove(tenantId: string, id: string): Promise<void> {
const invoice = await this.findOne(tenantId, id);
@@ -178,7 +195,8 @@ export class InvoicesService {
}
/**
* الحصول على الملف كـ Stream
* الحصول على الملف كـ Stream لتحميله أو عرضه
* Get the original invoice file as a streamable file for download/view
*/
async getFile(tenantId: string, id: string): Promise<StreamableFile> {
const invoice = await this.findOne(tenantId, id);

View File

@@ -1,7 +1,9 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — JoFotara Gateway Service
* مُصادَق (Musadaq) — JoFotara Gateway Service / بوابة جو فوترة
* ════════════════════════════════════════════════════════════
* This service handles communication with the Jordanian National
* Electronic Invoicing System (JoFotara) portal.
* يقوم بالتواصل مع بوابة دائرة ضريبة الدخل والمبيعات الأردنية (ISTD).
* يدير عملية تسجيل الفواتير والحصول على الـ Clearance أو الـ Reporting.
* ════════════════════════════════════════════════════════════
@@ -26,6 +28,7 @@ export class JoFotaraGatewayService {
/**
* إرسال الفاتورة إلى بوابة جو فوترة
* Submit an invoice (Base64 XML) to the JoFotara portal
*/
async submitInvoice(
xmlContent: string,
@@ -33,16 +36,19 @@ export class JoFotaraGatewayService {
secretKey: string,
): Promise<any> {
const url = this.currentEnv === 'production' ? this.prodUrl : this.sandboxUrl;
const maxRetries = 3;
let attempt = 0;
while (attempt < maxRetries) {
try {
const payload = {
invoice: Buffer.from(xmlContent).toString('base64'),
};
this.logger.debug(`Submitting to JoFotara: ${url}/submit`);
this.logger.debug(`Submitting to JoFotara: ${url} (Attempt ${attempt + 1})`);
const response = await axios.post(
`${url}/submit`,
url,
payload,
{
headers: {
@@ -50,21 +56,34 @@ export class JoFotaraGatewayService {
'X-Client-Id': clientId,
'X-Secret-Key': secretKey,
},
timeout: 10000, // 10s timeout
},
);
this.logger.log(`JoFotara Response: ${JSON.stringify(response.data)}`);
return response.data;
} catch (error: any) {
attempt++;
const isTransient = !error.response || (error.response.status >= 500);
if (isTransient && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000;
this.logger.warn(`Transient error from JoFotara. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
this.logger.error(`JoFotara API Error: ${error.response?.data || error.message}`);
throw new InternalServerErrorException(
`Failed to submit invoice to JoFotara: ${error.response?.data?.message || 'Unknown Error'}`,
`Failed to submit invoice to JoFotara: ${error.response?.data?.message || error.message}`,
);
}
}
}
/**
* التحقق من حالة الفاتورة
* التحقق من حالة الفاتورة باستخدام الـ UUID
* Check the status of a submitted invoice using its UUID
*/
async checkStatus(uuid: string, clientId: string, secretKey: string): Promise<any> {
const url = this.currentEnv === 'production' ? this.prodUrl : this.sandboxUrl;

View File

@@ -0,0 +1,64 @@
import { Controller, Post, UseInterceptors, UploadedFile, BadRequestException } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { GeminiExtractorService } from './gemini-extractor.service';
import { UBLGeneratorService } from './ubl-generator.service';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@Controller('public/tools')
export class PublicInvoiceController {
constructor(
private geminiExtractor: GeminiExtractorService,
private ublGenerator: UBLGeneratorService,
) {}
/**
* حصان طروادة (Trojan Horse) - تحويل مجاني بدون تسجيل
* Converts a PDF/Image invoice directly to JoFotara XML
*/
@Post('pdf-to-xml')
@UseInterceptors(FileInterceptor('file'))
async convertPdfToXml(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('الرجاء إرفاق ملف الفاتورة');
}
// Save temporary file
const tempDir = os.tmpdir();
const tempPath = path.join(tempDir, `trojan_${Date.now()}_${file.originalname}`);
fs.writeFileSync(tempPath, file.buffer);
try {
// 1. Extract data using AI (treating as standard company for now)
// Since it's public, we use a generic path
const extractedData = await this.geminiExtractor.extractInvoiceData(tempPath, '');
if (!extractedData || extractedData.length === 0) {
throw new BadRequestException('لم يتم العثور على بيانات صالحة في الفاتورة');
}
const invoiceData = extractedData[0]; // Take the first invoice if multiple
// 2. Generate XML (using generic company info for demo purposes)
const mockCompanyInfo = {
name: invoiceData.supplier_name || 'شركة تجريبية',
tax_identification_number: invoiceData.supplier_tin || '0000000000',
};
const xml = this.ublGenerator.generateXML(invoiceData, mockCompanyInfo);
return {
success: true,
message: 'تم التحويل بنجاح! لطباعة أو إرسال آلاف الفواتير، جرب محاسبي إيليت.',
data: invoiceData,
xmlContent: xml,
};
} finally {
// Cleanup temp file
if (fs.existsSync(tempPath)) {
fs.unlinkSync(tempPath);
}
}
}
}

View File

@@ -1,7 +1,9 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — UBL 2.1 Generator Service
* مُصادَق (Musadaq) — UBL 2.1 Generator Service / خدمة توليد ملفات UBL
* ════════════════════════════════════════════════════════════
* This service generates XML files compliant with the UBL 2.1 standard
* required by the Jordanian Income and Sales Tax Department (ISTD).
* يقوم بإنشاء ملفات XML المتوافقة مع معيار UBL 2.1 المطلوبة من
* دائرة ضريبة الدخل والمبيعات الأردنية (ISTD).
* ════════════════════════════════════════════════════════════
@@ -15,8 +17,14 @@ import { Invoice } from './entities/invoice.entity';
export class UBLGeneratorService {
/**
* توليد UBL 2.1 XML لفاتورة مبيعات
* Generate a UBL 2.1 compliant XML for a sales invoice
*/
generateXML(invoice: Invoice, company: any): string {
generateXML(invoice: any, company: any): string {
const currency = invoice.currency_code || 'JOD';
const invoiceDate = invoice.invoice_date instanceof Date
? invoice.invoice_date.toISOString().split('T')[0]
: (invoice.invoice_date || new Date().toISOString().split('T')[0]);
const doc = create({ version: '1.0', encoding: 'UTF-8' })
.ele('Invoice', {
xmlns: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
@@ -24,11 +32,12 @@ export class UBLGeneratorService {
'xmlns:cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2',
})
.ele('cbc:UBLVersionID').txt('2.1').up()
.ele('cbc:CustomizationID').txt('TRX-1.0').up()
.ele('cbc:ProfileID').txt('reporting:1.0').up()
.ele('cbc:CustomizationID').txt('urn:www.cenbii.eu:transaction:biitrns010:ver2.0').up()
.ele('cbc:ID').txt(invoice.invoice_number || 'N/A').up()
.ele('cbc:IssueDate').txt(invoice.invoice_date?.toISOString().split('T')[0] || '').up()
.ele('cbc:InvoiceTypeCode').txt(invoice.ubl_type_code).up()
.ele('cbc:DocumentCurrencyCode').txt(invoice.currency_code).up()
.ele('cbc:IssueDate').txt(invoiceDate).up()
.ele('cbc:InvoiceTypeCode').txt(invoice.ubl_type_code || '388').up()
.ele('cbc:DocumentCurrencyCode').txt(currency).up()
// ── AccountingSupplierParty (المُصدر) ───────────────
.ele('cac:AccountingSupplierParty')
@@ -62,15 +71,15 @@ export class UBLGeneratorService {
// ── TaxTotal ───────────────────────────────────────
.ele('cac:TaxTotal')
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt(invoice.tax_amount.toString()).up()
.ele('cbc:TaxAmount', { currencyID: currency }).txt(invoice.tax_amount.toString()).up()
.up()
// ── LegalMonetaryTotal ─────────────────────────────
.ele('cac:LegalMonetaryTotal')
.ele('cbc:LineExtensionAmount', { currencyID: invoice.currency_code }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxExclusiveAmount', { currencyID: invoice.currency_code }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxInclusiveAmount', { currencyID: invoice.currency_code }).txt(invoice.grand_total.toString()).up()
.ele('cbc:PayableAmount', { currencyID: invoice.currency_code }).txt(invoice.grand_total.toString()).up()
.ele('cbc:LineExtensionAmount', { currencyID: currency }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxExclusiveAmount', { currencyID: currency }).txt(invoice.subtotal.toString()).up()
.ele('cbc:TaxInclusiveAmount', { currencyID: currency }).txt(invoice.grand_total.toString()).up()
.ele('cbc:PayableAmount', { currencyID: currency }).txt(invoice.grand_total.toString()).up()
.up();
// ── InvoiceLines ─────────────────────────────────────
@@ -78,18 +87,18 @@ export class UBLGeneratorService {
doc.ele('cac:InvoiceLine')
.ele('cbc:ID').txt(line.line_number.toString()).up()
.ele('cbc:InvoicedQuantity', { unitCode: 'PCE' }).txt(line.quantity.toString()).up()
.ele('cbc:LineExtensionAmount', { currencyID: invoice.currency_code }).txt(line.line_total.toString()).up()
.ele('cbc:LineExtensionAmount', { currencyID: currency }).txt(line.line_total.toString()).up()
.ele('cac:Item')
.ele('cbc:Description').txt(line.description).up()
.up()
.ele('cac:Price')
.ele('cbc:PriceAmount', { currencyID: invoice.currency_code }).txt(line.unit_price.toString()).up()
.ele('cbc:PriceAmount', { currencyID: currency }).txt(line.unit_price.toString()).up()
.up()
.ele('cac:TaxTotal')
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
.ele('cbc:TaxAmount', { currencyID: currency }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
.ele('cac:TaxSubtotal')
.ele('cbc:TaxableAmount', { currencyID: invoice.currency_code }).txt(line.line_total.toString()).up()
.ele('cbc:TaxAmount', { currencyID: invoice.currency_code }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
.ele('cbc:TaxableAmount', { currencyID: currency }).txt(line.line_total.toString()).up()
.ele('cbc:TaxAmount', { currencyID: currency }).txt((line.line_total * line.tax_rate).toFixed(3)).up()
.ele('cac:TaxCategory')
.ele('cbc:ID').txt(line.tax_rate > 0 ? 'S' : 'Z').up()
.ele('cbc:Percent').txt((line.tax_rate * 100).toString()).up()

View File

@@ -0,0 +1,42 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GoogleGenerativeAI } from '@google/generative-ai';
@Injectable()
export class TaxAdvisorService {
private readonly logger = new Logger(TaxAdvisorService.name);
private genAI: GoogleGenerativeAI;
private model: any;
constructor(private configService: ConfigService) {
const apiKey = this.configService.getOrThrow<string>('GEMINI_API_KEY');
this.genAI = new GoogleGenerativeAI(apiKey);
this.model = this.genAI.getGenerativeModel({
model: this.configService.get<string>('GEMINI_MODEL', 'gemini-1.5-flash'),
});
}
/**
* الإجابة على استفسارات القوانين الضريبية (RAG Placeholder)
* Answer tax law queries using a specialized system prompt
*/
async askQuestion(question: string): Promise<string> {
const systemPrompt = `
أنت "مستشار ضريبي أردني ذكي". مرجعيتك هي قانون ضريبة الدخل والمبيعات الأردني وتعديلات 2025.
قواعد أساسية للإجابة:
1. اعتمد على جداول السلع (16%, 10%, 4%, 2%, 0%, معفى).
2. وضح دائماً الفرق بين السلع الصفرية (تسمح بخصم المدخلات) والمعفاة (لا تسمح).
3. التزم بمتطلبات "جو فوترة" بخصوص فواتير الذمم والـ 10,000 دينار.
4. كن دقيقاً، مهنياً، ومختصراً.
`;
try {
const result = await this.model.generateContent([systemPrompt, question]);
return result.response.text();
} catch (error) {
this.logger.error('Tax Advisor query failed', error);
return 'عذراً، تعذر الحصول على إجابة حالياً. يرجى مراجعة القوانين الرسمية.';
}
}
}

View File

@@ -15,6 +15,7 @@ import {
Index,
} from 'typeorm';
import { Tenant } from '../../tenants/entities/tenant.entity';
import { Company } from '../../companies/entities/company.entity';
import { UserRole } from '../enums/role.enum';
@Entity('users')
@@ -45,6 +46,13 @@ export class User {
})
role!: UserRole;
@Column({ name: 'company_id', type: 'uuid', nullable: true })
company_id?: string;
@ManyToOne(() => Company, { nullable: true, onDelete: 'SET NULL' })
@JoinColumn({ name: 'company_id' })
company?: Company;
@Column({ type: 'varchar', length: 255, nullable: true, select: false })
refresh_token_hash?: string;

View File

@@ -6,6 +6,7 @@
export enum UserRole {
ADMIN = 'admin', // مدير المكتب (قادر على الإضافة والتعديل الكامل)
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها)
ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات)
CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط)
VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط)
}

View File

@@ -0,0 +1,60 @@
import { Injectable, Logger } from '@nestjs/common';
import { Invoice } from '../invoices/entities/invoice.entity';
import { TaxValidationService, ValidationResult } from './tax-validation.service';
@Injectable()
export class JofotaraComplianceService {
private readonly logger = new Logger(JofotaraComplianceService.name);
constructor(private taxValidation: TaxValidationService) {}
/**
* الفحص الشامل للامتثال لمتطلبات جو فوترة
* Comprehensive JOFOTARA compliance check
*/
async checkCompliance(invoice: Invoice, credentials?: any): Promise<ValidationResult> {
const errors: string[] = [];
// 1. Schema Validation (التحقق من اكتمال الحقول الإلزامية)
this.validateSchema(invoice, errors);
// 2. Tax Rules Validation (التحقق من القواعد الحسابية)
const taxResult = this.taxValidation.validateInvoice(invoice);
if (!taxResult.isValid) {
errors.push(...taxResult.errors);
}
// 3. Credential Check (التحقق من وجود الربط مع الضريبة)
if (credentials && (!credentials.clientId || !credentials.secretKey)) {
errors.push('بيانات الربط مع نظام جو فوترة (Credentials) غير مكتملة لهذه الشركة');
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* التحقق من الحقول الإلزامية في صيغة الفاتورة
* Rule 008: Mandatory JOFOTARA Fields
*/
private validateSchema(invoice: Invoice, errors: string[]) {
if (!invoice.invoice_number) errors.push('رقم الفاتورة (Invoice Number) مطلوب');
if (!invoice.invoice_date) errors.push('تاريخ الفاتورة (Invoice Date) مطلوب');
if (!invoice.supplier_tin) errors.push('الرقم الضريبي للمورد (Supplier TIN) مطلوب');
if (invoice.supplier_tin && invoice.supplier_tin.length !== 10) {
errors.push('الرقم الضريبي للمورد يجب أن يتكون من 10 خانات');
}
// Check if lines exist
if (!invoice.lines || invoice.lines.length === 0) {
errors.push('يجب أن تحتوي الفاتورة على بند واحد على الأقل');
}
// Additional JoFotara specific checks
if (!['388', '381'].includes(invoice.ubl_type_code)) {
errors.push('نوع الفاتورة (UBL Type Code) غير مدعوم');
}
}
}

View File

@@ -6,9 +6,10 @@
import { Module } from '@nestjs/common';
import { TaxValidationService } from './tax-validation.service';
import { JofotaraComplianceService } from './jofotara-compliance.service';
@Module({
providers: [TaxValidationService],
exports: [TaxValidationService],
providers: [TaxValidationService, JofotaraComplianceService],
exports: [TaxValidationService, JofotaraComplianceService],
})
export class TaxValidationModule {}

View File

@@ -1,9 +1,11 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Tax Validation Service
* مُصادَق (Musadaq) — Tax Validation Service / محرك التدقيق الضريبي
* ════════════════════════════════════════════════════════════
* Jordanian Tax Rules Validation Engine (ISTD Rules).
* Ensures calculation accuracy before submission to JoFotara.
* محرك التحقق من القواعد الضريبية الأردنية (ISTD Rules).
* يضمن دقة الحسابات قبل إرسالها إلى "جو فوترة".
* يضمن دقة الحسابات والامتثال القانوني قبل إرسال البيانات إلى "جو فوترة".
* ════════════════════════════════════════════════════════════
*/
@@ -21,7 +23,8 @@ export class TaxValidationService {
private readonly PRECISION = 0.005; // السماحية في الفروقات العشرية البسيطة
/**
* التحقق الشامل من الفاتورة
* التحقق الشامل من الفاتورة وفق القواعد المبرمجة
* Comprehensive invoice validation based on predefined tax rules
*/
validateInvoice(invoice: Invoice): ValidationResult {
const errors: string[] = [];
@@ -41,6 +44,12 @@ export class TaxValidationService {
// 5. Rule 005: التحقق من صحة نسب الضريبة الأردنية
this.checkRule005(invoice, errors);
// 6. Rule 006: التحقق من وجود هوية المشتري (للذمم أو المبيعات > 10,000)
this.checkRule006(invoice, errors);
// 7. Rule 007: التحقق من مطابقة مجموع البنود للمجموع النهائي
this.checkRule007(invoice, errors);
return {
isValid: errors.length === 0,
errors,
@@ -51,7 +60,8 @@ export class TaxValidationService {
* Rule 005: Valid Jordanian Tax Rates (0.16, 0.04, 0.00, etc.)
*/
private checkRule005(invoice: Invoice, errors: string[]) {
const validRates = [0.16, 0.04, 0.0, 0.01]; // 1% is also used in some special cases
// المراجع القانونية: 16% (عامة)، 10%، 5%، 4%، 2% (مخفضة)، 0% (صفرية)
const validRates = [0.16, 0.10, 0.05, 0.04, 0.02, 0.00, 0.01];
invoice.lines.forEach((line) => {
const rate = Number(line.tax_rate);
if (!validRates.includes(rate)) {
@@ -119,4 +129,39 @@ export class TaxValidationService {
errors.push(`خطأ في القاعدة 004: المجموع النهائي المحسوب (${calculatedGrandTotal.toFixed(3)}) لا يطابق المسجل (${Number(invoice.grand_total).toFixed(3)})`);
}
}
/**
* Rule 006: Buyer Identity Requirement
* - Cash Invoices >= 10,000 JOD
* - Credit/Receivable Invoices (All amounts)
*/
private checkRule006(invoice: Invoice, errors: string[]) {
const isHighValue = Number(invoice.grand_total) >= 10000;
const isCredit = invoice.payment_method_code === '023'; // 023 is usually credit in JoFotara
if (isHighValue || isCredit) {
if (!invoice.buyer_tin && !invoice.buyer_national_id) {
const reason = isCredit ? 'فاتورة ذمم (Credit)' : 'فاتورة نقدية تتجاوز 10,000 دينار';
errors.push(`خطأ في القاعدة 006: يجب تزويد الرقم الضريبي أو الوطني للمشتري لأنها ${reason}`);
}
}
}
/**
* Rule 007: Line Totals Integrity
* Ensure Σ (LineTotal) matches Header (Subtotal - DiscountTotal)
*/
private checkRule007(invoice: Invoice, errors: string[]) {
const totalLinesBeforeTax = invoice.lines.reduce(
(sum, line) => sum + Number(line.line_total),
0,
);
const headerBeforeTax = Number(invoice.subtotal) - Number(invoice.discount_total);
const diff = Math.abs(totalLinesBeforeTax - headerBeforeTax);
if (diff > this.PRECISION) {
errors.push(`خطأ في القاعدة 007: مجموع قيم البنود قبل الضريبة (${totalLinesBeforeTax.toFixed(3)}) لا يطابق الإجمالي قبل الضريبة في ترويسة الفاتورة (${headerBeforeTax.toFixed(3)})`);
}
}
}

View File

@@ -1,6 +1,11 @@
/**
* ════════════════════════════════════════════════════════════
* مُصادَق (Musadaq) — Local Storage Service
* مُصادَق (Musadaq) — Local Storage Service / خدمة التخزين المحلي
* ════════════════════════════════════════════════════════════
* This service manages saving and deleting files on the local filesystem.
* Files are organized by tenant, company, and date.
* تدير هذه الخدمة عمليات حفظ وحذف الملفات على نظام الملفات المحلي.
* يتم تنظيم الملفات حسب المكتب، الشركة، والتاريخ.
* ════════════════════════════════════════════════════════════
*/
@@ -23,19 +28,24 @@ export class LocalStorageService {
}
/**
* حفظ ملف في التخزين المحلي
* حفظ ملف في التخزين المحلي بتنظيم هيكلي
* Save a file to local storage with a structured directory path
*/
async saveFile(
tenantId: string,
companyId: string,
companyName: string,
fileName: string,
buffer: Buffer,
): Promise<string> {
try {
const now = new Date();
// Safe company name (remove special characters)
const safeCompanyName = companyName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const relativePath = path.join(
tenantId,
companyId,
`${safeCompanyName}_${companyId}`,
'invoices',
now.getFullYear().toString(),
(now.getMonth() + 1).toString(),
@@ -59,7 +69,8 @@ export class LocalStorageService {
}
/**
* حذف ملف
* حذف ملف من التخزين باستخدام مساره النسبي
* Delete a file from storage using its relative path
*/
async deleteFile(filePath: string): Promise<void> {
try {

View File

@@ -0,0 +1,155 @@
import React, { useState } from 'react';
import { UploadCloud, FileType, CheckCircle2, ArrowRight } from 'lucide-react';
import { motion } from 'framer-motion';
export const TrojanHorseConverter = () => {
const [dragActive, setDragActive] = useState(false);
const [file, setFile] = useState<File | null>(null);
const [status, setStatus] = useState<'idle' | 'uploading' | 'success'>('idle');
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
setFile(e.dataTransfer.files[0]);
}
};
const handleConvert = () => {
if (!file) return;
setStatus('uploading');
// Simulate API call to the backend Trojan endpoint
setTimeout(() => {
setStatus('success');
}, 2000);
};
return (
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans selection:bg-emerald-200">
{/* Top Banner */}
<div className="bg-slate-900 text-center py-3 text-sm text-slate-300">
هل تدير عشرات الشركات؟ <span className="text-white font-medium ml-2 cursor-pointer hover:text-emerald-400 transition-colors">اكتشف لوحة تحكم محاسبي إيليت <ArrowRight className="inline w-4 h-4" /></span>
</div>
<div className="max-w-4xl mx-auto px-6 py-20">
{/* Header */}
<div className="text-center mb-16">
<div className="inline-block px-4 py-1.5 rounded-full bg-emerald-100 text-emerald-800 text-xs font-bold mb-6">
أداة مجانية 100%
</div>
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight mb-6 text-slate-900">
حوّل فواتيرك إلى صيغة <span className="text-emerald-600">جو فوترة</span> في ثوانٍ
</h1>
<p className="text-lg text-slate-600 max-w-2xl mx-auto">
قم برفع فاتورة الـ PDF الخاصة بك وسيقوم الذكاء الاصطناعي باستخراج البيانات وتحويلها إلى صيغة XML المتوافقة تماماً مع نظام دائرة ضريبة الدخل والمبيعات الأردنية.
</p>
</div>
{/* Upload Area */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-3xl shadow-xl p-8 border border-slate-100"
>
{status === 'idle' && (
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`border-2 border-dashed rounded-2xl p-12 text-center transition-all ${
dragActive ? 'border-emerald-500 bg-emerald-50/50' : 'border-slate-200 hover:border-slate-300'
}`}
>
<UploadCloud className={`w-16 h-16 mx-auto mb-6 ${dragActive ? 'text-emerald-500' : 'text-slate-400'}`} />
{file ? (
<div>
<p className="text-lg font-medium text-slate-900 mb-2">{file.name}</p>
<p className="text-sm text-slate-500 mb-8">{(file.size / 1024 / 1024).toFixed(2)} MB</p>
<button
onClick={handleConvert}
className="px-8 py-3 bg-slate-900 hover:bg-slate-800 text-white rounded-xl font-medium shadow-lg transition-all w-full md:w-auto"
>
بدء التحويل إلى XML
</button>
</div>
) : (
<div>
<p className="text-xl font-medium text-slate-900 mb-2">اسحب وأفلت الفاتورة هنا</p>
<p className="text-sm text-slate-500 mb-8">يدعم PDF, PNG, JPG</p>
<label className="px-8 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-medium shadow-lg shadow-emerald-500/30 transition-all cursor-pointer inline-block">
اختر ملف
<input type="file" className="hidden" onChange={(e) => e.target.files && setFile(e.target.files[0])} accept=".pdf,.png,.jpg" />
</label>
</div>
)}
</div>
)}
{status === 'uploading' && (
<div className="py-20 text-center">
<div className="w-16 h-16 border-4 border-slate-200 border-t-emerald-500 rounded-full animate-spin mx-auto mb-6"></div>
<h3 className="text-xl font-medium text-slate-900 mb-2">جاري استخراج البيانات...</h3>
<p className="text-slate-500">يقوم الذكاء الاصطناعي بقراءة الفاتورة وتنسيقها</p>
</div>
)}
{status === 'success' && (
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="py-12 text-center"
>
<div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle2 className="w-10 h-10 text-emerald-600" />
</div>
<h3 className="text-2xl font-bold text-slate-900 mb-4">تم التحويل بنجاح!</h3>
<div className="flex flex-col sm:flex-row gap-4 justify-center mt-8">
<button className="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 text-white rounded-xl font-medium flex items-center justify-center gap-2">
<FileType className="w-5 h-5" /> تحميل ملف XML
</button>
<button
onClick={() => { setStatus('idle'); setFile(null); }}
className="px-6 py-3 bg-white border border-slate-200 hover:bg-slate-50 text-slate-700 rounded-xl font-medium"
>
تحويل فاتورة أخرى
</button>
</div>
{/* Upsell Banner for Accountants */}
<div className="mt-12 bg-slate-900 rounded-2xl p-6 text-left flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<h4 className="text-white font-bold mb-1 flex items-center gap-2">
<span className="text-emerald-400">مُصادَق</span> | هل أنت محاسب؟
</h4>
<p className="text-slate-400 text-sm">توقف عن تحويل الفواتير واحدة تلو الأخرى. جرب لوحة تحكم إيليت وأتمت عملك لـ 50 شركة بنقرة واحدة.</p>
</div>
<button className="whitespace-nowrap px-6 py-2.5 bg-white text-slate-900 rounded-lg font-medium hover:bg-slate-100">
اكتشف إيليت
</button>
</div>
</motion.div>
)}
</motion.div>
</div>
</div>
);
};

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Building2, TrendingUp, AlertTriangle, FileText, ChevronDown } from 'lucide-react';
import { motion } from 'framer-motion';
// Mock data matching the API structure we built in the backend
const mockCompanies = [
{ id: '1', name: 'Apex Innovations', totalInvoices: 24, pendingAmount: 78450, totalTax: 12500, failedCount: 2, riskScore: 85, status: 'High' },
{ id: '2', name: 'Quantum Solutions', totalInvoices: 26, pendingAmount: 112000, totalTax: 18000, failedCount: 0, riskScore: 32, status: 'Low' },
{ id: '3', name: 'Nomad Ventures', totalInvoices: 45, pendingAmount: 319000, totalTax: 45000, failedCount: 5, riskScore: 68, status: 'Medium' },
{ id: '4', name: 'Elevate Tech', totalInvoices: 12, pendingAmount: 188000, totalTax: 30000, failedCount: 1, riskScore: 85, status: 'High' },
{ id: '5', name: 'Horizon Group', totalInvoices: 33, pendingAmount: 95000, totalTax: 14000, failedCount: 3, riskScore: 68, status: 'Medium' },
];
const RiskGauge = ({ score, status }: { score: number, status: string }) => {
const getColor = () => {
if (status === 'High') return 'text-red-500';
if (status === 'Medium') return 'text-orange-500';
return 'text-emerald-500';
};
return (
<div className="flex flex-col items-center">
<div className="relative w-16 h-16">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 36 36">
<path
className="text-slate-700"
strokeWidth="3"
stroke="currentColor"
fill="none"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
/>
<path
className={getColor()}
strokeWidth="3"
strokeDasharray={`${score}, 100`}
stroke="currentColor"
fill="none"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center text-lg font-bold text-white">
{score}
</div>
</div>
<span className={`text-xs mt-1 ${getColor()}`}>{status}</span>
</div>
);
};
export const MultiEntityDashboard = () => {
return (
<div className="min-h-screen bg-slate-950 text-slate-300 font-sans p-8 selection:bg-emerald-500/30">
{/* Header */}
<header className="flex justify-between items-center mb-10">
<div>
<h1 className="text-3xl font-light tracking-tight text-white flex items-center gap-3">
<span className="font-semibold text-emerald-400">مُصادَق</span> | لوحة تحكم الشركات
</h1>
<p className="text-slate-400 mt-2 text-sm">نظرة عامة على الموقف الضريبي لجميع عملائك (Elite View)</p>
</div>
<div className="flex gap-4">
<button className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-lg text-sm border border-slate-700 transition-colors flex items-center gap-2">
آخر 30 يوم <ChevronDown className="w-4 h-4" />
</button>
<button className="px-5 py-2 bg-emerald-500 hover:bg-emerald-600 text-slate-950 font-medium rounded-lg text-sm shadow-[0_0_15px_rgba(16,185,129,0.3)] transition-all">
+ إضافة شركة جديدة
</button>
</div>
</header>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{mockCompanies.map((company, index) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
key={company.id}
className="bg-slate-900/50 backdrop-blur-xl border border-slate-800 rounded-2xl p-6 relative overflow-hidden hover:border-slate-700 transition-colors group"
>
{/* Ambient glow */}
<div className={`absolute -inset-20 opacity-0 group-hover:opacity-20 blur-3xl transition-opacity duration-500 rounded-full
${company.status === 'High' ? 'bg-red-500' : company.status === 'Medium' ? 'bg-orange-500' : 'bg-emerald-500'}
`} />
<div className="relative z-10">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-800 flex items-center justify-center border border-slate-700">
<Building2 className="w-5 h-5 text-emerald-400" />
</div>
<div>
<h3 className="text-white font-medium text-lg">{company.name}</h3>
<p className="text-xs text-slate-500">الرقم الضريبي: 00{company.id}829471</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<p className="text-xs text-slate-400 mb-1">التدفق النقدي (المبيعات)</p>
<p className="text-2xl font-light text-white">${(company.pendingAmount / 1000).toFixed(1)}k</p>
<div className="flex items-center gap-1 mt-1 text-xs text-emerald-400">
<TrendingUp className="w-3 h-3" /> مستقر
</div>
</div>
<div className="flex justify-end">
<div>
<p className="text-xs text-slate-400 mb-1 text-center">درجة الخطر الضريبي</p>
<RiskGauge score={company.riskScore} status={company.status} />
</div>
</div>
</div>
<div className="pt-4 border-t border-slate-800/50">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-slate-300">الفواتير المعلقة</p>
<span className="text-xs text-slate-500">{company.totalInvoices} فاتورة | ${company.totalTax} ضرائب</span>
</div>
{/* Minimal Progress Bar */}
<div className="h-1.5 w-full bg-slate-800 rounded-full overflow-hidden flex">
<div className="h-full bg-emerald-500" style={{ width: '60%' }}></div>
<div className="h-full bg-orange-500" style={{ width: '25%' }}></div>
<div className="h-full bg-red-500" style={{ width: '15%' }}></div>
</div>
<div className="flex justify-between mt-2 text-[10px] text-slate-500">
<span>تم الرفع (60%)</span>
{company.failedCount > 0 && (
<span className="text-red-400 flex items-center gap-1">
<AlertTriangle className="w-3 h-3" /> {company.failedCount} مرفوضة
</span>
)}
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
);
};