diff --git a/backend/.env.example b/backend/.env.example index 464db05..28f756a 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -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= diff --git a/backend/src/modules/companies/entities/company.entity.ts b/backend/src/modules/companies/entities/company.entity.ts index 1d9d1eb..a0f30c2 100644 --- a/backend/src/modules/companies/entities/company.entity.ts +++ b/backend/src/modules/companies/entities/company.entity.ts @@ -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; diff --git a/backend/src/modules/dashboard/dashboard.controller.ts b/backend/src/modules/dashboard/dashboard.controller.ts index ede0d5f..7d9c214 100644 --- a/backend/src/modules/dashboard/dashboard.controller.ts +++ b/backend/src/modules/dashboard/dashboard.controller.ts @@ -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); + } } diff --git a/backend/src/modules/dashboard/dashboard.service.ts b/backend/src/modules/dashboard/dashboard.service.ts index 035cb5e..f936a5d 100644 --- a/backend/src/modules/dashboard/dashboard.service.ts +++ b/backend/src/modules/dashboard/dashboard.service.ts @@ -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 + } } diff --git a/backend/src/modules/invoices/bulk-upload.processor.ts b/backend/src/modules/invoices/bulk-upload.processor.ts new file mode 100644 index 0000000..0d7e021 --- /dev/null +++ b/backend/src/modules/invoices/bulk-upload.processor.ts @@ -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); + } +} diff --git a/backend/src/modules/invoices/entities/invoice.entity.ts b/backend/src/modules/invoices/entities/invoice.entity.ts index d456ce2..9a4b536 100644 --- a/backend/src/modules/invoices/entities/invoice.entity.ts +++ b/backend/src/modules/invoices/entities/invoice.entity.ts @@ -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; diff --git a/backend/src/modules/invoices/gemini-extractor.service.ts b/backend/src/modules/invoices/gemini-extractor.service.ts index e2abbf8..ee4fa80 100644 --- a/backend/src/modules/invoices/gemini-extractor.service.ts +++ b/backend/src/modules/invoices/gemini-extractor.service.ts @@ -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 { try { @@ -36,10 +39,27 @@ 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: { "invoices": [ @@ -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. `; diff --git a/backend/src/modules/invoices/invoice.controller.ts b/backend/src/modules/invoices/invoice.controller.ts index 0bb8af2..06fcb1e 100644 --- a/backend/src/modules/invoices/invoice.controller.ts +++ b/backend/src/modules/invoices/invoice.controller.ts @@ -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( diff --git a/backend/src/modules/invoices/invoice.module.ts b/backend/src/modules/invoices/invoice.module.ts index 5e06596..e7c15f7 100644 --- a/backend/src/modules/invoices/invoice.module.ts +++ b/backend/src/modules/invoices/invoice.module.ts @@ -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 {} diff --git a/backend/src/modules/invoices/invoice.processor.ts b/backend/src/modules/invoices/invoice.processor.ts index 29e9111..8baf896 100644 --- a/backend/src/modules/invoices/invoice.processor.ts +++ b/backend/src/modules/invoices/invoice.processor.ts @@ -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({ diff --git a/backend/src/modules/invoices/invoice.service.ts b/backend/src/modules/invoices/invoice.service.ts index 0d2ee7f..5c280c3 100644 --- a/backend/src/modules/invoices/invoice.service.ts +++ b/backend/src/modules/invoices/invoice.service.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { const invoice = await this.findOne(tenantId, id); diff --git a/backend/src/modules/invoices/jofotara-gateway.service.ts b/backend/src/modules/invoices/jofotara-gateway.service.ts index 5120d84..b7d8159 100644 --- a/backend/src/modules/invoices/jofotara-gateway.service.ts +++ b/backend/src/modules/invoices/jofotara-gateway.service.ts @@ -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,38 +36,54 @@ export class JoFotaraGatewayService { secretKey: string, ): Promise { const url = this.currentEnv === 'production' ? this.prodUrl : this.sandboxUrl; + const maxRetries = 3; + let attempt = 0; - try { - const payload = { - invoice: Buffer.from(xmlContent).toString('base64'), - }; + while (attempt < maxRetries) { + try { + const payload = { + invoice: Buffer.from(xmlContent).toString('base64'), + }; - this.logger.debug(`Submitting to JoFotara: ${url}/submit`); - - const response = await axios.post( - `${url}/submit`, - payload, - { - headers: { - 'Content-Type': 'application/json', - 'X-Client-Id': clientId, - 'X-Secret-Key': secretKey, + this.logger.debug(`Submitting to JoFotara: ${url} (Attempt ${attempt + 1})`); + + const response = await axios.post( + url, + payload, + { + headers: { + 'Content-Type': 'application/json', + '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) { - 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'}`, - ); + 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 || error.message}`, + ); + } } } /** - * التحقق من حالة الفاتورة + * التحقق من حالة الفاتورة باستخدام الـ UUID + * Check the status of a submitted invoice using its UUID */ async checkStatus(uuid: string, clientId: string, secretKey: string): Promise { const url = this.currentEnv === 'production' ? this.prodUrl : this.sandboxUrl; diff --git a/backend/src/modules/invoices/public-invoice.controller.ts b/backend/src/modules/invoices/public-invoice.controller.ts new file mode 100644 index 0000000..ab39406 --- /dev/null +++ b/backend/src/modules/invoices/public-invoice.controller.ts @@ -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); + } + } + } +} diff --git a/backend/src/modules/invoices/ubl-generator.service.ts b/backend/src/modules/invoices/ubl-generator.service.ts index 7d03a5e..5cbf1b9 100644 --- a/backend/src/modules/invoices/ubl-generator.service.ts +++ b/backend/src/modules/invoices/ubl-generator.service.ts @@ -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() diff --git a/backend/src/modules/tax-advisor/tax-advisor.service.ts b/backend/src/modules/tax-advisor/tax-advisor.service.ts new file mode 100644 index 0000000..6ac2e38 --- /dev/null +++ b/backend/src/modules/tax-advisor/tax-advisor.service.ts @@ -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('GEMINI_API_KEY'); + this.genAI = new GoogleGenerativeAI(apiKey); + this.model = this.genAI.getGenerativeModel({ + model: this.configService.get('GEMINI_MODEL', 'gemini-1.5-flash'), + }); + } + + /** + * الإجابة على استفسارات القوانين الضريبية (RAG Placeholder) + * Answer tax law queries using a specialized system prompt + */ + async askQuestion(question: string): Promise { + 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 'عذراً، تعذر الحصول على إجابة حالياً. يرجى مراجعة القوانين الرسمية.'; + } + } +} diff --git a/backend/src/modules/users/entities/user.entity.ts b/backend/src/modules/users/entities/user.entity.ts index 824467f..9a22e3c 100644 --- a/backend/src/modules/users/entities/user.entity.ts +++ b/backend/src/modules/users/entities/user.entity.ts @@ -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; diff --git a/backend/src/modules/users/enums/role.enum.ts b/backend/src/modules/users/enums/role.enum.ts index d3871dc..1823554 100644 --- a/backend/src/modules/users/enums/role.enum.ts +++ b/backend/src/modules/users/enums/role.enum.ts @@ -6,6 +6,7 @@ export enum UserRole { ADMIN = 'admin', // مدير المكتب (قادر على الإضافة والتعديل الكامل) - ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها) + ACCOUNTANT = 'accountant', // محاسب (قادر على رفع الفواتير وإدارتها لكل الشركات) + CLIENT = 'client', // عميل (قادر على رفع وإدارة فواتير شركته فقط) VIEWER = 'viewer', // مشاهد (قادر على الاطلاع فقط) } diff --git a/backend/src/modules/validation/jofotara-compliance.service.ts b/backend/src/modules/validation/jofotara-compliance.service.ts new file mode 100644 index 0000000..745d43f --- /dev/null +++ b/backend/src/modules/validation/jofotara-compliance.service.ts @@ -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 { + 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) غير مدعوم'); + } + } +} diff --git a/backend/src/modules/validation/tax-validation.module.ts b/backend/src/modules/validation/tax-validation.module.ts index 579b048..49fadf1 100644 --- a/backend/src/modules/validation/tax-validation.module.ts +++ b/backend/src/modules/validation/tax-validation.module.ts @@ -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 {} diff --git a/backend/src/modules/validation/tax-validation.service.ts b/backend/src/modules/validation/tax-validation.service.ts index b0d6e59..a685777 100644 --- a/backend/src/modules/validation/tax-validation.service.ts +++ b/backend/src/modules/validation/tax-validation.service.ts @@ -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)})`); + } + } } diff --git a/backend/src/services/storage/local-storage.service.ts b/backend/src/services/storage/local-storage.service.ts index f8f9944..962d479 100644 --- a/backend/src/services/storage/local-storage.service.ts +++ b/backend/src/services/storage/local-storage.service.ts @@ -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 { 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 { try { diff --git a/frontend/src/pages/Public/TrojanHorseConverter.tsx b/frontend/src/pages/Public/TrojanHorseConverter.tsx new file mode 100644 index 0000000..9299cf3 --- /dev/null +++ b/frontend/src/pages/Public/TrojanHorseConverter.tsx @@ -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(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 ( +
+ + {/* Top Banner */} +
+ هل تدير عشرات الشركات؟ اكتشف لوحة تحكم محاسبي إيليت +
+ +
+ + {/* Header */} +
+
+ أداة مجانية 100% +
+

+ حوّل فواتيرك إلى صيغة جو فوترة في ثوانٍ +

+

+ قم برفع فاتورة الـ PDF الخاصة بك وسيقوم الذكاء الاصطناعي باستخراج البيانات وتحويلها إلى صيغة XML المتوافقة تماماً مع نظام دائرة ضريبة الدخل والمبيعات الأردنية. +

+
+ + {/* Upload Area */} + + {status === 'idle' && ( +
+ + + {file ? ( +
+

{file.name}

+

{(file.size / 1024 / 1024).toFixed(2)} MB

+ +
+ ) : ( +
+

اسحب وأفلت الفاتورة هنا

+

يدعم PDF, PNG, JPG

+ +
+ )} +
+ )} + + {status === 'uploading' && ( +
+
+

جاري استخراج البيانات...

+

يقوم الذكاء الاصطناعي بقراءة الفاتورة وتنسيقها

+
+ )} + + {status === 'success' && ( + +
+ +
+

تم التحويل بنجاح!

+ +
+ + +
+ + {/* Upsell Banner for Accountants */} +
+
+

+ مُصادَق | هل أنت محاسب؟ +

+

توقف عن تحويل الفواتير واحدة تلو الأخرى. جرب لوحة تحكم إيليت وأتمت عملك لـ 50 شركة بنقرة واحدة.

+
+ +
+
+ )} + +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/dashboard/MultiEntityDashboard.tsx b/frontend/src/pages/dashboard/MultiEntityDashboard.tsx new file mode 100644 index 0000000..c66bcde --- /dev/null +++ b/frontend/src/pages/dashboard/MultiEntityDashboard.tsx @@ -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 ( +
+
+ + + + +
+ {score} +
+
+ {status} +
+ ); +}; + +export const MultiEntityDashboard = () => { + return ( +
+ + {/* Header */} +
+
+

+ مُصادَق | لوحة تحكم الشركات +

+

نظرة عامة على الموقف الضريبي لجميع عملائك (Elite View)

+
+ +
+ + +
+
+ + {/* Grid */} +
+ {mockCompanies.map((company, index) => ( + + {/* Ambient glow */} +
+ +
+
+
+
+ +
+
+

{company.name}

+

الرقم الضريبي: 00{company.id}829471

+
+
+
+ +
+
+

التدفق النقدي (المبيعات)

+

${(company.pendingAmount / 1000).toFixed(1)}k

+
+ مستقر +
+
+
+
+

درجة الخطر الضريبي

+ +
+
+
+ +
+
+

الفواتير المعلقة

+ {company.totalInvoices} فاتورة | ${company.totalTax} ضرائب +
+ + {/* Minimal Progress Bar */} +
+
+
+
+
+ +
+ تم الرفع (60%) + {company.failedCount > 0 && ( + + {company.failedCount} مرفوضة + + )} +
+
+
+ + ))} +
+
+ ); +};