almost finish persons
This commit is contained in:
parent
a409202f6f
commit
a371499e31
|
|
@ -713,9 +713,37 @@ async def import_persons_excel(
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
import logging
|
import logging
|
||||||
|
import zipfile
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def validate_excel_file(content: bytes) -> bool:
|
||||||
|
"""
|
||||||
|
Validate if the content is a valid Excel file
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Check if it starts with PK signature (zip file)
|
||||||
|
if not content.startswith(b'PK'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Try to open as zip file
|
||||||
|
with zipfile.ZipFile(io.BytesIO(content), 'r') as zip_file:
|
||||||
|
file_list = zip_file.namelist()
|
||||||
|
# Check for Excel structure (xl/ folder for .xlsx files)
|
||||||
|
excel_structure = any(f.startswith('xl/') for f in file_list)
|
||||||
|
if excel_structure:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check for older Excel format (.xls) - this would be a different structure
|
||||||
|
# But since we only support .xlsx, we'll return False for .xls
|
||||||
|
return False
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
logger.error("File is not a valid zip file")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error validating Excel file: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Convert dry_run string to boolean
|
# Convert dry_run string to boolean
|
||||||
dry_run_bool = dry_run.lower() in ('true', '1', 'yes', 'on')
|
dry_run_bool = dry_run.lower() in ('true', '1', 'yes', 'on')
|
||||||
|
|
@ -730,18 +758,27 @@ async def import_persons_excel(
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
logger.info(f"File content size: {len(content)} bytes")
|
logger.info(f"File content size: {len(content)} bytes")
|
||||||
|
|
||||||
|
# Log first few bytes for debugging
|
||||||
|
logger.info(f"File header (first 20 bytes): {content[:20].hex()}")
|
||||||
|
logger.info(f"File header (first 20 bytes as text): {content[:20]}")
|
||||||
|
|
||||||
# Check if content is empty or too small
|
# Check if content is empty or too small
|
||||||
if len(content) < 100:
|
if len(content) < 100:
|
||||||
logger.error(f"File too small: {len(content)} bytes")
|
logger.error(f"File too small: {len(content)} bytes")
|
||||||
raise HTTPException(status_code=400, detail="فایل خیلی کوچک است یا خالی است")
|
raise HTTPException(status_code=400, detail="فایل خیلی کوچک است یا خالی است")
|
||||||
|
|
||||||
# Check if it's a valid Excel file by looking at the first few bytes
|
# Validate Excel file format
|
||||||
if not content.startswith(b'PK'):
|
if not validate_excel_file(content):
|
||||||
logger.error("File does not start with PK signature (not a valid Excel file)")
|
logger.error("File is not a valid Excel file")
|
||||||
raise HTTPException(status_code=400, detail="فرمت فایل معتبر نیست. فایل Excel معتبر نیست")
|
raise HTTPException(status_code=400, detail="فرمت فایل معتبر نیست. فایل Excel معتبر نیست")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Try to load the workbook with additional error handling
|
||||||
wb = load_workbook(filename=io.BytesIO(content), data_only=True)
|
wb = load_workbook(filename=io.BytesIO(content), data_only=True)
|
||||||
|
logger.info(f"Successfully loaded workbook with {len(wb.worksheets)} worksheets")
|
||||||
|
except zipfile.BadZipFile as e:
|
||||||
|
logger.error(f"Bad zip file error: {str(e)}")
|
||||||
|
raise HTTPException(status_code=400, detail="فایل Excel خراب است یا فرمت آن معتبر نیست")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading workbook: {str(e)}")
|
logger.error(f"Error loading workbook: {str(e)}")
|
||||||
raise HTTPException(status_code=400, detail=f"امکان خواندن فایل وجود ندارد: {str(e)}")
|
raise HTTPException(status_code=400, detail=f"امکان خواندن فایل وجود ندارد: {str(e)}")
|
||||||
|
|
@ -835,21 +872,29 @@ async def import_persons_excel(
|
||||||
|
|
||||||
for data in valid_items:
|
for data in valid_items:
|
||||||
existing = find_existing(db, data)
|
existing = find_existing(db, data)
|
||||||
|
match_value = None
|
||||||
|
try:
|
||||||
|
match_value = data.get(match_by)
|
||||||
|
except Exception:
|
||||||
|
match_value = None
|
||||||
if existing is None:
|
if existing is None:
|
||||||
# create
|
# create
|
||||||
try:
|
try:
|
||||||
create_person(db, business_id, PersonCreateRequest(**data))
|
create_person(db, business_id, PersonCreateRequest(**data))
|
||||||
inserted += 1
|
inserted += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Create person failed for data={data}: {str(e)}")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
else:
|
else:
|
||||||
if conflict_policy == 'insert':
|
if conflict_policy == 'insert':
|
||||||
|
logger.info(f"Skipping existing person (match_by={match_by}, value={match_value}) due to conflict_policy=insert")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
elif conflict_policy in ('update', 'upsert'):
|
elif conflict_policy in ('update', 'upsert'):
|
||||||
try:
|
try:
|
||||||
update_person(db, existing.id, business_id, PersonUpdateRequest(**data))
|
update_person(db, existing.id, business_id, PersonUpdateRequest(**data))
|
||||||
updated += 1
|
updated += 1
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Update person failed for id={existing.id}, data={data}: {str(e)}")
|
||||||
skipped += 1
|
skipped += 1
|
||||||
|
|
||||||
summary = {
|
summary = {
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ class PersonBankAccountUpdateRequest(BaseModel):
|
||||||
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
|
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
|
||||||
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
|
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
|
||||||
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
|
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
|
||||||
is_active: Optional[bool] = Field(default=None, description="وضعیت فعال بودن")
|
|
||||||
|
|
||||||
|
|
||||||
class PersonBankAccountResponse(BaseModel):
|
class PersonBankAccountResponse(BaseModel):
|
||||||
|
|
@ -40,7 +39,6 @@ class PersonBankAccountResponse(BaseModel):
|
||||||
account_number: Optional[str] = Field(default=None, description="شماره حساب")
|
account_number: Optional[str] = Field(default=None, description="شماره حساب")
|
||||||
card_number: Optional[str] = Field(default=None, description="شماره کارت")
|
card_number: Optional[str] = Field(default=None, description="شماره کارت")
|
||||||
sheba_number: Optional[str] = Field(default=None, description="شماره شبا")
|
sheba_number: Optional[str] = Field(default=None, description="شماره شبا")
|
||||||
is_active: bool = Field(..., description="وضعیت فعال بودن")
|
|
||||||
created_at: str = Field(..., description="تاریخ ایجاد")
|
created_at: str = Field(..., description="تاریخ ایجاد")
|
||||||
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
||||||
|
|
||||||
|
|
@ -142,8 +140,6 @@ class PersonUpdateRequest(BaseModel):
|
||||||
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
|
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
|
||||||
website: Optional[str] = Field(default=None, max_length=255, description="وبسایت")
|
website: Optional[str] = Field(default=None, max_length=255, description="وبسایت")
|
||||||
|
|
||||||
# وضعیت
|
|
||||||
is_active: Optional[bool] = Field(default=None, description="وضعیت فعال بودن")
|
|
||||||
# سهام
|
# سهام
|
||||||
share_count: Optional[int] = Field(default=None, ge=1, description="تعداد سهام (برای سهامدار)")
|
share_count: Optional[int] = Field(default=None, ge=1, description="تعداد سهام (برای سهامدار)")
|
||||||
# پورسانت
|
# پورسانت
|
||||||
|
|
@ -210,9 +206,6 @@ class PersonResponse(BaseModel):
|
||||||
email: Optional[str] = Field(default=None, description="پست الکترونیکی")
|
email: Optional[str] = Field(default=None, description="پست الکترونیکی")
|
||||||
website: Optional[str] = Field(default=None, description="وبسایت")
|
website: Optional[str] = Field(default=None, description="وبسایت")
|
||||||
|
|
||||||
# وضعیت
|
|
||||||
is_active: bool = Field(..., description="وضعیت فعال بودن")
|
|
||||||
|
|
||||||
# زمانبندی
|
# زمانبندی
|
||||||
created_at: str = Field(..., description="تاریخ ایجاد")
|
created_at: str = Field(..., description="تاریخ ایجاد")
|
||||||
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
||||||
|
|
|
||||||
|
|
@ -245,9 +245,11 @@ def create_app() -> FastAPI:
|
||||||
async def smart_number_normalizer(request: Request, call_next):
|
async def smart_number_normalizer(request: Request, call_next):
|
||||||
"""Middleware هوشمند برای تبدیل اعداد فارسی/عربی به انگلیسی"""
|
"""Middleware هوشمند برای تبدیل اعداد فارسی/عربی به انگلیسی"""
|
||||||
if SmartNormalizerConfig.ENABLED and request.method in ["POST", "PUT", "PATCH"]:
|
if SmartNormalizerConfig.ENABLED and request.method in ["POST", "PUT", "PATCH"]:
|
||||||
|
# فقط برای درخواستهای JSON اعمال شود تا فایلهای باینری/چندبخشی خراب نشوند
|
||||||
|
content_type = request.headers.get("Content-Type", "").lower()
|
||||||
|
if content_type.startswith("application/json"):
|
||||||
# خواندن body درخواست
|
# خواندن body درخواست
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
|
|
||||||
if body:
|
if body:
|
||||||
# تبدیل اعداد در JSON
|
# تبدیل اعداد در JSON
|
||||||
normalized_body = smart_normalize_json(body)
|
normalized_body = smart_normalize_json(body)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ def create_person(db: Session, business_id: int, person_data: PersonCreateReques
|
||||||
t = person_data.person_type
|
t = person_data.person_type
|
||||||
types_list = [t.value if hasattr(t, 'value') else str(t)]
|
types_list = [t.value if hasattr(t, 'value') else str(t)]
|
||||||
|
|
||||||
|
# نوع تکی برای استفادههای بعدی (قبل از هر استفاده تعریف شود)
|
||||||
|
incoming_single_type = getattr(person_data, 'person_type', None)
|
||||||
|
|
||||||
# اعتبارسنجی سهام برای سهامدار
|
# اعتبارسنجی سهام برای سهامدار
|
||||||
is_shareholder = False
|
is_shareholder = False
|
||||||
if types_list:
|
if types_list:
|
||||||
|
|
@ -50,7 +53,6 @@ def create_person(db: Session, business_id: int, person_data: PersonCreateReques
|
||||||
|
|
||||||
# ایجاد شخص
|
# ایجاد شخص
|
||||||
# نگاشت person_type دریافتی از اسکیما به Enum مدل
|
# نگاشت person_type دریافتی از اسکیما به Enum مدل
|
||||||
incoming_single_type = getattr(person_data, 'person_type', None)
|
|
||||||
mapped_single_type = None
|
mapped_single_type = None
|
||||||
if incoming_single_type is not None:
|
if incoming_single_type is not None:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ migrations/versions/20250927_000019_seed_accounts_chart.py
|
||||||
migrations/versions/20250927_000020_add_share_count_and_shareholder_type.py
|
migrations/versions/20250927_000020_add_share_count_and_shareholder_type.py
|
||||||
migrations/versions/20250927_000021_update_person_type_enum_to_persian.py
|
migrations/versions/20250927_000021_update_person_type_enum_to_persian.py
|
||||||
migrations/versions/20250927_000022_add_person_commission_fields.py
|
migrations/versions/20250927_000022_add_person_commission_fields.py
|
||||||
|
migrations/versions/20250928_000023_remove_person_is_active_force.py
|
||||||
migrations/versions/4b2ea782bcb3_merge_heads.py
|
migrations/versions/4b2ea782bcb3_merge_heads.py
|
||||||
migrations/versions/5553f8745c6e_add_support_tables.py
|
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||||
migrations/versions/d3e84892c1c2_sync_person_type_enum_values_callable_.py
|
migrations/versions/d3e84892c1c2_sync_person_type_enum_values_callable_.py
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20250928_000023_remove_person_is_active_force'
|
||||||
|
down_revision = '4b2ea782bcb3'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
tables = set(inspector.get_table_names())
|
||||||
|
|
||||||
|
# Drop is_active from persons if exists
|
||||||
|
if 'persons' in tables:
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('persons')}
|
||||||
|
if 'is_active' in columns:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
try:
|
||||||
|
batch_op.drop_column('is_active')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Drop is_active from person_bank_accounts if exists
|
||||||
|
if 'person_bank_accounts' in tables:
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('person_bank_accounts')}
|
||||||
|
if 'is_active' in columns:
|
||||||
|
with op.batch_alter_table('person_bank_accounts') as batch_op:
|
||||||
|
try:
|
||||||
|
batch_op.drop_column('is_active')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Recreate columns with safe defaults if needed
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = inspect(bind)
|
||||||
|
tables = set(inspector.get_table_names())
|
||||||
|
|
||||||
|
if 'persons' in tables:
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('persons')}
|
||||||
|
if 'is_active' not in columns:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')))
|
||||||
|
|
||||||
|
if 'person_bank_accounts' in tables:
|
||||||
|
columns = {col['name'] for col in inspector.get_columns('person_bank_accounts')}
|
||||||
|
if 'is_active' not in columns:
|
||||||
|
with op.batch_alter_table('person_bank_accounts') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -166,6 +166,7 @@
|
||||||
"exportSuccess": "Export completed successfully",
|
"exportSuccess": "Export completed successfully",
|
||||||
"exportError": "Export error",
|
"exportError": "Export error",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
|
"importFromExcel": "Import from Excel",
|
||||||
"rowNumber": "Row",
|
"rowNumber": "Row",
|
||||||
"firstName": "First Name",
|
"firstName": "First Name",
|
||||||
"lastName": "Last Name",
|
"lastName": "Last Name",
|
||||||
|
|
@ -489,6 +490,7 @@
|
||||||
"printDocuments": "Print Documents",
|
"printDocuments": "Print Documents",
|
||||||
"people": "People",
|
"people": "People",
|
||||||
"peopleList": "People List",
|
"peopleList": "People List",
|
||||||
|
"personCode": "Person Code",
|
||||||
"receipts": "Receipts",
|
"receipts": "Receipts",
|
||||||
"payments": "Payments",
|
"payments": "Payments",
|
||||||
"receiptsAndPayments": "Receipts and Payments",
|
"receiptsAndPayments": "Receipts and Payments",
|
||||||
|
|
@ -858,6 +860,59 @@
|
||||||
"buy": "Buy",
|
"buy": "Buy",
|
||||||
"templates": "Templates",
|
"templates": "Templates",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"business": "Business"
|
"business": "Business",
|
||||||
|
"shareCount": "Share Count",
|
||||||
|
"commissionSalePercentLabel": "Commission Sale Percent",
|
||||||
|
"commissionSalesReturnPercentLabel": "Commission Sales Return Percent",
|
||||||
|
"commissionSalesAmountLabel": "Commission Sales Amount",
|
||||||
|
"commissionSalesReturnAmountLabel": "Commission Sales Return Amount",
|
||||||
|
"importPersonsFromExcel": "Import Persons from Excel",
|
||||||
|
"selectedFile": "Selected file",
|
||||||
|
"noFileSelected": "No file selected",
|
||||||
|
"chooseFile": "Choose file",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"code": "code",
|
||||||
|
"conflictPolicy": "Conflict policy",
|
||||||
|
"policyInsertOnly": "Insert-only",
|
||||||
|
"policyUpdateExisting": "Update existing",
|
||||||
|
"policyUpsert": "Upsert",
|
||||||
|
"dryRun": "Dry run",
|
||||||
|
"dryRunValidateOnly": "Dry run (validate only)",
|
||||||
|
"downloadTemplate": "Download template",
|
||||||
|
"reviewDryRun": "Review (Dry run)",
|
||||||
|
"import": "Import",
|
||||||
|
"importReal": "Import (real)",
|
||||||
|
"templateDownloaded": "Template downloaded",
|
||||||
|
"pickFileError": "Error picking file",
|
||||||
|
"templateDownloadError": "Error downloading template",
|
||||||
|
"importError": "Import error",
|
||||||
|
"result": "Result",
|
||||||
|
"valid": "Valid",
|
||||||
|
"invalid": "Invalid",
|
||||||
|
"inserted": "Inserted",
|
||||||
|
"updated": "Updated",
|
||||||
|
"skipped": "Skipped",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No",
|
||||||
|
"row": "Row",
|
||||||
|
"onlyForMarketerSeller": "This section is shown only for marketer/seller",
|
||||||
|
"percentFromSales": "Percent from sales",
|
||||||
|
"percentFromSalesReturn": "Percent from sales return",
|
||||||
|
"salesAmount": "Sales amount",
|
||||||
|
"salesReturnAmount": "Sales return amount",
|
||||||
|
"mustBeBetweenZeroAndHundred": "Must be between 0 and 100",
|
||||||
|
"mustBePositiveNumber": "Must be a positive number",
|
||||||
|
"personCodeOptional": "Person code (optional)",
|
||||||
|
"uniqueCodeNumeric": "Unique code (numeric)",
|
||||||
|
"automatic": "Automatic",
|
||||||
|
"manual": "Manual",
|
||||||
|
"personCodeRequired": "Person code is required",
|
||||||
|
"codeMustBeNumeric": "Code must be numeric",
|
||||||
|
"integerNoDecimal": "Integer number (no decimals)",
|
||||||
|
"shareholderShareCountRequired": "For shareholder, share count is required",
|
||||||
|
"noBankAccountsAdded": "No bank accounts added",
|
||||||
|
"commissionExcludeDiscounts": "Exclude discounts from commission",
|
||||||
|
"commissionExcludeAdditionsDeductions": "Exclude additions/deductions from commission",
|
||||||
|
"commissionPostInInvoiceDocument": "Post commission in invoice accounting document"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
"hideFilters": "مخفی کردن فیلترها",
|
"hideFilters": "مخفی کردن فیلترها",
|
||||||
"showFilters": "نمایش فیلترها",
|
"showFilters": "نمایش فیلترها",
|
||||||
"clear": "پاک کردن",
|
"clear": "پاک کردن",
|
||||||
"searchInNameEmail": "جستجو در نام، نام خانوادگی و ایمیل...",
|
"searchInNameEmail": "جستجو",
|
||||||
"recordsPerPage": "تعداد در صفحه",
|
"recordsPerPage": "تعداد در صفحه",
|
||||||
"records": "رکورد",
|
"records": "رکورد",
|
||||||
"test": "تست",
|
"test": "تست",
|
||||||
|
|
@ -165,6 +165,7 @@
|
||||||
"exportSuccess": "خروجی با موفقیت انجام شد",
|
"exportSuccess": "خروجی با موفقیت انجام شد",
|
||||||
"exportError": "خطا در خروجی",
|
"exportError": "خطا در خروجی",
|
||||||
"export": "خروجی",
|
"export": "خروجی",
|
||||||
|
"importFromExcel": "ایمپورت از اکسل",
|
||||||
"rowNumber": "ردیف",
|
"rowNumber": "ردیف",
|
||||||
"firstName": "نام",
|
"firstName": "نام",
|
||||||
"lastName": "نام خانوادگی",
|
"lastName": "نام خانوادگی",
|
||||||
|
|
@ -488,6 +489,7 @@
|
||||||
"printDocuments": "چاپ اسناد",
|
"printDocuments": "چاپ اسناد",
|
||||||
"people": "اشخاص",
|
"people": "اشخاص",
|
||||||
"peopleList": "لیست اشخاص",
|
"peopleList": "لیست اشخاص",
|
||||||
|
"personCode": "کد شخص",
|
||||||
"receipts": "دریافتها",
|
"receipts": "دریافتها",
|
||||||
"payments": "پرداختها",
|
"payments": "پرداختها",
|
||||||
"receiptsAndPayments": "دریافت و پرداخت",
|
"receiptsAndPayments": "دریافت و پرداخت",
|
||||||
|
|
@ -737,7 +739,6 @@
|
||||||
"eventHistory": "تاریخچه رویدادها",
|
"eventHistory": "تاریخچه رویدادها",
|
||||||
"usersAndPermissions": "کاربران و دسترسیها",
|
"usersAndPermissions": "کاربران و دسترسیها",
|
||||||
"storageSpace": "فضای ذخیرهسازی",
|
"storageSpace": "فضای ذخیرهسازی",
|
||||||
"viewStorage": "مشاهده فضای ذخیرهسازی",
|
|
||||||
"deleteFiles": "حذف فایلها",
|
"deleteFiles": "حذف فایلها",
|
||||||
"smsPanel": "پنل پیامک",
|
"smsPanel": "پنل پیامک",
|
||||||
"viewSmsHistory": "مشاهده تاریخچه پیامکها",
|
"viewSmsHistory": "مشاهده تاریخچه پیامکها",
|
||||||
|
|
@ -780,6 +781,7 @@
|
||||||
"accountManagement": "مدیریت حساب کاربری",
|
"accountManagement": "مدیریت حساب کاربری",
|
||||||
"persons": "اشخاص",
|
"persons": "اشخاص",
|
||||||
"personsList": "لیست اشخاص",
|
"personsList": "لیست اشخاص",
|
||||||
|
"personCode": "کد شخص",
|
||||||
"addPerson": "افزودن شخص",
|
"addPerson": "افزودن شخص",
|
||||||
"editPerson": "ویرایش شخص",
|
"editPerson": "ویرایش شخص",
|
||||||
"personDetails": "جزئیات شخص",
|
"personDetails": "جزئیات شخص",
|
||||||
|
|
@ -857,6 +859,59 @@
|
||||||
"buy": "خرید",
|
"buy": "خرید",
|
||||||
"templates": "قالبها",
|
"templates": "قالبها",
|
||||||
"history": "تاریخچه",
|
"history": "تاریخچه",
|
||||||
"business": "کسب و کار"
|
"business": "کسب و کار",
|
||||||
|
"shareCount": "تعداد سهام",
|
||||||
|
"commissionSalePercentLabel": "درصد پورسانت فروش",
|
||||||
|
"commissionSalesReturnPercentLabel": "درصد پورسانت برگشت از فروش",
|
||||||
|
"commissionSalesAmountLabel": "مبلغ پورسانت فروش",
|
||||||
|
"commissionSalesReturnAmountLabel": "مبلغ پورسانت برگشت از فروش",
|
||||||
|
"importPersonsFromExcel": "ایمپورت اشخاص از اکسل",
|
||||||
|
"selectedFile": "فایل انتخابشده",
|
||||||
|
"noFileSelected": "هیچ فایلی انتخاب نشده",
|
||||||
|
"chooseFile": "انتخاب فایل",
|
||||||
|
"matchBy": "معیار تطبیق",
|
||||||
|
"code": "کد",
|
||||||
|
"conflictPolicy": "سیاست تداخل",
|
||||||
|
"policyInsertOnly": "فقط ایجاد",
|
||||||
|
"policyUpdateExisting": "بهروزرسانی موجود",
|
||||||
|
"policyUpsert": "آپسرت",
|
||||||
|
"dryRun": "اجرای آزمایشی",
|
||||||
|
"dryRunValidateOnly": "اجرای آزمایشی (فقط اعتبارسنجی)",
|
||||||
|
"downloadTemplate": "دانلود تمپلیت",
|
||||||
|
"reviewDryRun": "بررسی (اجرای آزمایشی)",
|
||||||
|
"import": "ایمپورت",
|
||||||
|
"importReal": "ایمپورت واقعی",
|
||||||
|
"templateDownloaded": "تمپلیت دانلود شد",
|
||||||
|
"pickFileError": "خطا در انتخاب فایل",
|
||||||
|
"templateDownloadError": "خطا در دانلود تمپلیت",
|
||||||
|
"importError": "خطا در ایمپورت",
|
||||||
|
"result": "نتیجه",
|
||||||
|
"valid": "معتبر",
|
||||||
|
"invalid": "نامعتبر",
|
||||||
|
"inserted": "ایجاد شده",
|
||||||
|
"updated": "بهروزرسانی",
|
||||||
|
"skipped": "رد شده",
|
||||||
|
"yes": "بله",
|
||||||
|
"no": "خیر",
|
||||||
|
"row": "ردیف",
|
||||||
|
"onlyForMarketerSeller": "این بخش فقط برای بازاریاب/فروشنده نمایش داده میشود",
|
||||||
|
"percentFromSales": "درصد از فروش",
|
||||||
|
"percentFromSalesReturn": "درصد از برگشت از فروش",
|
||||||
|
"salesAmount": "مبلغ فروش",
|
||||||
|
"salesReturnAmount": "مبلغ برگشت از فروش",
|
||||||
|
"mustBeBetweenZeroAndHundred": "باید بین 0 تا 100 باشد",
|
||||||
|
"mustBePositiveNumber": "باید عدد مثبت باشد",
|
||||||
|
"personCodeOptional": "کد شخص (اختیاری)",
|
||||||
|
"uniqueCodeNumeric": "کد یکتا (عددی)",
|
||||||
|
"automatic": "اتوماتیک",
|
||||||
|
"manual": "دستی",
|
||||||
|
"personCodeRequired": "کد شخص الزامی است",
|
||||||
|
"codeMustBeNumeric": "کد باید عددی باشد",
|
||||||
|
"integerNoDecimal": "عدد صحیح بدون اعشار",
|
||||||
|
"shareholderShareCountRequired": "برای سهامدار، تعداد سهام الزامی است",
|
||||||
|
"noBankAccountsAdded": "هیچ حساب بانکی اضافه نشده است",
|
||||||
|
"commissionExcludeDiscounts": "عدم محاسبه تخفیف",
|
||||||
|
"commissionExcludeAdditionsDeductions": "عدم محاسبه اضافات و کسورات فاکتور",
|
||||||
|
"commissionPostInInvoiceDocument": "ثبت پورسانت در سند حسابداری فاکتور"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1022,6 +1022,12 @@ abstract class AppLocalizations {
|
||||||
/// **'Export'**
|
/// **'Export'**
|
||||||
String get export;
|
String get export;
|
||||||
|
|
||||||
|
/// No description provided for @importFromExcel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import from Excel'**
|
||||||
|
String get importFromExcel;
|
||||||
|
|
||||||
/// No description provided for @rowNumber.
|
/// No description provided for @rowNumber.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2744,6 +2750,12 @@ abstract class AppLocalizations {
|
||||||
/// **'People List'**
|
/// **'People List'**
|
||||||
String get peopleList;
|
String get peopleList;
|
||||||
|
|
||||||
|
/// No description provided for @personCode.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Person Code'**
|
||||||
|
String get personCode;
|
||||||
|
|
||||||
/// No description provided for @receipts.
|
/// No description provided for @receipts.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -4627,6 +4639,324 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Business'**
|
/// **'Business'**
|
||||||
String get business;
|
String get business;
|
||||||
|
|
||||||
|
/// No description provided for @shareCount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Share Count'**
|
||||||
|
String get shareCount;
|
||||||
|
|
||||||
|
/// No description provided for @commissionSalePercentLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Commission Sale Percent'**
|
||||||
|
String get commissionSalePercentLabel;
|
||||||
|
|
||||||
|
/// No description provided for @commissionSalesReturnPercentLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Commission Sales Return Percent'**
|
||||||
|
String get commissionSalesReturnPercentLabel;
|
||||||
|
|
||||||
|
/// No description provided for @commissionSalesAmountLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Commission Sales Amount'**
|
||||||
|
String get commissionSalesAmountLabel;
|
||||||
|
|
||||||
|
/// No description provided for @commissionSalesReturnAmountLabel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Commission Sales Return Amount'**
|
||||||
|
String get commissionSalesReturnAmountLabel;
|
||||||
|
|
||||||
|
/// No description provided for @importPersonsFromExcel.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import Persons from Excel'**
|
||||||
|
String get importPersonsFromExcel;
|
||||||
|
|
||||||
|
/// No description provided for @selectedFile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Selected file'**
|
||||||
|
String get selectedFile;
|
||||||
|
|
||||||
|
/// No description provided for @noFileSelected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No file selected'**
|
||||||
|
String get noFileSelected;
|
||||||
|
|
||||||
|
/// No description provided for @chooseFile.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose file'**
|
||||||
|
String get chooseFile;
|
||||||
|
|
||||||
|
/// No description provided for @matchBy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Match by'**
|
||||||
|
String get matchBy;
|
||||||
|
|
||||||
|
/// No description provided for @code.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'code'**
|
||||||
|
String get code;
|
||||||
|
|
||||||
|
/// No description provided for @conflictPolicy.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Conflict policy'**
|
||||||
|
String get conflictPolicy;
|
||||||
|
|
||||||
|
/// No description provided for @policyInsertOnly.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Insert-only'**
|
||||||
|
String get policyInsertOnly;
|
||||||
|
|
||||||
|
/// No description provided for @policyUpdateExisting.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Update existing'**
|
||||||
|
String get policyUpdateExisting;
|
||||||
|
|
||||||
|
/// No description provided for @policyUpsert.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Upsert'**
|
||||||
|
String get policyUpsert;
|
||||||
|
|
||||||
|
/// No description provided for @dryRun.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Dry run'**
|
||||||
|
String get dryRun;
|
||||||
|
|
||||||
|
/// No description provided for @dryRunValidateOnly.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Dry run (validate only)'**
|
||||||
|
String get dryRunValidateOnly;
|
||||||
|
|
||||||
|
/// No description provided for @downloadTemplate.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Download template'**
|
||||||
|
String get downloadTemplate;
|
||||||
|
|
||||||
|
/// No description provided for @reviewDryRun.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Review (Dry run)'**
|
||||||
|
String get reviewDryRun;
|
||||||
|
|
||||||
|
/// No description provided for @import.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import'**
|
||||||
|
String get import;
|
||||||
|
|
||||||
|
/// No description provided for @importReal.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import (real)'**
|
||||||
|
String get importReal;
|
||||||
|
|
||||||
|
/// No description provided for @templateDownloaded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Template downloaded'**
|
||||||
|
String get templateDownloaded;
|
||||||
|
|
||||||
|
/// No description provided for @pickFileError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Error picking file'**
|
||||||
|
String get pickFileError;
|
||||||
|
|
||||||
|
/// No description provided for @templateDownloadError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Error downloading template'**
|
||||||
|
String get templateDownloadError;
|
||||||
|
|
||||||
|
/// No description provided for @importError.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Import error'**
|
||||||
|
String get importError;
|
||||||
|
|
||||||
|
/// No description provided for @result.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Result'**
|
||||||
|
String get result;
|
||||||
|
|
||||||
|
/// No description provided for @valid.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Valid'**
|
||||||
|
String get valid;
|
||||||
|
|
||||||
|
/// No description provided for @invalid.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Invalid'**
|
||||||
|
String get invalid;
|
||||||
|
|
||||||
|
/// No description provided for @inserted.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Inserted'**
|
||||||
|
String get inserted;
|
||||||
|
|
||||||
|
/// No description provided for @updated.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Updated'**
|
||||||
|
String get updated;
|
||||||
|
|
||||||
|
/// No description provided for @skipped.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Skipped'**
|
||||||
|
String get skipped;
|
||||||
|
|
||||||
|
/// No description provided for @yes.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Yes'**
|
||||||
|
String get yes;
|
||||||
|
|
||||||
|
/// No description provided for @no.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No'**
|
||||||
|
String get no;
|
||||||
|
|
||||||
|
/// No description provided for @row.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Row'**
|
||||||
|
String get row;
|
||||||
|
|
||||||
|
/// No description provided for @onlyForMarketerSeller.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'This section is shown only for marketer/seller'**
|
||||||
|
String get onlyForMarketerSeller;
|
||||||
|
|
||||||
|
/// No description provided for @percentFromSales.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Percent from sales'**
|
||||||
|
String get percentFromSales;
|
||||||
|
|
||||||
|
/// No description provided for @percentFromSalesReturn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Percent from sales return'**
|
||||||
|
String get percentFromSalesReturn;
|
||||||
|
|
||||||
|
/// No description provided for @salesAmount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sales amount'**
|
||||||
|
String get salesAmount;
|
||||||
|
|
||||||
|
/// No description provided for @salesReturnAmount.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Sales return amount'**
|
||||||
|
String get salesReturnAmount;
|
||||||
|
|
||||||
|
/// No description provided for @mustBeBetweenZeroAndHundred.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Must be between 0 and 100'**
|
||||||
|
String get mustBeBetweenZeroAndHundred;
|
||||||
|
|
||||||
|
/// No description provided for @mustBePositiveNumber.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Must be a positive number'**
|
||||||
|
String get mustBePositiveNumber;
|
||||||
|
|
||||||
|
/// No description provided for @personCodeOptional.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Person code (optional)'**
|
||||||
|
String get personCodeOptional;
|
||||||
|
|
||||||
|
/// No description provided for @uniqueCodeNumeric.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Unique code (numeric)'**
|
||||||
|
String get uniqueCodeNumeric;
|
||||||
|
|
||||||
|
/// No description provided for @automatic.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Automatic'**
|
||||||
|
String get automatic;
|
||||||
|
|
||||||
|
/// No description provided for @manual.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Manual'**
|
||||||
|
String get manual;
|
||||||
|
|
||||||
|
/// No description provided for @personCodeRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Person code is required'**
|
||||||
|
String get personCodeRequired;
|
||||||
|
|
||||||
|
/// No description provided for @codeMustBeNumeric.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Code must be numeric'**
|
||||||
|
String get codeMustBeNumeric;
|
||||||
|
|
||||||
|
/// No description provided for @integerNoDecimal.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Integer number (no decimals)'**
|
||||||
|
String get integerNoDecimal;
|
||||||
|
|
||||||
|
/// No description provided for @shareholderShareCountRequired.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'For shareholder, share count is required'**
|
||||||
|
String get shareholderShareCountRequired;
|
||||||
|
|
||||||
|
/// No description provided for @noBankAccountsAdded.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'No bank accounts added'**
|
||||||
|
String get noBankAccountsAdded;
|
||||||
|
|
||||||
|
/// No description provided for @commissionExcludeDiscounts.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Exclude discounts from commission'**
|
||||||
|
String get commissionExcludeDiscounts;
|
||||||
|
|
||||||
|
/// No description provided for @commissionExcludeAdditionsDeductions.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Exclude additions/deductions from commission'**
|
||||||
|
String get commissionExcludeAdditionsDeductions;
|
||||||
|
|
||||||
|
/// No description provided for @commissionPostInInvoiceDocument.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Post commission in invoice accounting document'**
|
||||||
|
String get commissionPostInInvoiceDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get export => 'Export';
|
String get export => 'Export';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFromExcel => 'Import from Excel';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get rowNumber => 'Row';
|
String get rowNumber => 'Row';
|
||||||
|
|
||||||
|
|
@ -1373,6 +1376,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get peopleList => 'People List';
|
String get peopleList => 'People List';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCode => 'Person Code';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get receipts => 'Receipts';
|
String get receipts => 'Receipts';
|
||||||
|
|
||||||
|
|
@ -2332,4 +2338,169 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get business => 'Business';
|
String get business => 'Business';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get shareCount => 'Share Count';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalePercentLabel => 'Commission Sale Percent';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesReturnPercentLabel =>
|
||||||
|
'Commission Sales Return Percent';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesAmountLabel => 'Commission Sales Amount';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesReturnAmountLabel =>
|
||||||
|
'Commission Sales Return Amount';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importPersonsFromExcel => 'Import Persons from Excel';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectedFile => 'Selected file';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noFileSelected => 'No file selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chooseFile => 'Choose file';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get matchBy => 'Match by';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get code => 'code';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get conflictPolicy => 'Conflict policy';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyInsertOnly => 'Insert-only';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyUpdateExisting => 'Update existing';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyUpsert => 'Upsert';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dryRun => 'Dry run';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dryRunValidateOnly => 'Dry run (validate only)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get downloadTemplate => 'Download template';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reviewDryRun => 'Review (Dry run)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get import => 'Import';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importReal => 'Import (real)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templateDownloaded => 'Template downloaded';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pickFileError => 'Error picking file';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templateDownloadError => 'Error downloading template';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importError => 'Import error';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get result => 'Result';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get valid => 'Valid';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalid => 'Invalid';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inserted => 'Inserted';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updated => 'Updated';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get skipped => 'Skipped';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yes => 'Yes';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get no => 'No';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get row => 'Row';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlyForMarketerSeller =>
|
||||||
|
'This section is shown only for marketer/seller';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get percentFromSales => 'Percent from sales';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get percentFromSalesReturn => 'Percent from sales return';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get salesAmount => 'Sales amount';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get salesReturnAmount => 'Sales return amount';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mustBeBetweenZeroAndHundred => 'Must be between 0 and 100';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mustBePositiveNumber => 'Must be a positive number';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCodeOptional => 'Person code (optional)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uniqueCodeNumeric => 'Unique code (numeric)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get automatic => 'Automatic';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manual => 'Manual';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCodeRequired => 'Person code is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get codeMustBeNumeric => 'Code must be numeric';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get integerNoDecimal => 'Integer number (no decimals)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get shareholderShareCountRequired =>
|
||||||
|
'For shareholder, share count is required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noBankAccountsAdded => 'No bank accounts added';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionExcludeDiscounts => 'Exclude discounts from commission';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionExcludeAdditionsDeductions =>
|
||||||
|
'Exclude additions/deductions from commission';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionPostInInvoiceDocument =>
|
||||||
|
'Post commission in invoice accounting document';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -362,7 +362,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get clear => 'پاک کردن';
|
String get clear => 'پاک کردن';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get searchInNameEmail => 'جستجو در نام، نام خانوادگی و ایمیل...';
|
String get searchInNameEmail => 'جستجو';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get recordsPerPage => 'سطر در هر صفحه';
|
String get recordsPerPage => 'سطر در هر صفحه';
|
||||||
|
|
@ -484,6 +484,9 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get export => 'خروجی';
|
String get export => 'خروجی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importFromExcel => 'ایمپورت از اکسل';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get rowNumber => 'ردیف';
|
String get rowNumber => 'ردیف';
|
||||||
|
|
||||||
|
|
@ -1362,6 +1365,9 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get peopleList => 'لیست اشخاص';
|
String get peopleList => 'لیست اشخاص';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCode => 'کد شخص';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get receipts => 'دریافتها';
|
String get receipts => 'دریافتها';
|
||||||
|
|
||||||
|
|
@ -2010,7 +2016,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get eventHistory => 'تاریخچه رویدادها';
|
String get eventHistory => 'تاریخچه رویدادها';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get viewStorage => 'مشاهده فضای ذخیرهسازی';
|
String get viewStorage => 'View Storage';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteFiles => 'فایلها';
|
String get deleteFiles => 'فایلها';
|
||||||
|
|
@ -2316,4 +2322,167 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get business => 'کسب و کار';
|
String get business => 'کسب و کار';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get shareCount => 'تعداد سهام';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalePercentLabel => 'درصد پورسانت فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesReturnPercentLabel => 'درصد پورسانت برگشت از فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesAmountLabel => 'مبلغ پورسانت فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionSalesReturnAmountLabel => 'مبلغ پورسانت برگشت از فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importPersonsFromExcel => 'ایمپورت اشخاص از اکسل';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get selectedFile => 'فایل انتخابشده';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noFileSelected => 'هیچ فایلی انتخاب نشده';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get chooseFile => 'انتخاب فایل';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get matchBy => 'معیار تطبیق';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get code => 'کد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get conflictPolicy => 'سیاست تداخل';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyInsertOnly => 'فقط ایجاد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyUpdateExisting => 'بهروزرسانی موجود';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get policyUpsert => 'آپسرت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dryRun => 'اجرای آزمایشی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get dryRunValidateOnly => 'اجرای آزمایشی (فقط اعتبارسنجی)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get downloadTemplate => 'دانلود تمپلیت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get reviewDryRun => 'بررسی (اجرای آزمایشی)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get import => 'ایمپورت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importReal => 'ایمپورت واقعی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templateDownloaded => 'تمپلیت دانلود شد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get pickFileError => 'خطا در انتخاب فایل';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get templateDownloadError => 'خطا در دانلود تمپلیت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get importError => 'خطا در ایمپورت';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get result => 'نتیجه';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get valid => 'معتبر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get invalid => 'نامعتبر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get inserted => 'ایجاد شده';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get updated => 'بهروزرسانی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get skipped => 'رد شده';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get yes => 'بله';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get no => 'خیر';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get row => 'ردیف';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get onlyForMarketerSeller =>
|
||||||
|
'این بخش فقط برای بازاریاب/فروشنده نمایش داده میشود';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get percentFromSales => 'درصد از فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get percentFromSalesReturn => 'درصد از برگشت از فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get salesAmount => 'مبلغ فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get salesReturnAmount => 'مبلغ برگشت از فروش';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mustBeBetweenZeroAndHundred => 'باید بین 0 تا 100 باشد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get mustBePositiveNumber => 'باید عدد مثبت باشد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCodeOptional => 'کد شخص (اختیاری)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uniqueCodeNumeric => 'کد یکتا (عددی)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get automatic => 'اتوماتیک';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get manual => 'دستی';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get personCodeRequired => 'کد شخص الزامی است';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get codeMustBeNumeric => 'کد باید عددی باشد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get integerNoDecimal => 'عدد صحیح بدون اعشار';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get shareholderShareCountRequired =>
|
||||||
|
'برای سهامدار، تعداد سهام الزامی است';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get noBankAccountsAdded => 'هیچ حساب بانکی اضافه نشده است';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionExcludeDiscounts => 'عدم محاسبه تخفیف';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionExcludeAdditionsDeductions =>
|
||||||
|
'عدم محاسبه اضافات و کسورات فاکتور';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get commissionPostInInvoiceDocument =>
|
||||||
|
'ثبت پورسانت در سند حسابداری فاکتور';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
columns: [
|
columns: [
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'code',
|
'code',
|
||||||
'کد شخص',
|
t.personCode,
|
||||||
width: ColumnWidth.small,
|
width: ColumnWidth.small,
|
||||||
formatter: (person) => (person.code?.toString() ?? '-'),
|
formatter: (person) => (person.code?.toString() ?? '-'),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
@ -92,6 +92,16 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
'person_type',
|
'person_type',
|
||||||
t.personType,
|
t.personType,
|
||||||
width: ColumnWidth.medium,
|
width: ColumnWidth.medium,
|
||||||
|
filterType: ColumnFilterType.multiSelect,
|
||||||
|
filterOptions: [
|
||||||
|
FilterOption(value: 'مشتری', label: t.personTypeCustomer),
|
||||||
|
FilterOption(value: 'بازاریاب', label: t.personTypeMarketer),
|
||||||
|
FilterOption(value: 'کارمند', label: t.personTypeEmployee),
|
||||||
|
FilterOption(value: 'تامینکننده', label: t.personTypeSupplier),
|
||||||
|
FilterOption(value: 'همکار', label: t.personTypePartner),
|
||||||
|
FilterOption(value: 'فروشنده', label: t.personTypeSeller),
|
||||||
|
FilterOption(value: 'سهامدار', label: 'سهامدار'),
|
||||||
|
],
|
||||||
formatter: (person) => (person.personTypes.isNotEmpty
|
formatter: (person) => (person.personTypes.isNotEmpty
|
||||||
? person.personTypes.map((e) => e.persianName).join('، ')
|
? person.personTypes.map((e) => e.persianName).join('، ')
|
||||||
: person.personType.persianName),
|
: person.personType.persianName),
|
||||||
|
|
@ -122,39 +132,39 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
),
|
),
|
||||||
DateColumn(
|
DateColumn(
|
||||||
'created_at',
|
'created_at',
|
||||||
'تاریخ ایجاد',
|
t.createdAt,
|
||||||
width: ColumnWidth.medium,
|
width: ColumnWidth.medium,
|
||||||
),
|
),
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'share_count',
|
'share_count',
|
||||||
'تعداد سهام',
|
t.shareCount,
|
||||||
width: ColumnWidth.small,
|
width: ColumnWidth.small,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
decimalPlaces: 0,
|
decimalPlaces: 0,
|
||||||
),
|
),
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'commission_sale_percent',
|
'commission_sale_percent',
|
||||||
'درصد پورسانت فروش',
|
t.commissionSalePercentLabel,
|
||||||
width: ColumnWidth.medium,
|
width: ColumnWidth.medium,
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
suffix: '٪',
|
suffix: '٪',
|
||||||
),
|
),
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'commission_sales_return_percent',
|
'commission_sales_return_percent',
|
||||||
'درصد پورسانت برگشت از فروش',
|
t.commissionSalesReturnPercentLabel,
|
||||||
width: ColumnWidth.medium,
|
width: ColumnWidth.medium,
|
||||||
decimalPlaces: 2,
|
decimalPlaces: 2,
|
||||||
suffix: '٪',
|
suffix: '٪',
|
||||||
),
|
),
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'commission_sales_amount',
|
'commission_sales_amount',
|
||||||
'مبلغ پورسانت فروش',
|
t.commissionSalesAmountLabel,
|
||||||
width: ColumnWidth.large,
|
width: ColumnWidth.large,
|
||||||
decimalPlaces: 0,
|
decimalPlaces: 0,
|
||||||
),
|
),
|
||||||
NumberColumn(
|
NumberColumn(
|
||||||
'commission_sales_return_amount',
|
'commission_sales_return_amount',
|
||||||
'مبلغ پورسانت برگشت از فروش',
|
t.commissionSalesReturnAmountLabel,
|
||||||
width: ColumnWidth.large,
|
width: ColumnWidth.large,
|
||||||
decimalPlaces: 0,
|
decimalPlaces: 0,
|
||||||
),
|
),
|
||||||
|
|
@ -226,7 +236,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
),
|
),
|
||||||
ActionColumn(
|
ActionColumn(
|
||||||
'actions',
|
'actions',
|
||||||
'عملیات',
|
t.actions,
|
||||||
actions: [
|
actions: [
|
||||||
DataTableAction(
|
DataTableAction(
|
||||||
icon: Icons.edit,
|
icon: Icons.edit,
|
||||||
|
|
@ -255,7 +265,6 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
filterFields: [
|
filterFields: [
|
||||||
'person_type',
|
'person_type',
|
||||||
'person_types',
|
'person_types',
|
||||||
'is_active',
|
|
||||||
'country',
|
'country',
|
||||||
'province',
|
'province',
|
||||||
],
|
],
|
||||||
|
|
@ -274,10 +283,12 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Tooltip(
|
Builder(builder: (context) {
|
||||||
message: 'ایمپورت اشخاص از اکسل',
|
final theme = Theme.of(context);
|
||||||
child: IconButton(
|
return Tooltip(
|
||||||
onPressed: () async {
|
message: t.importFromExcel,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
final ok = await showDialog<bool>(
|
final ok = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PersonImportDialog(businessId: widget.businessId),
|
builder: (context) => PersonImportDialog(businessId: widget.businessId),
|
||||||
|
|
@ -290,9 +301,24 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.upload_file),
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.upload_file,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'helpers/file_saver.dart';
|
import 'helpers/file_saver.dart';
|
||||||
// // import 'dart:html' as html; // Not available on Linux // Not available on Linux
|
// // import 'dart:html' as html; // Not available on Linux // Not available on Linux
|
||||||
|
|
@ -672,18 +673,24 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
|
|
||||||
// Platform-specific download functions for Linux
|
// Platform-specific download functions for Linux
|
||||||
Future<void> _downloadPdf(dynamic data, String filename) async {
|
Future<void> _downloadPdf(dynamic data, String filename) async {
|
||||||
// For Linux desktop, we'll save to Downloads folder
|
await _saveBytesToDownloads(data, filename);
|
||||||
print('Download PDF: $filename (Linux desktop - save to Downloads folder)');
|
|
||||||
// TODO: Implement proper file saving for Linux
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _downloadExcel(dynamic data, String filename) async {
|
Future<void> _downloadExcel(dynamic data, String filename) async {
|
||||||
// For Linux desktop, we'll save to Downloads folder
|
await _saveBytesToDownloads(data, filename);
|
||||||
print('Download Excel: $filename (Linux desktop - save to Downloads folder)');
|
|
||||||
// TODO: Implement proper file saving for Linux
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
double _measureTextWidth(String text, TextStyle style) {
|
||||||
|
final painter = TextPainter(
|
||||||
|
text: TextSpan(text: text, style: style),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
..layout(minWidth: 0, maxWidth: double.infinity);
|
||||||
|
return painter.width;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
final t = Localizations.of<AppLocalizations>(context, AppLocalizations)!;
|
||||||
|
|
@ -1331,6 +1338,15 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
final dataColumnsToShow = columnsToShow.where((c) => c is! ActionColumn).toList();
|
final dataColumnsToShow = columnsToShow.where((c) => c is! ActionColumn).toList();
|
||||||
|
|
||||||
columns.addAll(dataColumnsToShow.map((column) {
|
columns.addAll(dataColumnsToShow.map((column) {
|
||||||
|
final headerTextStyle = theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
) ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.w600);
|
||||||
|
final double baseWidth = DataTableUtils.getColumnWidth(column.width);
|
||||||
|
final double affordancePadding = 48.0;
|
||||||
|
final double headerTextWidth = _measureTextWidth(column.label, headerTextStyle) + affordancePadding;
|
||||||
|
final double computedWidth = math.max(baseWidth, headerTextWidth);
|
||||||
|
|
||||||
return DataColumn2(
|
return DataColumn2(
|
||||||
label: _ColumnHeaderWithSearch(
|
label: _ColumnHeaderWithSearch(
|
||||||
text: column.label,
|
text: column.label,
|
||||||
|
|
@ -1345,18 +1361,30 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
enabled: widget.config.enableSorting && column.sortable,
|
enabled: widget.config.enableSorting && column.sortable,
|
||||||
),
|
),
|
||||||
size: DataTableUtils.getColumnSize(column.width),
|
size: DataTableUtils.getColumnSize(column.width),
|
||||||
fixedWidth: DataTableUtils.getColumnWidth(column.width),
|
fixedWidth: computedWidth,
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return Scrollbar(
|
return Scrollbar(
|
||||||
controller: _horizontalScrollController,
|
controller: _horizontalScrollController,
|
||||||
thumbVisibility: true,
|
thumbVisibility: true,
|
||||||
|
child: DataTableTheme(
|
||||||
|
data: DataTableThemeData(
|
||||||
|
headingRowColor: MaterialStatePropertyAll(
|
||||||
|
theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.6),
|
||||||
|
),
|
||||||
|
headingTextStyle: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
dividerThickness: 0.6,
|
||||||
|
),
|
||||||
child: DataTable2(
|
child: DataTable2(
|
||||||
columnSpacing: 8,
|
columnSpacing: 8,
|
||||||
horizontalMargin: 8,
|
horizontalMargin: 8,
|
||||||
minWidth: widget.config.minTableWidth ?? 600,
|
minWidth: widget.config.minTableWidth ?? 600,
|
||||||
horizontalScrollController: _horizontalScrollController,
|
horizontalScrollController: _horizontalScrollController,
|
||||||
|
headingRowHeight: 44,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
rows: _items.asMap().entries.map((entry) {
|
rows: _items.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
|
|
@ -1433,6 +1461,7 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
if (hasCommissionTab) {
|
if (hasCommissionTab) {
|
||||||
tabs.add(const Tab(text: 'پورسانت'));
|
tabs.add(Tab(text: t.commissionSalePercentLabel));
|
||||||
views.add(
|
views.add(
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -467,11 +467,12 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCommissionTab() {
|
Widget _buildCommissionTab() {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
final isMarketer = _selectedPersonTypes.contains(PersonType.marketer);
|
final isMarketer = _selectedPersonTypes.contains(PersonType.marketer);
|
||||||
final isSeller = _selectedPersonTypes.contains(PersonType.seller);
|
final isSeller = _selectedPersonTypes.contains(PersonType.seller);
|
||||||
if (!isMarketer && !isSeller) {
|
if (!isMarketer && !isSeller) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text('این بخش فقط برای بازاریاب/فروشنده نمایش داده میشود'),
|
child: Text(t.onlyForMarketerSeller),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -482,8 +483,8 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _commissionSalePercentController,
|
controller: _commissionSalePercentController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'درصد از فروش',
|
labelText: t.percentFromSales,
|
||||||
suffixText: '%',
|
suffixText: '%',
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
|
@ -491,7 +492,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if ((isMarketer || isSeller) && (v != null && v.isNotEmpty)) {
|
if ((isMarketer || isSeller) && (v != null && v.isNotEmpty)) {
|
||||||
final num? val = num.tryParse(v);
|
final num? val = num.tryParse(v);
|
||||||
if (val == null || val < 0 || val > 100) return 'باید بین 0 تا 100 باشد';
|
if (val == null || val < 0 || val > 100) return t.mustBeBetweenZeroAndHundred;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -501,8 +502,8 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _commissionSalesReturnPercentController,
|
controller: _commissionSalesReturnPercentController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'درصد از برگشت از فروش',
|
labelText: t.percentFromSalesReturn,
|
||||||
suffixText: '%',
|
suffixText: '%',
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
|
@ -510,7 +511,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if ((isMarketer || isSeller) && (v != null && v.isNotEmpty)) {
|
if ((isMarketer || isSeller) && (v != null && v.isNotEmpty)) {
|
||||||
final num? val = num.tryParse(v);
|
final num? val = num.tryParse(v);
|
||||||
if (val == null || val < 0 || val > 100) return 'باید بین 0 تا 100 باشد';
|
if (val == null || val < 0 || val > 100) return t.mustBeBetweenZeroAndHundred;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -524,15 +525,15 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _commissionSalesAmountController,
|
controller: _commissionSalesAmountController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'مبلغ فروش',
|
labelText: t.salesAmount,
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))],
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))],
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v != null && v.isNotEmpty) {
|
if (v != null && v.isNotEmpty) {
|
||||||
final num? val = num.tryParse(v);
|
final num? val = num.tryParse(v);
|
||||||
if (val == null || val < 0) return 'باید عدد مثبت باشد';
|
if (val == null || val < 0) return t.mustBePositiveNumber;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -542,15 +543,15 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _commissionSalesReturnAmountController,
|
controller: _commissionSalesReturnAmountController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'مبلغ برگشت از فروش',
|
labelText: t.salesReturnAmount,
|
||||||
),
|
),
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))],
|
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.,]'))],
|
||||||
validator: (v) {
|
validator: (v) {
|
||||||
if (v != null && v.isNotEmpty) {
|
if (v != null && v.isNotEmpty) {
|
||||||
final num? val = num.tryParse(v);
|
final num? val = num.tryParse(v);
|
||||||
if (val == null || val < 0) return 'باید عدد مثبت باشد';
|
if (val == null || val < 0) return t.mustBePositiveNumber;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
@ -563,7 +564,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
title: const Text('عدم محاسبه تخفیف'),
|
title: Text(t.commissionExcludeDiscounts),
|
||||||
value: _commissionExcludeDiscounts,
|
value: _commissionExcludeDiscounts,
|
||||||
onChanged: (v) { setState(() { _commissionExcludeDiscounts = v; }); },
|
onChanged: (v) { setState(() { _commissionExcludeDiscounts = v; }); },
|
||||||
),
|
),
|
||||||
|
|
@ -571,7 +572,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SwitchListTile(
|
child: SwitchListTile(
|
||||||
title: const Text('عدم محاسبه اضافات و کسورات فاکتور'),
|
title: Text(t.commissionExcludeAdditionsDeductions),
|
||||||
value: _commissionExcludeAdditionsDeductions,
|
value: _commissionExcludeAdditionsDeductions,
|
||||||
onChanged: (v) { setState(() { _commissionExcludeAdditionsDeductions = v; }); },
|
onChanged: (v) { setState(() { _commissionExcludeAdditionsDeductions = v; }); },
|
||||||
),
|
),
|
||||||
|
|
@ -580,7 +581,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: const Text('ثبت پورسانت در سند حسابداری فاکتور'),
|
title: Text(t.commissionPostInInvoiceDocument),
|
||||||
value: _commissionPostInInvoiceDocument,
|
value: _commissionPostInInvoiceDocument,
|
||||||
onChanged: (v) { setState(() { _commissionPostInInvoiceDocument = v; }); },
|
onChanged: (v) { setState(() { _commissionPostInInvoiceDocument = v; }); },
|
||||||
),
|
),
|
||||||
|
|
@ -608,8 +609,8 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
controller: _codeController,
|
controller: _codeController,
|
||||||
readOnly: _autoGenerateCode,
|
readOnly: _autoGenerateCode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'کد شخص (اختیاری)',
|
labelText: t.personCodeOptional,
|
||||||
hintText: 'کد یکتا (عددی)',
|
hintText: t.uniqueCodeNumeric,
|
||||||
suffixIcon: Container(
|
suffixIcon: Container(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
|
|
@ -626,14 +627,14 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
_autoGenerateCode = (index == 0);
|
_autoGenerateCode = (index == 0);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
children: const [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: Text('اتوماتیک'),
|
child: Text(t.automatic),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: Text('دستی'),
|
child: Text(t.manual),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -643,10 +644,10 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (!_autoGenerateCode) {
|
if (!_autoGenerateCode) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'کد شخص الزامی است';
|
return t.personCodeRequired;
|
||||||
}
|
}
|
||||||
if (int.tryParse(value.trim()) == null) {
|
if (int.tryParse(value.trim()) == null) {
|
||||||
return 'کد باید عددی باشد';
|
return t.codeMustBeNumeric;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -684,15 +685,15 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _shareCountController,
|
controller: _shareCountController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'تعداد سهام',
|
labelText: t.shareCount,
|
||||||
hintText: 'عدد صحیح بدون اعشار',
|
hintText: t.integerNoDecimal,
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (_selectedPersonTypes.contains(PersonType.shareholder)) {
|
if (_selectedPersonTypes.contains(PersonType.shareholder)) {
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return 'برای سهامدار، تعداد سهام الزامی است';
|
return t.shareholderShareCountRequired;
|
||||||
}
|
}
|
||||||
final parsed = int.tryParse(value.trim());
|
final parsed = int.tryParse(value.trim());
|
||||||
if (parsed == null || parsed <= 0) {
|
if (parsed == null || parsed <= 0) {
|
||||||
|
|
@ -974,10 +975,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(t.noBankAccountsAdded, style: TextStyle(color: Colors.grey.shade600)),
|
||||||
'هیچ حساب بانکی اضافه نشده است',
|
|
||||||
style: TextStyle(color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -1007,10 +1005,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: bankAccount.bankName,
|
initialValue: bankAccount.bankName,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: t.bankName, hintText: t.bankName),
|
||||||
labelText: t.bankName,
|
|
||||||
hintText: t.bankName,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_updateBankAccount(index, bankAccount.copyWith(bankName: value));
|
_updateBankAccount(index, bankAccount.copyWith(bankName: value));
|
||||||
},
|
},
|
||||||
|
|
@ -1029,10 +1024,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: bankAccount.accountNumber ?? '',
|
initialValue: bankAccount.accountNumber ?? '',
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: t.accountNumber, hintText: t.accountNumber),
|
||||||
labelText: t.accountNumber,
|
|
||||||
hintText: t.accountNumber,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_updateBankAccount(index, bankAccount.copyWith(accountNumber: value.isEmpty ? null : value));
|
_updateBankAccount(index, bankAccount.copyWith(accountNumber: value.isEmpty ? null : value));
|
||||||
},
|
},
|
||||||
|
|
@ -1042,10 +1034,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: bankAccount.cardNumber ?? '',
|
initialValue: bankAccount.cardNumber ?? '',
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: t.cardNumber, hintText: t.cardNumber),
|
||||||
labelText: t.cardNumber,
|
|
||||||
hintText: t.cardNumber,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_updateBankAccount(index, bankAccount.copyWith(cardNumber: value.isEmpty ? null : value));
|
_updateBankAccount(index, bankAccount.copyWith(cardNumber: value.isEmpty ? null : value));
|
||||||
},
|
},
|
||||||
|
|
@ -1056,10 +1045,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: bankAccount.shebaNumber ?? '',
|
initialValue: bankAccount.shebaNumber ?? '',
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: t.shebaNumber, hintText: t.shebaNumber),
|
||||||
labelText: t.shebaNumber,
|
|
||||||
hintText: t.shebaNumber,
|
|
||||||
),
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_updateBankAccount(index, bankAccount.copyWith(shebaNumber: value.isEmpty ? null : value));
|
_updateBankAccount(index, bankAccount.copyWith(shebaNumber: value.isEmpty ? null : value));
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'file_picker_bridge.dart';
|
import 'file_picker_bridge.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
|
||||||
import '../../core/api_client.dart';
|
import '../../core/api_client.dart';
|
||||||
import '../data_table/helpers/file_saver.dart';
|
import '../data_table/helpers/file_saver.dart';
|
||||||
|
|
@ -45,8 +46,9 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
Future<void> _pickFile() async {
|
Future<void> _pickFile() async {
|
||||||
if (!_isInitialized) {
|
if (!_isInitialized) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('لطفاً صبر کنید تا دیالوگ کاملاً بارگذاری شود')),
|
SnackBar(content: Text(t.loading)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -62,8 +64,9 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('خطا در انتخاب فایل: $e')),
|
SnackBar(content: Text('${t.pickFileError}: $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,14 +99,16 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
}
|
}
|
||||||
await FileSaver.saveBytes((res.data as List<int>), filename);
|
await FileSaver.saveBytes((res.data as List<int>), filename);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('تمپلیت دانلود شد: $filename')),
|
SnackBar(content: Text('${t.templateDownloaded}: $filename')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('خطا در دانلود تمپلیت: $e')),
|
SnackBar(content: Text('${t.templateDownloadError}: $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -145,8 +150,9 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('خطا در ایمپورت: $e')),
|
SnackBar(content: Text('${t.importError}: $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -156,8 +162,9 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('ایمپورت اشخاص از اکسل'),
|
title: Text(t.importPersonsFromExcel),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 560,
|
width: 560,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -168,9 +175,9 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _pathCtrl,
|
controller: _pathCtrl,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'فایل انتخابشده',
|
labelText: t.selectedFile,
|
||||||
hintText: 'هیچ فایلی انتخاب نشده',
|
hintText: t.noFileSelected,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,7 +186,7 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: (_loading || !_isInitialized) ? null : _pickFile,
|
onPressed: (_loading || !_isInitialized) ? null : _pickFile,
|
||||||
icon: const Icon(Icons.attach_file),
|
icon: const Icon(Icons.attach_file),
|
||||||
label: const Text('انتخاب فایل'),
|
label: Text(t.chooseFile),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -189,10 +196,10 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _matchBy,
|
value: _matchBy,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(value: 'code', child: Text('match by: code')),
|
DropdownMenuItem(value: 'code', child: Text('${t.matchBy}: ${t.code}')),
|
||||||
DropdownMenuItem(value: 'national_id', child: Text('match by: national_id')),
|
DropdownMenuItem(value: 'national_id', child: Text('${t.matchBy}: ${t.personNationalId}')),
|
||||||
DropdownMenuItem(value: 'email', child: Text('match by: email')),
|
DropdownMenuItem(value: 'email', child: Text('${t.matchBy}: ${t.personEmail}')),
|
||||||
],
|
],
|
||||||
onChanged: (v) => setState(() => _matchBy = v ?? 'code'),
|
onChanged: (v) => setState(() => _matchBy = v ?? 'code'),
|
||||||
decoration: const InputDecoration(isDense: true),
|
decoration: const InputDecoration(isDense: true),
|
||||||
|
|
@ -203,10 +210,10 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
child: DropdownButtonFormField<String>(
|
child: DropdownButtonFormField<String>(
|
||||||
value: _conflictPolicy,
|
value: _conflictPolicy,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
items: const [
|
items: [
|
||||||
DropdownMenuItem(value: 'insert', child: Text('policy: insert-only')),
|
DropdownMenuItem(value: 'insert', child: Text('${t.conflictPolicy}: ${t.policyInsertOnly}')),
|
||||||
DropdownMenuItem(value: 'update', child: Text('policy: update-existing')),
|
DropdownMenuItem(value: 'update', child: Text('${t.conflictPolicy}: ${t.policyUpdateExisting}')),
|
||||||
DropdownMenuItem(value: 'upsert', child: Text('policy: upsert')),
|
DropdownMenuItem(value: 'upsert', child: Text('${t.conflictPolicy}: ${t.policyUpsert}')),
|
||||||
],
|
],
|
||||||
onChanged: (v) => setState(() => _conflictPolicy = v ?? 'upsert'),
|
onChanged: (v) => setState(() => _conflictPolicy = v ?? 'upsert'),
|
||||||
decoration: const InputDecoration(isDense: true),
|
decoration: const InputDecoration(isDense: true),
|
||||||
|
|
@ -221,7 +228,7 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
value: _dryRun,
|
value: _dryRun,
|
||||||
onChanged: (v) => setState(() => _dryRun = v ?? true),
|
onChanged: (v) => setState(() => _dryRun = v ?? true),
|
||||||
),
|
),
|
||||||
const Text('Dry run (فقط اعتبارسنجی)')
|
Text(t.dryRunValidateOnly)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -230,13 +237,13 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: _loading ? null : _downloadTemplate,
|
onPressed: _loading ? null : _downloadTemplate,
|
||||||
icon: const Icon(Icons.download),
|
icon: const Icon(Icons.download),
|
||||||
label: const Text('دانلود تمپلیت'),
|
label: Text(t.downloadTemplate),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: _loading ? null : () => _runImport(dryRun: _dryRun),
|
onPressed: _loading ? null : () => _runImport(dryRun: _dryRun),
|
||||||
icon: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow),
|
icon: _loading ? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.play_arrow),
|
||||||
label: Text(_dryRun ? 'بررسی (Dry run)' : 'ایمپورت'),
|
label: Text(_dryRun ? t.reviewDryRun : t.import),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
if (_dryRun)
|
if (_dryRun)
|
||||||
|
|
@ -247,7 +254,7 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
await _runImport(dryRun: false);
|
await _runImport(dryRun: false);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.cloud_upload),
|
icon: const Icon(Icons.cloud_upload),
|
||||||
label: const Text('ایمپورت واقعی'),
|
label: Text(t.importReal),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -255,7 +262,7 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Text('نتیجه:', style: Theme.of(context).textTheme.titleSmall),
|
child: Text('${t.result}:', style: Theme.of(context).textTheme.titleSmall),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_ResultSummary(result: _result!),
|
_ResultSummary(result: _result!),
|
||||||
|
|
@ -266,7 +273,7 @@ class _PersonImportDialogState extends State<PersonImportDialog> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _loading ? null : () => Navigator.of(context).pop(false),
|
onPressed: _loading ? null : () => Navigator.of(context).pop(false),
|
||||||
child: const Text('بستن'),
|
child: Text(t.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -279,6 +286,7 @@ class _ResultSummary extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
final data = result['data'] as Map<String, dynamic>?;
|
final data = result['data'] as Map<String, dynamic>?;
|
||||||
final summary = (data?['summary'] as Map<String, dynamic>?) ?? {};
|
final summary = (data?['summary'] as Map<String, dynamic>?) ?? {};
|
||||||
final errors = (data?['errors'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
|
final errors = (data?['errors'] as List?)?.cast<Map<String, dynamic>>() ?? const [];
|
||||||
|
|
@ -290,13 +298,13 @@ class _ResultSummary extends StatelessWidget {
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: [
|
children: [
|
||||||
_chip('کل', summary['total']),
|
_chip(t.total, summary['total']),
|
||||||
_chip('معتبر', summary['valid']),
|
_chip(t.valid, summary['valid']),
|
||||||
_chip('نامعتبر', summary['invalid']),
|
_chip(t.invalid, summary['invalid']),
|
||||||
_chip('ایجاد شده', summary['inserted']),
|
_chip(t.inserted, summary['inserted']),
|
||||||
_chip('بهروزرسانی', summary['updated']),
|
_chip(t.updated, summary['updated']),
|
||||||
_chip('رد شده', summary['skipped']),
|
_chip(t.skipped, summary['skipped']),
|
||||||
_chip('Dry run', summary['dry_run'] == true ? 'بله' : 'خیر'),
|
_chip(t.dryRun, summary['dry_run'] == true ? t.yes : t.no),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -310,8 +318,8 @@ class _ResultSummary extends StatelessWidget {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
leading: const Icon(Icons.error_outline, color: Colors.red),
|
leading: const Icon(Icons.error_outline, color: Colors.red),
|
||||||
title: Text('ردیف ${e['row']}'),
|
title: Text('${t.row} ${e['row']}'),
|
||||||
subtitle: Text(((e['errors'] as List?)?.join('، ')) ?? ''),
|
subtitle: Text(((e['errors'] as List?)?.join(', ')) ?? ''),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue