feat(purchases): implement purchase receipt functionality with form and receipt template

- Added `ProductPurchaseComponent` to handle product purchases.
- Created `PurchaseReceiptTemplateComponent` for displaying purchase receipt details.
- Developed `PurchaseFormComponent` for managing purchase form inputs and submission.
- Introduced `ProductChargeRowFormComponent` and `ProductChargeRowFormFieldsComponent` for handling product charge rows in the form.
- Implemented `PurchaseReceiptProductRowComponent` for displaying individual product rows in the receipt.
- Established API routes for purchase operations in `apiRoutes`.
- Integrated suppliers and inventories selection components.
- Added utility functions for converting prices to Persian alphabet.
- Created a utility for generating full names from name parts.
- Enhanced form validation and submission handling in the purchase form.
- Updated styles and templates for improved UI/UX.
This commit is contained in:
2025-12-09 20:17:00 +03:30
parent abf53bac03
commit 50bc9a4632
77 changed files with 1807 additions and 18834 deletions
+91
View File
@@ -0,0 +1,91 @@
/**
* Persian number words for currency conversion
*/
const ONES = ['', 'یک', 'دو', 'سه', 'چهار', 'پنج', 'شش', 'هفت', 'هشت', 'نه'];
const TENS = ['', '', 'بیست', 'سی', 'چهل', 'پنجاه', 'شصت', 'هفتاد', 'هشتاد', 'نود'];
const HUNDREDS = ['', 'یکصد', 'دویست', 'سیصد', 'چهارصد', 'پانصد', 'ششصد', 'هفتصد', 'هشتصد', 'نهصد'];
const SCALE = ['', 'هزار', 'میلیون', 'میلیارد', 'تریلیون', 'کوادریلیون'];
const TEENS = [
'ده',
'یازده',
'دوازده',
'سیزده',
'چهارده',
'پانزده',
'شانزده',
'هفده',
'هجده',
'نوزده',
];
/**
* Convert a three-digit number to Persian words
*/
function convertThreeDigits(num: number): string {
if (num === 0) return '';
const hundred = Math.floor(num / 100);
const remainder = num % 100;
const ten = Math.floor(remainder / 10);
const one = remainder % 10;
let result = '';
if (hundred > 0) {
result += HUNDREDS[hundred];
}
if (remainder >= 10 && remainder <= 19) {
if (result) result += ' و ';
result += TEENS[remainder - 10];
} else {
if (ten > 0) {
if (result) result += ' و ';
result += TENS[ten];
}
if (one > 0) {
if (result) result += ' و ';
result += ONES[one];
}
}
return result;
}
/**
* Convert a number to Persian words for Rial currency
* @param price - The price as a number
* @returns Persian text representation of the price in Rials
* @example
* getPriceInPersianAlphabet(1500) // "هزار و پانصد ریال"
* getPriceInPersianAlphabet(1000000) // "یک میلیون ریال"
*/
export function getPriceInPersianAlphabet(price: number): string {
if (price === 0) return 'صفر ریال';
if (price < 0) return 'منفی ' + getPriceInPersianAlphabet(-price);
const parts: string[] = [];
let scaleIndex = 0;
while (price > 0 && scaleIndex < SCALE.length) {
const threeDigits = price % 1000;
if (threeDigits > 0) {
const threeDigitWords = convertThreeDigits(threeDigits);
if (SCALE[scaleIndex]) {
parts.unshift(threeDigitWords + ' ' + SCALE[scaleIndex]);
} else {
parts.unshift(threeDigitWords);
}
}
price = Math.floor(price / 1000);
scaleIndex++;
}
return parts.join(' و ') + ' ریال';
}
+2
View File
@@ -1 +1,3 @@
export * from './currency';
export * from './name';
export * from './time';
+28
View File
@@ -0,0 +1,28 @@
export interface NameParts {
gender?: string;
firstName?: string;
lastName?: string;
}
/**
* Get full name from name parts
* @param nameParts - Object containing optional gender, firstName, and lastName
* @returns Full name string with parts separated by space
*/
export function getFullName(nameParts: NameParts): string {
const parts: string[] = [];
if (nameParts.gender) {
parts.push(nameParts.gender);
}
if (nameParts.firstName) {
parts.push(nameParts.firstName);
}
if (nameParts.lastName) {
parts.push(nameParts.lastName);
}
return parts.join(' ').trim();
}