diff --git a/AUTH_FOOTER_INTEGRATION.md b/docs/AUTH_FOOTER_INTEGRATION.md
similarity index 100%
rename from AUTH_FOOTER_INTEGRATION.md
rename to docs/AUTH_FOOTER_INTEGRATION.md
diff --git a/CALENDAR_FEATURE_README.md b/docs/CALENDAR_FEATURE_README.md
similarity index 100%
rename from CALENDAR_FEATURE_README.md
rename to docs/CALENDAR_FEATURE_README.md
diff --git a/CALENDAR_SWITCHER_ICON_ONLY.md b/docs/CALENDAR_SWITCHER_ICON_ONLY.md
similarity index 100%
rename from CALENDAR_SWITCHER_ICON_ONLY.md
rename to docs/CALENDAR_SWITCHER_ICON_ONLY.md
diff --git a/CALENDAR_SWITCHER_MULTILINGUAL.md b/docs/CALENDAR_SWITCHER_MULTILINGUAL.md
similarity index 100%
rename from CALENDAR_SWITCHER_MULTILINGUAL.md
rename to docs/CALENDAR_SWITCHER_MULTILINGUAL.md
diff --git a/CALENDAR_SWITCHER_REDESIGN.md b/docs/CALENDAR_SWITCHER_REDESIGN.md
similarity index 100%
rename from CALENDAR_SWITCHER_REDESIGN.md
rename to docs/CALENDAR_SWITCHER_REDESIGN.md
diff --git a/CALENDAR_SWITCHER_UPDATE.md b/docs/CALENDAR_SWITCHER_UPDATE.md
similarity index 100%
rename from CALENDAR_SWITCHER_UPDATE.md
rename to docs/CALENDAR_SWITCHER_UPDATE.md
diff --git a/FLUTTER_MIRROR_SETUP.md b/docs/FLUTTER_MIRROR_SETUP.md
similarity index 100%
rename from FLUTTER_MIRROR_SETUP.md
rename to docs/FLUTTER_MIRROR_SETUP.md
diff --git a/FLUTTER_WEB_TROUBLESHOOTING.md b/docs/FLUTTER_WEB_TROUBLESHOOTING.md
similarity index 100%
rename from FLUTTER_WEB_TROUBLESHOOTING.md
rename to docs/FLUTTER_WEB_TROUBLESHOOTING.md
diff --git a/JALALI_CALENDAR_FINAL.md b/docs/JALALI_CALENDAR_FINAL.md
similarity index 100%
rename from JALALI_CALENDAR_FINAL.md
rename to docs/JALALI_CALENDAR_FINAL.md
diff --git a/JALALI_CALENDAR_IMPLEMENTATION.md b/docs/JALALI_CALENDAR_IMPLEMENTATION.md
similarity index 100%
rename from JALALI_CALENDAR_IMPLEMENTATION.md
rename to docs/JALALI_CALENDAR_IMPLEMENTATION.md
diff --git a/LOGIN_PAGE_LAYOUT_FIX.md b/docs/LOGIN_PAGE_LAYOUT_FIX.md
similarity index 100%
rename from LOGIN_PAGE_LAYOUT_FIX.md
rename to docs/LOGIN_PAGE_LAYOUT_FIX.md
diff --git a/MARKETING_CALENDAR_INTEGRATION.md b/docs/MARKETING_CALENDAR_INTEGRATION.md
similarity index 100%
rename from MARKETING_CALENDAR_INTEGRATION.md
rename to docs/MARKETING_CALENDAR_INTEGRATION.md
diff --git a/hesabixAPI/=3.1.0 b/hesabixAPI/=3.1.0
new file mode 100644
index 0000000..1b5b713
--- /dev/null
+++ b/hesabixAPI/=3.1.0
@@ -0,0 +1,9 @@
+Collecting openpyxl
+ Downloading openpyxl-3.1.5-py2.py3-none-any.whl.metadata (2.5 kB)
+Collecting et-xmlfile (from openpyxl)
+ Downloading et_xmlfile-2.0.0-py3-none-any.whl.metadata (2.7 kB)
+Downloading openpyxl-3.1.5-py2.py3-none-any.whl (250 kB)
+Downloading et_xmlfile-2.0.0-py3-none-any.whl (18 kB)
+Installing collected packages: et-xmlfile, openpyxl
+
+Successfully installed et-xmlfile-2.0.0 openpyxl-3.1.5
diff --git a/hesabixAPI/adapters/api/v1/auth.py b/hesabixAPI/adapters/api/v1/auth.py
index 1fc5be0..a2c48a6 100644
--- a/hesabixAPI/adapters/api/v1/auth.py
+++ b/hesabixAPI/adapters/api/v1/auth.py
@@ -1,13 +1,16 @@
from __future__ import annotations
+import datetime
from fastapi import APIRouter, Depends, Request
+from fastapi.responses import Response
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.responses import success_response, format_datetime_fields
from app.services.captcha_service import create_captcha
-from app.services.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats, referral_list
-from .schemas import RegisterRequest, LoginRequest, ForgotPasswordRequest, ResetPasswordRequest, ChangePasswordRequest, CreateApiKeyRequest
+from app.services.auth_service import register_user, login_user, create_password_reset, reset_password, change_password, referral_stats
+from app.services.pdf import PDFService
+from .schemas import RegisterRequest, LoginRequest, ForgotPasswordRequest, ResetPasswordRequest, ChangePasswordRequest, CreateApiKeyRequest, QueryInfo, FilterItem
from app.core.auth_dependency import get_current_user, AuthContext
from app.services.api_key_service import list_personal_keys, create_personal_key, revoke_key
@@ -96,13 +99,13 @@ def reset_password_endpoint(payload: ResetPasswordRequest, db: Session = Depends
@router.get("/api-keys", summary="List personal API keys")
-def list_keys(ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
+def list_keys(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
items = list_personal_keys(db, ctx.user.id)
return success_response(items)
@router.post("/api-keys", summary="Create personal API key")
-def create_key(payload: CreateApiKeyRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
+def create_key(request: Request, payload: CreateApiKeyRequest, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
id_, api_key = create_personal_key(db, ctx.user.id, payload.name, payload.scopes, None)
return success_response({"id": id_, "api_key": api_key})
@@ -124,13 +127,13 @@ def change_password_endpoint(request: Request, payload: ChangePasswordRequest, c
@router.delete("/api-keys/{key_id}", summary="Revoke API key")
-def delete_key(key_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
+def delete_key(request: Request, key_id: int, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
revoke_key(db, ctx.user.id, key_id)
return success_response({"ok": True})
@router.get("/referrals/stats", summary="Referral stats for current user")
-def get_referral_stats(ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), start: str | None = None, end: str | None = None) -> dict:
+def get_referral_stats(request: Request, ctx: AuthContext = Depends(get_current_user), db: Session = Depends(get_db), start: str | None = None, end: str | None = None) -> dict:
from datetime import datetime
start_dt = datetime.fromisoformat(start) if start else None
end_dt = datetime.fromisoformat(end) if end else None
@@ -138,19 +141,292 @@ def get_referral_stats(ctx: AuthContext = Depends(get_current_user), db: Session
return success_response(stats)
-@router.get("/referrals/list", summary="Referral list for current user")
-def get_referral_list(
+@router.post("/referrals/list", summary="Referral list with advanced filtering")
+def get_referral_list_advanced(
+ request: Request,
+ query_info: QueryInfo,
ctx: AuthContext = Depends(get_current_user),
- db: Session = Depends(get_db),
- start: str | None = None,
- end: str | None = None,
- search: str | None = None,
- page: int = 1,
- limit: int = 20,
+ db: Session = Depends(get_db)
) -> dict:
+ """
+ دریافت لیست معرفیها با قابلیت فیلتر پیشرفته
+
+ پارامترهای QueryInfo:
+ - sort_by: فیلد مرتبسازی (مثال: created_at, first_name, last_name, email)
+ - sort_desc: ترتیب نزولی (true/false)
+ - take: تعداد رکورد در هر صفحه (پیشفرض: 10)
+ - skip: تعداد رکورد صرفنظر شده (پیشفرض: 0)
+ - search: عبارت جستجو
+ - search_fields: فیلدهای جستجو (مثال: ["first_name", "last_name", "email"])
+ - filters: آرایه فیلترها با ساختار:
+ [
+ {
+ "property": "created_at",
+ "operator": ">=",
+ "value": "2024-01-01T00:00:00"
+ },
+ {
+ "property": "first_name",
+ "operator": "*",
+ "value": "احمد"
+ }
+ ]
+ """
+ from adapters.db.repositories.user_repo import UserRepository
+ from adapters.db.models.user import User
from datetime import datetime
- start_dt = datetime.fromisoformat(start) if start else None
- end_dt = datetime.fromisoformat(end) if end else None
- resp = referral_list(db=db, user_id=ctx.user.id, start=start_dt, end=end_dt, search=search, page=page, limit=limit)
- return success_response(resp)
+
+ # Create a custom query for referrals
+ repo = UserRepository(db)
+
+ # Add filter for referrals only (users with referred_by_user_id = current user)
+ referral_filter = FilterItem(
+ property="referred_by_user_id",
+ operator="=",
+ value=ctx.user.id
+ )
+
+ # Add referral filter to existing filters
+ if query_info.filters is None:
+ query_info.filters = [referral_filter]
+ else:
+ query_info.filters.append(referral_filter)
+
+ # Set default search fields for referrals
+ if query_info.search_fields is None:
+ query_info.search_fields = ["first_name", "last_name", "email"]
+
+ # Execute query with filters
+ referrals, total = repo.query_with_filters(query_info)
+
+ # Convert to dictionary format
+ referral_dicts = [repo.to_dict(referral) for referral in referrals]
+
+ # Format datetime fields
+ formatted_referrals = format_datetime_fields(referral_dicts, request)
+
+ # Calculate pagination info
+ page = (query_info.skip // query_info.take) + 1
+ total_pages = (total + query_info.take - 1) // query_info.take
+
+ return success_response({
+ "items": formatted_referrals,
+ "total": total,
+ "page": page,
+ "limit": query_info.take,
+ "total_pages": total_pages,
+ "has_next": page < total_pages,
+ "has_prev": page > 1
+ }, request)
+
+
+@router.post("/referrals/export/pdf", summary="Export referrals to PDF")
+def export_referrals_pdf(
+ request: Request,
+ query_info: QueryInfo,
+ selected_only: bool = False,
+ selected_indices: str | None = None,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> Response:
+ """
+ خروجی PDF لیست معرفیها
+
+ پارامترها:
+ - selected_only: آیا فقط سطرهای انتخاب شده export شوند
+ - selected_indices: لیست ایندکسهای انتخاب شده (JSON string)
+ - سایر پارامترهای QueryInfo برای فیلتر
+ """
+ from app.services.pdf import PDFService
+ from app.services.auth_service import referral_stats
+ import json
+
+ # Parse selected indices if provided
+ indices = None
+ if selected_only and selected_indices:
+ try:
+ indices = json.loads(selected_indices)
+ except (json.JSONDecodeError, TypeError):
+ indices = None
+
+ # Get stats for the report
+ stats = None
+ try:
+ # Extract date range from filters if available
+ start_date = None
+ end_date = None
+ if query_info.filters:
+ for filter_item in query_info.filters:
+ if filter_item.property == 'created_at':
+ if filter_item.operator == '>=':
+ start_date = filter_item.value
+ elif filter_item.operator == '<':
+ end_date = filter_item.value
+
+ stats = referral_stats(
+ db=db,
+ user_id=ctx.user.id,
+ start=start_date,
+ end=end_date
+ )
+ except Exception:
+ pass # Continue without stats
+
+ # Get calendar type from request headers
+ calendar_header = request.headers.get("X-Calendar-Type", "jalali")
+ calendar_type = "jalali" if calendar_header.lower() in ["jalali", "persian", "shamsi"] else "gregorian"
+
+ # Generate PDF using new modular service
+ pdf_service = PDFService()
+
+ # Get locale from request headers
+ locale_header = request.headers.get("Accept-Language", "fa")
+ locale = "fa" if locale_header.startswith("fa") else "en"
+
+ pdf_bytes = pdf_service.generate_pdf(
+ module_name='marketing',
+ data={}, # Empty data - module will fetch its own data
+ calendar_type=calendar_type,
+ locale=locale,
+ db=db,
+ user_id=ctx.user.id,
+ query_info=query_info,
+ selected_indices=indices,
+ stats=stats
+ )
+
+ # Return PDF response
+ from fastapi.responses import Response
+ import datetime
+
+ filename = f"referrals_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
+
+ return Response(
+ content=pdf_bytes,
+ media_type="application/pdf",
+ headers={
+ "Content-Disposition": f"attachment; filename={filename}",
+ "Content-Length": str(len(pdf_bytes))
+ }
+ )
+
+
+@router.post("/referrals/export/excel", summary="Export referrals to Excel")
+def export_referrals_excel(
+ request: Request,
+ query_info: QueryInfo,
+ selected_only: bool = False,
+ selected_indices: str | None = None,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> Response:
+ """
+ خروجی Excel لیست معرفیها (فایل Excel واقعی برای دانلود)
+
+ پارامترها:
+ - selected_only: آیا فقط سطرهای انتخاب شده export شوند
+ - selected_indices: لیست ایندکسهای انتخاب شده (JSON string)
+ - سایر پارامترهای QueryInfo برای فیلتر
+ """
+ from app.services.pdf import PDFService
+ import json
+ import io
+ from openpyxl import Workbook
+ from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
+
+ # Parse selected indices if provided
+ indices = None
+ if selected_only and selected_indices:
+ try:
+ indices = json.loads(selected_indices)
+ except (json.JSONDecodeError, TypeError):
+ indices = None
+
+ # Get calendar type from request headers
+ calendar_header = request.headers.get("X-Calendar-Type", "jalali")
+ calendar_type = "jalali" if calendar_header.lower() in ["jalali", "persian", "shamsi"] else "gregorian"
+
+ # Generate Excel data using new modular service
+ pdf_service = PDFService()
+
+ # Get locale from request headers
+ locale_header = request.headers.get("Accept-Language", "fa")
+ locale = "fa" if locale_header.startswith("fa") else "en"
+
+ excel_data = pdf_service.generate_excel_data(
+ module_name='marketing',
+ data={}, # Empty data - module will fetch its own data
+ calendar_type=calendar_type,
+ locale=locale,
+ db=db,
+ user_id=ctx.user.id,
+ query_info=query_info,
+ selected_indices=indices
+ )
+
+ # Create Excel workbook
+ wb = Workbook()
+ ws = wb.active
+ ws.title = "Referrals"
+
+ # Define styles
+ header_font = Font(bold=True, color="FFFFFF")
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
+ header_alignment = Alignment(horizontal="center", vertical="center")
+ border = Border(
+ left=Side(style='thin'),
+ right=Side(style='thin'),
+ top=Side(style='thin'),
+ bottom=Side(style='thin')
+ )
+
+ # Add headers
+ if excel_data:
+ headers = list(excel_data[0].keys())
+ for col, header in enumerate(headers, 1):
+ cell = ws.cell(row=1, column=col, value=header)
+ cell.font = header_font
+ cell.fill = header_fill
+ cell.alignment = header_alignment
+ cell.border = border
+
+ # Add data rows
+ for row, data in enumerate(excel_data, 2):
+ for col, header in enumerate(headers, 1):
+ cell = ws.cell(row=row, column=col, value=data.get(header, ""))
+ cell.border = border
+ # Center align for numbers and dates
+ if header in ["ردیف", "Row", "تاریخ ثبت", "Registration Date"]:
+ cell.alignment = Alignment(horizontal="center")
+
+ # Auto-adjust column widths
+ for column in ws.columns:
+ max_length = 0
+ column_letter = column[0].column_letter
+ for cell in column:
+ try:
+ if len(str(cell.value)) > max_length:
+ max_length = len(str(cell.value))
+ except:
+ pass
+ adjusted_width = min(max_length + 2, 50)
+ ws.column_dimensions[column_letter].width = adjusted_width
+
+ # Save to BytesIO
+ excel_buffer = io.BytesIO()
+ wb.save(excel_buffer)
+ excel_buffer.seek(0)
+
+ # Generate filename
+ filename = f"referrals_export_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
+
+ # Return Excel file as response
+ return Response(
+ content=excel_buffer.getvalue(),
+ media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ headers={
+ "Content-Disposition": f"attachment; filename={filename}",
+ "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ }
+ )
diff --git a/hesabixAPI/adapters/api/v1/schemas.py b/hesabixAPI/adapters/api/v1/schemas.py
index b0e9afa..07f806b 100644
--- a/hesabixAPI/adapters/api/v1/schemas.py
+++ b/hesabixAPI/adapters/api/v1/schemas.py
@@ -1,8 +1,25 @@
from __future__ import annotations
+from typing import Any
from pydantic import BaseModel, EmailStr, Field
+class FilterItem(BaseModel):
+ property: str = Field(..., description="نام فیلد مورد نظر برای اعمال فیلتر")
+ operator: str = Field(..., description="نوع عملگر: =, >, >=, <, <=, !=, *, ?*, *?, in")
+ value: Any = Field(..., description="مقدار مورد نظر")
+
+
+class QueryInfo(BaseModel):
+ sort_by: str | None = Field(default=None, description="نام فیلد مورد نظر برای مرتب سازی")
+ sort_desc: bool = Field(default=False, description="false = مرتب سازی صعودی، true = مرتب سازی نزولی")
+ take: int = Field(default=10, ge=1, le=1000, description="حداکثر تعداد رکورد بازگشتی")
+ skip: int = Field(default=0, ge=0, description="تعداد رکوردی که از ابتدای لیست صرف نظر می شود")
+ search: str | None = Field(default=None, description="عبارت جستجو")
+ search_fields: list[str] | None = Field(default=None, description="آرایه ای از فیلدهایی که جستجو در آن انجام می گیرد")
+ filters: list[FilterItem] | None = Field(default=None, description="آرایه ای از اشیا برای اعمال فیلتر بر روی لیست")
+
+
class CaptchaSolve(BaseModel):
captcha_id: str = Field(..., min_length=8)
captcha_code: str = Field(..., min_length=3, max_length=8)
diff --git a/hesabixAPI/adapters/api/v1/users.py b/hesabixAPI/adapters/api/v1/users.py
new file mode 100644
index 0000000..3e837a8
--- /dev/null
+++ b/hesabixAPI/adapters/api/v1/users.py
@@ -0,0 +1,144 @@
+from __future__ import annotations
+
+from fastapi import APIRouter, Depends, Request, Query
+from sqlalchemy.orm import Session
+
+from adapters.db.session import get_db
+from adapters.db.repositories.user_repo import UserRepository
+from adapters.api.v1.schemas import QueryInfo
+from app.core.responses import success_response, format_datetime_fields
+from app.core.auth_dependency import get_current_user, AuthContext
+
+
+router = APIRouter(prefix="/users", tags=["users"])
+
+
+@router.get("", summary="لیست کاربران با فیلتر پیشرفته")
+def list_users(
+ request: Request,
+ query_info: QueryInfo = Depends(),
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """
+ دریافت لیست کاربران با قابلیت فیلتر، جستجو، مرتبسازی و صفحهبندی
+
+ پارامترهای QueryInfo:
+ - sort_by: فیلد مرتبسازی (مثال: created_at, first_name)
+ - sort_desc: ترتیب نزولی (true/false)
+ - take: تعداد رکورد در هر صفحه (پیشفرض: 10)
+ - skip: تعداد رکورد صرفنظر شده (پیشفرض: 0)
+ - search: عبارت جستجو
+ - search_fields: فیلدهای جستجو (مثال: ["first_name", "last_name", "email"])
+ - filters: آرایه فیلترها با ساختار:
+ [
+ {
+ "property": "is_active",
+ "operator": "=",
+ "value": true
+ },
+ {
+ "property": "first_name",
+ "operator": "*",
+ "value": "احمد"
+ }
+ ]
+
+ عملگرهای پشتیبانی شده:
+ - = : برابر
+ - > : بزرگتر از
+ - >= : بزرگتر یا مساوی
+ - < : کوچکتر از
+ - <= : کوچکتر یا مساوی
+ - != : نامساوی
+ - * : شامل (contains)
+ - ?* : خاتمه یابد (ends with)
+ - *? : شروع شود (starts with)
+ - in : در بین مقادیر آرایه
+ """
+ repo = UserRepository(db)
+ users, total = repo.query_with_filters(query_info)
+
+ # تبدیل User objects به dictionary
+ user_dicts = [repo.to_dict(user) for user in users]
+
+ # فرمت کردن تاریخها
+ formatted_users = [format_datetime_fields(user_dict, request) for user_dict in user_dicts]
+
+ # محاسبه اطلاعات صفحهبندی
+ page = (query_info.skip // query_info.take) + 1
+ total_pages = (total + query_info.take - 1) // query_info.take
+
+ response_data = {
+ "items": formatted_users,
+ "pagination": {
+ "total": total,
+ "page": page,
+ "per_page": query_info.take,
+ "total_pages": total_pages,
+ "has_next": page < total_pages,
+ "has_prev": page > 1
+ },
+ "query_info": {
+ "sort_by": query_info.sort_by,
+ "sort_desc": query_info.sort_desc,
+ "search": query_info.search,
+ "search_fields": query_info.search_fields,
+ "filters": [{"property": f.property, "operator": f.operator, "value": f.value} for f in (query_info.filters or [])]
+ }
+ }
+
+ return success_response(response_data, request)
+
+
+@router.get("/{user_id}", summary="دریافت اطلاعات یک کاربر")
+def get_user(
+ user_id: int,
+ request: Request,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """دریافت اطلاعات یک کاربر بر اساس ID"""
+ repo = UserRepository(db)
+ user = repo.get_by_id(user_id)
+
+ if not user:
+ from fastapi import HTTPException
+ raise HTTPException(status_code=404, detail="کاربر یافت نشد")
+
+ user_dict = repo.to_dict(user)
+ formatted_user = format_datetime_fields(user_dict, request)
+
+ return success_response(formatted_user, request)
+
+
+@router.get("/stats/summary", summary="آمار کلی کاربران")
+def get_users_summary(
+ request: Request,
+ ctx: AuthContext = Depends(get_current_user),
+ db: Session = Depends(get_db)
+) -> dict:
+ """دریافت آمار کلی کاربران"""
+ repo = UserRepository(db)
+
+ # تعداد کل کاربران
+ total_users = repo.count_all()
+
+ # تعداد کاربران فعال
+ active_users = repo.query_with_filters(QueryInfo(
+ filters=[{"property": "is_active", "operator": "=", "value": True}]
+ ))[1]
+
+ # تعداد کاربران غیرفعال
+ inactive_users = total_users - active_users
+
+ response_data = {
+ "total_users": total_users,
+ "active_users": active_users,
+ "inactive_users": inactive_users,
+ "active_percentage": round((active_users / total_users * 100), 2) if total_users > 0 else 0
+ }
+
+ return success_response(response_data, request)
+
+
diff --git a/hesabixAPI/adapters/db/repositories/base_repo.py b/hesabixAPI/adapters/db/repositories/base_repo.py
new file mode 100644
index 0000000..2b6109f
--- /dev/null
+++ b/hesabixAPI/adapters/db/repositories/base_repo.py
@@ -0,0 +1,55 @@
+from __future__ import annotations
+
+from typing import Type, TypeVar, Generic, Any
+from sqlalchemy.orm import Session
+from sqlalchemy import select, func
+
+from app.services.query_service import QueryService
+from adapters.api.v1.schemas import QueryInfo
+
+T = TypeVar('T')
+
+
+class BaseRepository(Generic[T]):
+ """کلاس پایه برای Repository ها با قابلیت فیلتر پیشرفته"""
+
+ def __init__(self, db: Session, model_class: Type[T]) -> None:
+ self.db = db
+ self.model_class = model_class
+
+ def query_with_filters(self, query_info: QueryInfo) -> tuple[list[T], int]:
+ """
+ اجرای کوئری با فیلتر و بازگرداندن نتایج و تعداد کل
+
+ Args:
+ query_info: اطلاعات کوئری شامل فیلترها، مرتبسازی و صفحهبندی
+
+ Returns:
+ tuple: (لیست نتایج, تعداد کل رکوردها)
+ """
+ return QueryService.query_with_filters(self.model_class, self.db, query_info)
+
+ def get_by_id(self, id: int) -> T | None:
+ """دریافت رکورد بر اساس ID"""
+ stmt = select(self.model_class).where(self.model_class.id == id)
+ return self.db.execute(stmt).scalars().first()
+
+ def get_all(self, limit: int = 100, offset: int = 0) -> list[T]:
+ """دریافت تمام رکوردها با محدودیت"""
+ stmt = select(self.model_class).offset(offset).limit(limit)
+ return list(self.db.execute(stmt).scalars().all())
+
+ def count_all(self) -> int:
+ """شمارش تمام رکوردها"""
+ stmt = select(func.count()).select_from(self.model_class)
+ return int(self.db.execute(stmt).scalar() or 0)
+
+ def exists(self, **filters) -> bool:
+ """بررسی وجود رکورد بر اساس فیلترهای مشخص شده"""
+ stmt = select(self.model_class)
+ for field, value in filters.items():
+ if hasattr(self.model_class, field):
+ column = getattr(self.model_class, field)
+ stmt = stmt.where(column == value)
+
+ return self.db.execute(stmt).scalars().first() is not None
diff --git a/hesabixAPI/adapters/db/repositories/user_repo.py b/hesabixAPI/adapters/db/repositories/user_repo.py
index 28cb64b..f75faa5 100644
--- a/hesabixAPI/adapters/db/repositories/user_repo.py
+++ b/hesabixAPI/adapters/db/repositories/user_repo.py
@@ -6,11 +6,13 @@ from sqlalchemy import select, func, and_, or_
from sqlalchemy.orm import Session
from adapters.db.models.user import User
+from adapters.db.repositories.base_repo import BaseRepository
+from adapters.api.v1.schemas import QueryInfo
-class UserRepository:
+class UserRepository(BaseRepository[User]):
def __init__(self, db: Session) -> None:
- self.db = db
+ super().__init__(db, User)
def get_by_email(self, email: str) -> Optional[User]:
stmt = select(User).where(User.email == email)
@@ -71,5 +73,20 @@ class UserRepository:
stmt = stmt.where(or_(User.first_name.ilike(like), User.last_name.ilike(like), User.email.ilike(like)))
stmt = stmt.order_by(User.created_at.desc()).offset(offset).limit(limit)
return self.db.execute(stmt).scalars().all()
+
+ def to_dict(self, user: User) -> dict:
+ """تبدیل User object به dictionary برای API response"""
+ return {
+ "id": user.id,
+ "email": user.email,
+ "mobile": user.mobile,
+ "first_name": user.first_name,
+ "last_name": user.last_name,
+ "is_active": user.is_active,
+ "referral_code": user.referral_code,
+ "referred_by_user_id": user.referred_by_user_id,
+ "created_at": user.created_at,
+ "updated_at": user.updated_at,
+ }
diff --git a/hesabixAPI/app/core/auth_dependency.py b/hesabixAPI/app/core/auth_dependency.py
index da7b1a9..7ee65ab 100644
--- a/hesabixAPI/app/core/auth_dependency.py
+++ b/hesabixAPI/app/core/auth_dependency.py
@@ -1,8 +1,7 @@
from __future__ import annotations
from typing import Optional
-
-from fastapi import Depends, Header
+from fastapi import Depends, Header, Request
from sqlalchemy.orm import Session
from adapters.db.session import get_db
@@ -10,15 +9,99 @@ from adapters.db.repositories.api_key_repo import ApiKeyRepository
from adapters.db.models.user import User
from app.core.security import hash_api_key
from app.core.responses import ApiError
+from app.core.i18n import negotiate_locale, Translator
+from app.core.calendar import get_calendar_type_from_header, CalendarType
class AuthContext:
- def __init__(self, user: User, api_key_id: int) -> None:
+ """کلاس مرکزی برای نگهداری اطلاعات کاربر کنونی و تنظیمات"""
+
+ def __init__(
+ self,
+ user: User,
+ api_key_id: int,
+ language: str = "fa",
+ calendar_type: CalendarType = "jalali",
+ timezone: Optional[str] = None,
+ business_id: Optional[int] = None,
+ fiscal_year_id: Optional[int] = None
+ ) -> None:
self.user = user
self.api_key_id = api_key_id
+ self.language = language
+ self.calendar_type = calendar_type
+ self.timezone = timezone
+ self.business_id = business_id
+ self.fiscal_year_id = fiscal_year_id
+
+ # ایجاد translator برای زبان تشخیص داده شده
+ self._translator = Translator(language)
+
+ def get_translator(self) -> Translator:
+ """دریافت translator برای ترجمه"""
+ return self._translator
+
+ def get_calendar_type(self) -> CalendarType:
+ """دریافت نوع تقویم"""
+ return self.calendar_type
+
+ def get_user_id(self) -> int:
+ """دریافت ID کاربر"""
+ return self.user.id
+
+ def get_user_email(self) -> Optional[str]:
+ """دریافت ایمیل کاربر"""
+ return self.user.email
+
+ def get_user_mobile(self) -> Optional[str]:
+ """دریافت شماره موبایل کاربر"""
+ return self.user.mobile
+
+ def get_user_name(self) -> str:
+ """دریافت نام کامل کاربر"""
+ first_name = self.user.first_name or ""
+ last_name = self.user.last_name or ""
+ return f"{first_name} {last_name}".strip()
+
+ def get_referral_code(self) -> Optional[str]:
+ """دریافت کد معرف کاربر"""
+ return getattr(self.user, "referral_code", None)
+
+ def is_user_active(self) -> bool:
+ """بررسی فعال بودن کاربر"""
+ return self.user.is_active
+
+ def to_dict(self) -> dict:
+ """تبدیل به dictionary برای استفاده در API"""
+ return {
+ "user": {
+ "id": self.user.id,
+ "first_name": self.user.first_name,
+ "last_name": self.user.last_name,
+ "email": self.user.email,
+ "mobile": self.user.mobile,
+ "referral_code": getattr(self.user, "referral_code", None),
+ "is_active": self.user.is_active,
+ "created_at": self.user.created_at.isoformat() if self.user.created_at else None,
+ "updated_at": self.user.updated_at.isoformat() if self.user.updated_at else None,
+ },
+ "api_key_id": self.api_key_id,
+ "settings": {
+ "language": self.language,
+ "calendar_type": self.calendar_type,
+ "timezone": self.timezone,
+ "business_id": self.business_id,
+ "fiscal_year_id": self.fiscal_year_id,
+ }
+ }
-def get_current_user(authorization: Optional[str] = Header(default=None), db: Session = Depends(get_db)) -> AuthContext:
+def get_current_user(
+ request: Request,
+ authorization: Optional[str] = Header(default=None),
+ db: Session = Depends(get_db)
+) -> AuthContext:
+ """دریافت اطلاعات کامل کاربر کنونی و تنظیمات از درخواست"""
if not authorization or not authorization.startswith("ApiKey "):
raise ApiError("UNAUTHORIZED", "Missing or invalid API key", http_status=401)
@@ -34,6 +117,68 @@ def get_current_user(authorization: Optional[str] = Header(default=None), db: Se
if not user or not user.is_active:
raise ApiError("UNAUTHORIZED", "Invalid API key", http_status=401)
- return AuthContext(user=user, api_key_id=obj.id)
+ # تشخیص زبان از هدر Accept-Language
+ language = _detect_language(request)
+
+ # تشخیص نوع تقویم از هدر X-Calendar-Type
+ calendar_type = _detect_calendar_type(request)
+
+ # تشخیص منطقه زمانی از هدر X-Timezone (اختیاری)
+ timezone = _detect_timezone(request)
+
+ # تشخیص کسب و کار از هدر X-Business-ID (آینده)
+ business_id = _detect_business_id(request)
+
+ # تشخیص سال مالی از هدر X-Fiscal-Year-ID (آینده)
+ fiscal_year_id = _detect_fiscal_year_id(request)
+
+ return AuthContext(
+ user=user,
+ api_key_id=obj.id,
+ language=language,
+ calendar_type=calendar_type,
+ timezone=timezone,
+ business_id=business_id,
+ fiscal_year_id=fiscal_year_id
+ )
+
+
+def _detect_language(request: Request) -> str:
+ """تشخیص زبان از هدر Accept-Language"""
+ accept_language = request.headers.get("Accept-Language")
+ return negotiate_locale(accept_language)
+
+
+def _detect_calendar_type(request: Request) -> CalendarType:
+ """تشخیص نوع تقویم از هدر X-Calendar-Type"""
+ calendar_header = request.headers.get("X-Calendar-Type")
+ return get_calendar_type_from_header(calendar_header)
+
+
+def _detect_timezone(request: Request) -> Optional[str]:
+ """تشخیص منطقه زمانی از هدر X-Timezone"""
+ return request.headers.get("X-Timezone")
+
+
+def _detect_business_id(request: Request) -> Optional[int]:
+ """تشخیص ID کسب و کار از هدر X-Business-ID (آینده)"""
+ business_id_str = request.headers.get("X-Business-ID")
+ if business_id_str:
+ try:
+ return int(business_id_str)
+ except ValueError:
+ pass
+ return None
+
+
+def _detect_fiscal_year_id(request: Request) -> Optional[int]:
+ """تشخیص ID سال مالی از هدر X-Fiscal-Year-ID (آینده)"""
+ fiscal_year_id_str = request.headers.get("X-Fiscal-Year-ID")
+ if fiscal_year_id_str:
+ try:
+ return int(fiscal_year_id_str)
+ except ValueError:
+ pass
+ return None
diff --git a/hesabixAPI/app/core/calendar.py b/hesabixAPI/app/core/calendar.py
index a48de03..27c10f1 100644
--- a/hesabixAPI/app/core/calendar.py
+++ b/hesabixAPI/app/core/calendar.py
@@ -41,7 +41,7 @@ class CalendarConverter:
"date_only": jalali.strftime("%Y/%m/%d"),
"time_only": jalali.strftime("%H:%M:%S"),
"is_leap_year": jalali.isleap(),
- "month_days": jalali.days_in_month,
+ "month_days": jdatetime.j_days_in_month[jalali.month - 1],
}
@staticmethod
diff --git a/hesabixAPI/app/core/i18n.py b/hesabixAPI/app/core/i18n.py
index b38f25d..54d59ce 100644
--- a/hesabixAPI/app/core/i18n.py
+++ b/hesabixAPI/app/core/i18n.py
@@ -46,3 +46,8 @@ async def locale_dependency(request: Request) -> Translator:
return Translator(lang)
+def get_translator(locale: str = "fa") -> Translator:
+ """Get translator for the given locale"""
+ return Translator(locale)
+
+
diff --git a/hesabixAPI/app/core/responses.py b/hesabixAPI/app/core/responses.py
index 1ad10c1..c8752fc 100644
--- a/hesabixAPI/app/core/responses.py
+++ b/hesabixAPI/app/core/responses.py
@@ -29,7 +29,11 @@ def format_datetime_fields(data: Any, request: Request) -> Any:
for key, value in data.items():
if isinstance(value, datetime):
formatted_data[key] = CalendarConverter.format_datetime(value, calendar_type)
- formatted_data[f"{key}_raw"] = value.isoformat() # Keep original for reference
+ # Convert raw date to the same calendar type as the formatted date
+ if calendar_type == "jalali":
+ formatted_data[f"{key}_raw"] = CalendarConverter.to_jalali(value)["formatted"]
+ else:
+ formatted_data[f"{key}_raw"] = value.isoformat()
elif isinstance(value, (dict, list)):
formatted_data[key] = format_datetime_fields(value, request)
else:
diff --git a/hesabixAPI/app/main.py b/hesabixAPI/app/main.py
index 0dabbfd..a96093c 100644
--- a/hesabixAPI/app/main.py
+++ b/hesabixAPI/app/main.py
@@ -5,6 +5,7 @@ from app.core.settings import get_settings
from app.core.logging import configure_logging
from adapters.api.v1.health import router as health_router
from adapters.api.v1.auth import router as auth_router
+from adapters.api.v1.users import router as users_router
from app.core.i18n import negotiate_locale, Translator
from app.core.error_handlers import register_error_handlers
from app.core.smart_normalizer import smart_normalize_json, SmartNormalizerConfig
@@ -60,6 +61,7 @@ def create_app() -> FastAPI:
application.include_router(health_router, prefix=settings.api_v1_prefix)
application.include_router(auth_router, prefix=settings.api_v1_prefix)
+ application.include_router(users_router, prefix=settings.api_v1_prefix)
register_error_handlers(application)
diff --git a/hesabixAPI/app/services/pdf/README.md b/hesabixAPI/app/services/pdf/README.md
new file mode 100644
index 0000000..52962c0
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/README.md
@@ -0,0 +1,148 @@
+# PDF Service - Modular Architecture
+
+## Overview
+This is a modular PDF generation service designed for scalability and AI integration. Each module handles specific business domains and can be easily extended.
+
+## Structure
+```
+app/services/pdf/
+├── __init__.py
+├── base_pdf_service.py # Base classes and main service
+├── modules/ # Business domain modules
+│ ├── __init__.py
+│ └── marketing/ # Marketing & referrals module
+│ ├── __init__.py
+│ ├── marketing_module.py # Module implementation
+│ └── templates/ # HTML templates
+│ └── marketing_referrals.html
+└── README.md
+```
+
+## Adding New Modules
+
+### 1. Create Module Directory
+```bash
+mkdir -p app/services/pdf/modules/your_module/templates
+```
+
+### 2. Implement Module Class
+```python
+# app/services/pdf/modules/your_module/your_module.py
+from ..base_pdf_service import BasePDFModule
+from app.core.calendar import CalendarType
+
+class YourModulePDFModule(BasePDFModule):
+ def __init__(self):
+ super().__init__("your_module")
+
+ def generate_pdf(self, data, calendar_type="jalali", locale="fa"):
+ # Your PDF generation logic
+ pass
+
+ def generate_excel_data(self, data, calendar_type="jalali", locale="fa"):
+ # Your Excel data generation logic
+ pass
+```
+
+### 3. Register Module
+Add to `base_pdf_service.py`:
+```python
+def _register_modules(self):
+ from .modules.marketing.marketing_module import MarketingPDFModule
+ from .modules.your_module.your_module import YourModulePDFModule
+ self.modules['marketing'] = MarketingPDFModule()
+ self.modules['your_module'] = YourModulePDFModule()
+```
+
+## Usage
+
+### Generate PDF
+```python
+from app.services.pdf import PDFService
+
+pdf_service = PDFService()
+pdf_bytes = pdf_service.generate_pdf(
+ module_name='marketing',
+ data=your_data,
+ calendar_type='jalali',
+ locale='fa'
+)
+```
+
+### Generate Excel Data
+```python
+excel_data = pdf_service.generate_excel_data(
+ module_name='marketing',
+ data=your_data,
+ calendar_type='jalali',
+ locale='fa'
+)
+```
+
+## Features
+
+### 1. Calendar Support
+- Automatic Jalali/Gregorian conversion
+- Configurable via `calendar_type` parameter
+
+### 2. Internationalization
+- Built-in translation support
+- Template-level translation integration
+- Configurable via `locale` parameter
+
+### 3. Modular Design
+- Easy to add new business domains
+- Clean separation of concerns
+- Reusable base classes
+
+### 4. AI Integration Ready
+- Function calling compatible
+- Clear module boundaries
+- Extensible architecture
+
+## Template Development
+
+### Using Translations
+```html
+
+
{{ t('yourTranslationKey') }}
+{{ t('anotherKey') }}
+```
+
+### Calendar Formatting
+```python
+# In your module
+formatted_date = self.format_datetime(datetime_obj, calendar_type)
+```
+
+### Data Formatting
+```python
+# In your module
+formatted_data = self._format_data_for_template(data, calendar_type, translator)
+```
+
+## Future Extensions
+
+### AI Integration
+Each module can be exposed as a function for AI systems:
+```python
+# Example AI function definition
+{
+ "name": "generate_marketing_pdf",
+ "description": "Generate marketing referrals PDF report",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "user_id": {"type": "integer"},
+ "filters": {"type": "object"},
+ "calendar_type": {"type": "string", "enum": ["jalali", "gregorian"]}
+ }
+ }
+}
+```
+
+### Additional Modules
+- `invoices/` - Invoice generation
+- `reports/` - General reports
+- `analytics/` - Analytics dashboards
+- `notifications/` - Notification templates
diff --git a/hesabixAPI/app/services/pdf/__init__.py b/hesabixAPI/app/services/pdf/__init__.py
new file mode 100644
index 0000000..23f103e
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/__init__.py
@@ -0,0 +1,6 @@
+"""
+PDF Service Package
+"""
+from .base_pdf_service import PDFService
+
+__all__ = ['PDFService']
diff --git a/hesabixAPI/app/services/pdf/base_pdf_service.py b/hesabixAPI/app/services/pdf/base_pdf_service.py
new file mode 100644
index 0000000..d732522
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/base_pdf_service.py
@@ -0,0 +1,135 @@
+"""
+Base PDF Service for modular PDF generation
+"""
+import os
+from abc import ABC, abstractmethod
+from typing import Dict, Any, Optional, List
+from datetime import datetime
+from pathlib import Path
+
+from weasyprint import HTML, CSS
+from weasyprint.text.fonts import FontConfiguration
+from jinja2 import Environment, FileSystemLoader, select_autoescape
+
+from app.core.calendar import CalendarConverter, CalendarType
+from app.core.i18n import get_translator
+from adapters.api.v1.schemas import QueryInfo
+
+
+class BasePDFModule(ABC):
+ """Base class for PDF modules"""
+
+ def __init__(self, module_name: str):
+ self.module_name = module_name
+ self.template_dir = Path(__file__).parent / "modules" / module_name / "templates"
+ self.jinja_env = Environment(
+ loader=FileSystemLoader(str(self.template_dir)),
+ autoescape=select_autoescape(['html', 'xml'])
+ )
+ self.font_config = FontConfiguration()
+
+ @abstractmethod
+ def generate_pdf(
+ self,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa"
+ ) -> bytes:
+ """Generate PDF for this module"""
+ pass
+
+ @abstractmethod
+ def generate_excel_data(
+ self,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa"
+ ) -> list:
+ """Generate Excel data for this module"""
+ pass
+
+ def format_datetime(self, dt: datetime, calendar_type: CalendarType) -> str:
+ """Format datetime based on calendar type"""
+ if dt is None:
+ return ""
+
+ formatted_date = CalendarConverter.format_datetime(dt, calendar_type)
+ return formatted_date['formatted']
+
+ def get_translator(self, locale: str = "fa"):
+ """Get translator for the given locale"""
+ return get_translator(locale)
+
+ def render_template(self, template_name: str, context: Dict[str, Any]) -> str:
+ """Render template with context"""
+ template = self.jinja_env.get_template(template_name)
+ return template.render(**context)
+
+
+class PDFService:
+ """Main PDF Service that manages modules"""
+
+ def __init__(self):
+ self.modules: Dict[str, BasePDFModule] = {}
+ self._register_modules()
+
+ def _register_modules(self):
+ """Register all available modules"""
+ from .modules.marketing.marketing_module import MarketingPDFModule
+ self.modules['marketing'] = MarketingPDFModule()
+
+ def generate_pdf(
+ self,
+ module_name: str,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa",
+ db=None,
+ user_id: Optional[int] = None,
+ query_info: Optional[QueryInfo] = None,
+ selected_indices: Optional[List[int]] = None,
+ stats: Optional[Dict[str, Any]] = None
+ ) -> bytes:
+ """Generate PDF using specified module"""
+ if module_name not in self.modules:
+ raise ValueError(f"Module '{module_name}' not found")
+
+ return self.modules[module_name].generate_pdf_content(
+ db=db,
+ user_id=user_id,
+ query_info=query_info,
+ selected_indices=selected_indices,
+ stats=stats,
+ calendar_type=calendar_type,
+ locale=locale,
+ common_data=data
+ )
+
+ def generate_excel_data(
+ self,
+ module_name: str,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa",
+ db=None,
+ user_id: Optional[int] = None,
+ query_info: Optional[QueryInfo] = None,
+ selected_indices: Optional[List[int]] = None
+ ) -> list:
+ """Generate Excel data using specified module"""
+ if module_name not in self.modules:
+ raise ValueError(f"Module '{module_name}' not found")
+
+ return self.modules[module_name].generate_excel_content(
+ db=db,
+ user_id=user_id,
+ query_info=query_info,
+ selected_indices=selected_indices,
+ calendar_type=calendar_type,
+ locale=locale,
+ common_data=data
+ )
+
+ def list_modules(self) -> list:
+ """List all available modules"""
+ return list(self.modules.keys())
diff --git a/hesabixAPI/app/services/pdf/modules/__init__.py b/hesabixAPI/app/services/pdf/modules/__init__.py
new file mode 100644
index 0000000..7d39827
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/modules/__init__.py
@@ -0,0 +1,3 @@
+"""
+PDF Modules Package
+"""
diff --git a/hesabixAPI/app/services/pdf/modules/marketing/__init__.py b/hesabixAPI/app/services/pdf/modules/marketing/__init__.py
new file mode 100644
index 0000000..9088041
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/modules/marketing/__init__.py
@@ -0,0 +1,6 @@
+"""
+Marketing PDF Module
+"""
+from .marketing_module import MarketingPDFModule
+
+__all__ = ['MarketingPDFModule']
diff --git a/hesabixAPI/app/services/pdf/modules/marketing/marketing_module.py b/hesabixAPI/app/services/pdf/modules/marketing/marketing_module.py
new file mode 100644
index 0000000..e55a5b4
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/modules/marketing/marketing_module.py
@@ -0,0 +1,441 @@
+"""
+Marketing PDF Module for referrals and marketing reports
+"""
+from typing import Dict, Any, List
+from datetime import datetime
+
+from ...base_pdf_service import BasePDFModule
+from app.core.calendar import CalendarType
+
+
+class MarketingPDFModule(BasePDFModule):
+ """PDF Module for marketing and referrals"""
+
+ def __init__(self):
+ super().__init__("marketing")
+ self.template_name = 'marketing_referrals.html'
+
+ def generate_pdf(
+ self,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa"
+ ) -> bytes:
+ """Generate marketing referrals PDF"""
+ # Get translator
+ t = self.get_translator(locale)
+
+ # Format data with translations and calendar
+ formatted_data = self._format_data_for_template(data, calendar_type, t)
+
+ # Render template
+ html_content = self.render_template('marketing_referrals.html', formatted_data)
+
+ # Generate PDF
+ html_doc = HTML(string=html_content)
+ pdf_bytes = html_doc.write_pdf(font_config=self.font_config)
+
+ return pdf_bytes
+
+ def generate_excel_data(
+ self,
+ data: Dict[str, Any],
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa"
+ ) -> list:
+ """Generate marketing referrals Excel data"""
+ # Get translator
+ t = self.get_translator(locale)
+
+ # Format data
+ items = data.get('items', [])
+ excel_data = []
+
+ for i, item in enumerate(items, 1):
+ # Format created_at based on calendar type
+ created_at = item.get('created_at', '')
+ if created_at and isinstance(created_at, datetime):
+ created_at = self.format_datetime(created_at, calendar_type)
+ elif created_at and isinstance(created_at, str):
+ try:
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
+ created_at = self.format_datetime(dt, calendar_type)
+ except:
+ pass
+
+ excel_data.append({
+ t('row_number'): i,
+ t('first_name'): item.get('first_name', ''),
+ t('last_name'): item.get('last_name', ''),
+ t('email'): item.get('email', ''),
+ t('registration_date'): created_at,
+ t('referral_code'): item.get('referral_code', ''),
+ t('status'): t('active') if item.get('is_active', False) else t('inactive')
+ })
+
+ return excel_data
+
+ def _format_data_for_template(
+ self,
+ data: Dict[str, Any],
+ calendar_type: CalendarType,
+ translator
+ ) -> Dict[str, Any]:
+ """Format data for template rendering"""
+ # Format items
+ items = data.get('items', [])
+ formatted_items = []
+
+ for item in items:
+ formatted_item = item.copy()
+ if item.get('created_at'):
+ if isinstance(item['created_at'], datetime):
+ formatted_item['created_at'] = self.format_datetime(item['created_at'], calendar_type)
+ elif isinstance(item['created_at'], str):
+ try:
+ dt = datetime.fromisoformat(item['created_at'].replace('Z', '+00:00'))
+ formatted_item['created_at'] = self.format_datetime(dt, calendar_type)
+ except:
+ pass
+ formatted_items.append(formatted_item)
+
+ # Format current date
+ now = datetime.now()
+ formatted_now = self.format_datetime(now, calendar_type)
+
+ # Prepare template data with translations
+ template_data = {
+ 'items': formatted_items,
+ 'total_count': data.get('total_count', 0),
+ 'report_date': formatted_now.split(' ')[0] if ' ' in formatted_now else formatted_now,
+ 'report_time': formatted_now.split(' ')[1] if ' ' in formatted_now else '',
+ 'selected_only': data.get('selected_only', False),
+ 'stats': data.get('stats', {}),
+ 'filters': self._format_filters(data.get('filters', []), translator),
+ 'calendar_type': calendar_type,
+ 'locale': translator.locale,
+ 't': translator, # Pass translator to template
+ }
+
+ return template_data
+
+ def _format_filters(self, query_info, locale: str, calendar_type: CalendarType = "jalali") -> List[str]:
+ """Format query filters for display in PDF"""
+ formatted_filters = []
+ translator = self.get_translator(locale)
+
+ # Add search filter
+ if query_info.search and query_info.search.strip():
+ search_fields = ', '.join(query_info.search_fields) if query_info.search_fields else translator.t('allFields')
+ formatted_filters.append(f"{translator.t('search')}: '{query_info.search}' {translator.t('in')} {search_fields}")
+
+ # Add column filters
+ if query_info.filters:
+ for filter_item in query_info.filters:
+ if filter_item.property == "referred_by_user_id":
+ continue # Skip internal filter
+
+ # Get translated column name
+ column_name = self._get_column_translation(filter_item.property, translator)
+ operator_text = self._get_operator_translation(filter_item.operator, translator)
+
+ # Format value based on column type and calendar
+ formatted_value = self._format_filter_value(filter_item.property, filter_item.value, calendar_type, translator)
+
+ formatted_filters.append(f"{column_name} {operator_text} '{formatted_value}'")
+
+ return formatted_filters
+
+ def _get_operator_translation(self, op: str, translator) -> str:
+ """Convert operator to translated text"""
+ operator_map = {
+ '=': translator.t('equals'),
+ '>': translator.t('greater_than'),
+ '>=': translator.t('greater_equal'),
+ '<': translator.t('less_than'),
+ '<=': translator.t('less_equal'),
+ '!=': translator.t('not_equals'),
+ '*': translator.t('contains'),
+ '*?': translator.t('starts_with'),
+ '?*': translator.t('ends_with'),
+ 'in': translator.t('in_list')
+ }
+
+ operator_text = operator_map.get(op, op)
+ return operator_text
+
+ def _get_column_translation(self, property_name: str, translator) -> str:
+ """Get translated column name"""
+ column_map = {
+ 'first_name': translator.t('firstName'),
+ 'last_name': translator.t('lastName'),
+ 'email': translator.t('email'),
+ 'created_at': translator.t('registrationDate'),
+ 'referral_code': translator.t('referralCode'),
+ 'is_active': translator.t('status'),
+ }
+ return column_map.get(property_name, property_name)
+
+ def _format_filter_value(self, property_name: str, value: Any, calendar_type: CalendarType, translator) -> str:
+ """Format filter value based on column type and calendar"""
+ # Handle date fields
+ if property_name == 'created_at':
+ try:
+ if isinstance(value, str):
+ # Try to parse ISO format
+ dt = datetime.fromisoformat(value.replace('Z', '+00:00'))
+ elif isinstance(value, datetime):
+ dt = value
+ else:
+ return str(value)
+
+ # Format based on calendar type - only date, no time
+ from app.core.calendar import CalendarConverter
+ formatted_date = CalendarConverter.format_datetime(dt, calendar_type)
+ return formatted_date['date_only'] # Only show date, not time
+ except:
+ return str(value)
+
+ # Handle boolean fields
+ elif property_name == 'is_active':
+ if isinstance(value, bool):
+ return translator.t('active') if value else translator.t('inactive')
+ elif str(value).lower() in ['true', '1', 'yes']:
+ return translator.t('active')
+ elif str(value).lower() in ['false', '0', 'no']:
+ return translator.t('inactive')
+ else:
+ return str(value)
+
+ # Default: return as string
+ return str(value)
+
+ def _get_referral_data(self, db, user_id: int, query_info, selected_indices: List[int] | None = None) -> tuple[List[Dict[str, Any]], int]:
+ """Get referral data from database"""
+ from adapters.db.repositories.user_repo import UserRepository
+ from adapters.api.v1.schemas import FilterItem
+ from sqlalchemy.orm import Session
+ from adapters.db.models.user import User
+
+ repo = UserRepository(db)
+
+ # Add filter for referrals only (users with referred_by_user_id = current user)
+ referral_filter = FilterItem(
+ property="referred_by_user_id",
+ operator="=",
+ value=user_id
+ )
+
+ # Create a mutable copy of query_info.filters
+ current_filters = list(query_info.filters) if query_info.filters else []
+ current_filters.append(referral_filter)
+
+ # For export, we need to get all data without take limit
+ # Use the repository's direct query method
+ try:
+ # Get all referrals for the user without pagination
+ query = db.query(User).filter(User.referred_by_user_id == user_id)
+
+ # Apply search if provided
+ if query_info.search and query_info.search.strip():
+ search_term = f"%{query_info.search}%"
+ if query_info.search_fields:
+ search_conditions = []
+ for field in query_info.search_fields:
+ if hasattr(User, field):
+ search_conditions.append(getattr(User, field).ilike(search_term))
+ if search_conditions:
+ from sqlalchemy import or_
+ query = query.filter(or_(*search_conditions))
+ else:
+ # Search in common fields
+ query = query.filter(
+ (User.first_name.ilike(search_term)) |
+ (User.last_name.ilike(search_term)) |
+ (User.email.ilike(search_term))
+ )
+
+ # Apply additional filters
+ for filter_item in current_filters:
+ if filter_item.property == "referred_by_user_id":
+ continue # Already applied
+
+ if hasattr(User, filter_item.property):
+ field = getattr(User, filter_item.property)
+ if filter_item.operator == "=":
+ query = query.filter(field == filter_item.value)
+ elif filter_item.operator == "!=":
+ query = query.filter(field != filter_item.value)
+ elif filter_item.operator == ">":
+ query = query.filter(field > filter_item.value)
+ elif filter_item.operator == ">=":
+ query = query.filter(field >= filter_item.value)
+ elif filter_item.operator == "<":
+ query = query.filter(field < filter_item.value)
+ elif filter_item.operator == "<=":
+ query = query.filter(field <= filter_item.value)
+ elif filter_item.operator == "*": # contains
+ query = query.filter(field.ilike(f"%{filter_item.value}%"))
+ elif filter_item.operator == "*?": # starts with
+ query = query.filter(field.ilike(f"{filter_item.value}%"))
+ elif filter_item.operator == "?*": # ends with
+ query = query.filter(field.ilike(f"%{filter_item.value}"))
+ elif filter_item.operator == "in":
+ query = query.filter(field.in_(filter_item.value))
+
+ # Apply sorting
+ if query_info.sort_by and hasattr(User, query_info.sort_by):
+ sort_field = getattr(User, query_info.sort_by)
+ if query_info.sort_desc:
+ query = query.order_by(sort_field.desc())
+ else:
+ query = query.order_by(sort_field.asc())
+ else:
+ # Default sort by created_at desc
+ query = query.order_by(User.created_at.desc())
+
+ # Execute query
+ referrals = query.all()
+ total = len(referrals)
+ referral_dicts = [repo.to_dict(referral) for referral in referrals]
+
+ # Apply selected indices filter if provided
+ if selected_indices is not None:
+ filtered_referrals = [referral_dicts[i] for i in selected_indices if i < len(referral_dicts)]
+ return filtered_referrals, len(filtered_referrals)
+
+ return referral_dicts, total
+
+ except Exception as e:
+ print(f"Error in _get_referral_data: {e}")
+ # Fallback to repository method with max take
+ data_query_info = query_info.__class__(
+ sort_by=query_info.sort_by,
+ sort_desc=query_info.sort_desc,
+ search=query_info.search,
+ search_fields=query_info.search_fields,
+ filters=current_filters,
+ take=1000,
+ skip=0,
+ )
+
+ referrals, total = repo.query_with_filters(data_query_info)
+ referral_dicts = [repo.to_dict(referral) for referral in referrals]
+
+ if selected_indices is not None:
+ filtered_referrals = [referral_dicts[i] for i in selected_indices if i < len(referral_dicts)]
+ return filtered_referrals, len(filtered_referrals)
+
+ return referral_dicts, total
+
+ def generate_pdf_content(
+ self,
+ db,
+ user_id: int,
+ query_info,
+ selected_indices: List[int] | None = None,
+ stats: Dict[str, Any] | None = None,
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa",
+ common_data: Dict[str, Any] = None
+ ) -> bytes:
+ """Generate PDF content using the new signature"""
+ # Get referral data
+ referrals_data, total_count = self._get_referral_data(db, user_id, query_info, selected_indices)
+
+ # Format datetime fields for display in PDF
+ for item in referrals_data:
+ if 'created_at' in item and item['created_at']:
+ if isinstance(item['created_at'], datetime):
+ from app.core.calendar import CalendarConverter
+ formatted_date = CalendarConverter.format_datetime(item['created_at'], calendar_type)
+ item['formatted_created_at'] = formatted_date['formatted']
+ else:
+ try:
+ dt = datetime.fromisoformat(item['created_at'].replace('Z', '+00:00'))
+ from app.core.calendar import CalendarConverter
+ formatted_date = CalendarConverter.format_datetime(dt, calendar_type)
+ item['formatted_created_at'] = formatted_date['formatted']
+ except:
+ item['formatted_created_at'] = str(item['created_at'])
+ else:
+ item['formatted_created_at'] = '-'
+
+ # Prepare context for template
+ from app.core.calendar import CalendarConverter
+ current_time = datetime.now()
+ formatted_current_time = CalendarConverter.format_datetime(current_time, calendar_type)
+
+ context = {
+ 'items': referrals_data,
+ 'total_count': total_count,
+ 'stats': stats,
+ 'filters': self._format_filters(query_info, locale, calendar_type),
+ 'report_date': formatted_current_time['date_only'],
+ 'report_time': formatted_current_time['time_only'],
+ 'locale': locale,
+ 'selected_only': selected_indices is not None and len(selected_indices) > 0,
+ }
+
+ # Include common data if provided
+ if common_data:
+ context.update(common_data)
+
+ # Get translator
+ t = self.get_translator(locale)
+ context['t'] = t.t # Pass the t method instead of the object
+
+ # Render template
+ html_content = self.render_template(self.template_name, context)
+
+ # Generate PDF from HTML
+ from weasyprint import HTML
+ from pathlib import Path
+ pdf_file = HTML(string=html_content, base_url=str(Path(__file__).parent / "templates")).write_pdf(font_config=self.font_config)
+ return pdf_file
+
+ def generate_excel_content(
+ self,
+ db,
+ user_id: int,
+ query_info,
+ selected_indices: List[int] | None = None,
+ calendar_type: CalendarType = "jalali",
+ locale: str = "fa",
+ common_data: Dict[str, Any] = None
+ ) -> List[Dict[str, Any]]:
+ """Generate Excel content using the new signature"""
+ # Get referral data
+ referrals_data, total_count = self._get_referral_data(db, user_id, query_info, selected_indices)
+
+ # Format data for Excel with calendar support
+ excel_data = []
+ t = self.get_translator(locale)
+
+ for i, item in enumerate(referrals_data, 1):
+ # Format created_at based on calendar type
+ created_at = item.get('created_at', '')
+ if created_at and isinstance(created_at, datetime):
+ from app.core.calendar import CalendarConverter
+ formatted_date = CalendarConverter.format_datetime(created_at, calendar_type)
+ created_at = formatted_date['formatted']
+ elif created_at and isinstance(created_at, str):
+ try:
+ dt = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
+ from app.core.calendar import CalendarConverter
+ formatted_date = CalendarConverter.format_datetime(dt, calendar_type)
+ created_at = formatted_date['formatted']
+ except:
+ pass
+
+ excel_data.append({
+ t.t('rowNumber'): i,
+ t.t('firstName'): item.get('first_name', ''),
+ t.t('lastName'): item.get('last_name', ''),
+ t.t('email'): item.get('email', ''),
+ t.t('registrationDate'): created_at,
+ t.t('referralCode'): item.get('referral_code', ''),
+ t.t('status'): t.t('active') if item.get('is_active', False) else t.t('inactive')
+ })
+
+ return excel_data
diff --git a/hesabixAPI/app/services/pdf/modules/marketing/templates/marketing_referrals.html b/hesabixAPI/app/services/pdf/modules/marketing/templates/marketing_referrals.html
new file mode 100644
index 0000000..31717ba
--- /dev/null
+++ b/hesabixAPI/app/services/pdf/modules/marketing/templates/marketing_referrals.html
@@ -0,0 +1,331 @@
+
+
+
+
+
+ {{ t('marketingReport') }} - {{ t('referralList') }}
+
+
+
+
+
+ {% if stats %}
+
+
+
{{ stats.today or 0 }}
+
{{ t('today') }}
+
+
+
{{ stats.this_month or 0 }}
+
{{ t('thisMonth') }}
+
+
+
{{ stats.total or 0 }}
+
{{ t('total') }}
+
+ {% if stats.range %}
+
+
{{ stats.range }}
+
{{ t('selectedRange') }}
+
+ {% endif %}
+
+ {% endif %}
+
+
+
+
{{ t('reportDate') }}
+
{{ report_date }}
+
+
+
{{ t('totalRecords') }}
+
{{ total_count }}
+
+
+
{{ t('displayedRecords') }}
+
{{ items|length }}
+
+ {% if selected_only %}
+
+
{{ t('outputType') }}
+
{{ t('selectedOnly') }}
+
+ {% endif %}
+
+
+ {% if filters %}
+
+
{{ t('activeFilters') }}:
+ {% for filter in filters %}
+ {{ filter }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% if items %}
+
+
+
+ | {{ t('rowNumber') }} |
+ {{ t('firstName') }} |
+ {{ t('lastName') }} |
+ {{ t('email') }} |
+ {{ t('registrationDate') }} |
+
+
+
+ {% for item in items %}
+
+ | {{ loop.index }} |
+ {{ item.first_name or '-' }} |
+ {{ item.last_name or '-' }} |
+ {{ item.email or '-' }} |
+ {{ item.formatted_created_at }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
+
📊
+
{{ t('noDataFound') }}
+
+ {% endif %}
+
+
+
+
+
diff --git a/hesabixAPI/app/services/query_service.py b/hesabixAPI/app/services/query_service.py
new file mode 100644
index 0000000..59c4bb2
--- /dev/null
+++ b/hesabixAPI/app/services/query_service.py
@@ -0,0 +1,162 @@
+from __future__ import annotations
+
+from typing import Any, Type, TypeVar
+from sqlalchemy import select, func, or_, and_
+from sqlalchemy.orm import Session
+from sqlalchemy.sql import Select
+
+from adapters.api.v1.schemas import QueryInfo, FilterItem
+
+T = TypeVar('T')
+
+
+class QueryBuilder:
+ """سرویس برای ساخت کوئریهای دینامیک بر اساس QueryInfo"""
+
+ def __init__(self, model_class: Type[T], db_session: Session) -> None:
+ self.model_class = model_class
+ self.db = db_session
+ self.stmt: Select = select(model_class)
+
+ def apply_filters(self, filters: list[FilterItem] | None) -> 'QueryBuilder':
+ """اعمال فیلترها بر روی کوئری"""
+ if not filters:
+ return self
+
+ conditions = []
+ for filter_item in filters:
+ try:
+ column = getattr(self.model_class, filter_item.property)
+ condition = self._build_condition(column, filter_item.operator, filter_item.value)
+ conditions.append(condition)
+ except AttributeError:
+ # اگر فیلد وجود نداشته باشد، آن را نادیده بگیر
+ continue
+
+ if conditions:
+ self.stmt = self.stmt.where(and_(*conditions))
+
+ return self
+
+ def apply_search(self, search: str | None, search_fields: list[str] | None) -> 'QueryBuilder':
+ """اعمال جستجو بر روی فیلدهای مشخص شده"""
+ if not search or not search_fields:
+ return self
+
+ conditions = []
+ for field in search_fields:
+ try:
+ column = getattr(self.model_class, field)
+ conditions.append(column.ilike(f"%{search}%"))
+ except AttributeError:
+ # اگر فیلد وجود نداشته باشد، آن را نادیده بگیر
+ continue
+
+ if conditions:
+ self.stmt = self.stmt.where(or_(*conditions))
+
+ return self
+
+ def apply_sorting(self, sort_by: str | None, sort_desc: bool) -> 'QueryBuilder':
+ """اعمال مرتبسازی بر روی کوئری"""
+ if not sort_by:
+ return self
+
+ try:
+ column = getattr(self.model_class, sort_by)
+ if sort_desc:
+ self.stmt = self.stmt.order_by(column.desc())
+ else:
+ self.stmt = self.stmt.order_by(column.asc())
+ except AttributeError:
+ # اگر فیلد وجود نداشته باشد، مرتبسازی را نادیده بگیر
+ pass
+
+ return self
+
+ def apply_pagination(self, skip: int, take: int) -> 'QueryBuilder':
+ """اعمال صفحهبندی بر روی کوئری"""
+ self.stmt = self.stmt.offset(skip).limit(take)
+ return self
+
+ def apply_query_info(self, query_info: QueryInfo) -> 'QueryBuilder':
+ """اعمال تمام تنظیمات QueryInfo بر روی کوئری"""
+ return (self
+ .apply_filters(query_info.filters)
+ .apply_search(query_info.search, query_info.search_fields)
+ .apply_sorting(query_info.sort_by, query_info.sort_desc)
+ .apply_pagination(query_info.skip, query_info.take))
+
+ def _build_condition(self, column, operator: str, value: Any):
+ """ساخت شرط بر اساس عملگر و مقدار"""
+ if operator == "=":
+ return column == value
+ elif operator == ">":
+ return column > value
+ elif operator == ">=":
+ return column >= value
+ elif operator == "<":
+ return column < value
+ elif operator == "<=":
+ return column <= value
+ elif operator == "!=":
+ return column != value
+ elif operator == "*": # contains
+ return column.ilike(f"%{value}%")
+ elif operator == "?*": # ends with
+ return column.ilike(f"%{value}")
+ elif operator == "*?": # starts with
+ return column.ilike(f"{value}%")
+ elif operator == "in":
+ if not isinstance(value, list):
+ raise ValueError("برای عملگر 'in' مقدار باید آرایه باشد")
+ return column.in_(value)
+ else:
+ raise ValueError(f"عملگر پشتیبانی نشده: {operator}")
+
+ def get_count_query(self) -> Select:
+ """دریافت کوئری شمارش (بدون pagination)"""
+ return select(func.count()).select_from(self.stmt.subquery())
+
+ def execute(self) -> list[T]:
+ """اجرای کوئری و بازگرداندن نتایج"""
+ return list(self.db.execute(self.stmt).scalars().all())
+
+ def execute_count(self) -> int:
+ """اجرای کوئری شمارش"""
+ count_stmt = self.get_count_query()
+ return int(self.db.execute(count_stmt).scalar() or 0)
+
+
+class QueryService:
+ """سرویس اصلی برای مدیریت کوئریهای فیلتر شده"""
+
+ @staticmethod
+ def query_with_filters(
+ model_class: Type[T],
+ db: Session,
+ query_info: QueryInfo
+ ) -> tuple[list[T], int]:
+ """
+ اجرای کوئری با فیلتر و بازگرداندن نتایج و تعداد کل
+
+ Args:
+ model_class: کلاس مدل SQLAlchemy
+ db: جلسه پایگاه داده
+ query_info: اطلاعات کوئری شامل فیلترها، مرتبسازی و صفحهبندی
+
+ Returns:
+ tuple: (لیست نتایج, تعداد کل رکوردها)
+ """
+ # کوئری شمارش (بدون pagination)
+ count_builder = QueryBuilder(model_class, db)
+ count_builder.apply_filters(query_info.filters)
+ count_builder.apply_search(query_info.search, query_info.search_fields)
+ total_count = count_builder.execute_count()
+
+ # کوئری دادهها (با pagination)
+ data_builder = QueryBuilder(model_class, db)
+ data_builder.apply_query_info(query_info)
+ results = data_builder.execute()
+
+ return results, total_count
diff --git a/hesabixAPI/hesabix_api.egg-info/PKG-INFO b/hesabixAPI/hesabix_api.egg-info/PKG-INFO
index 97dc2ab..d302d60 100644
--- a/hesabixAPI/hesabix_api.egg-info/PKG-INFO
+++ b/hesabixAPI/hesabix_api.egg-info/PKG-INFO
@@ -19,6 +19,8 @@ Requires-Dist: pillow>=10.3.0
Requires-Dist: phonenumbers>=8.13.40
Requires-Dist: Babel>=2.15.0
Requires-Dist: jdatetime>=4.1.0
+Requires-Dist: weasyprint>=62.3
+Requires-Dist: jinja2>=3.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8.2.0; extra == "dev"
Requires-Dist: httpx>=0.27.0; extra == "dev"
diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
index 32b703a..9ea634e 100644
--- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
+++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt
@@ -6,6 +6,7 @@ adapters/api/v1/__init__.py
adapters/api/v1/auth.py
adapters/api/v1/health.py
adapters/api/v1/schemas.py
+adapters/api/v1/users.py
adapters/db/__init__.py
adapters/db/session.py
adapters/db/models/__init__.py
@@ -14,6 +15,7 @@ adapters/db/models/captcha.py
adapters/db/models/password_reset.py
adapters/db/models/user.py
adapters/db/repositories/api_key_repo.py
+adapters/db/repositories/base_repo.py
adapters/db/repositories/password_reset_repo.py
adapters/db/repositories/user_repo.py
app/__init__.py
@@ -33,6 +35,9 @@ app/core/smart_normalizer.py
app/services/api_key_service.py
app/services/auth_service.py
app/services/captcha_service.py
+app/services/pdf_service.py
+app/services/query_service.py
+app/services/user_context_service.py
hesabix_api.egg-info/PKG-INFO
hesabix_api.egg-info/SOURCES.txt
hesabix_api.egg-info/dependency_links.txt
diff --git a/hesabixAPI/hesabix_api.egg-info/requires.txt b/hesabixAPI/hesabix_api.egg-info/requires.txt
index cc5f92d..7c7baca 100644
--- a/hesabixAPI/hesabix_api.egg-info/requires.txt
+++ b/hesabixAPI/hesabix_api.egg-info/requires.txt
@@ -12,6 +12,8 @@ pillow>=10.3.0
phonenumbers>=8.13.40
Babel>=2.15.0
jdatetime>=4.1.0
+weasyprint>=62.3
+jinja2>=3.1.0
[dev]
pytest>=8.2.0
diff --git a/hesabixAPI/locales/en/LC_MESSAGES/messages.mo b/hesabixAPI/locales/en/LC_MESSAGES/messages.mo
index a00c3b8..57b00d3 100644
Binary files a/hesabixAPI/locales/en/LC_MESSAGES/messages.mo and b/hesabixAPI/locales/en/LC_MESSAGES/messages.mo differ
diff --git a/hesabixAPI/locales/en/LC_MESSAGES/messages.po b/hesabixAPI/locales/en/LC_MESSAGES/messages.po
index 1c7310c..88ba57d 100644
--- a/hesabixAPI/locales/en/LC_MESSAGES/messages.po
+++ b/hesabixAPI/locales/en/LC_MESSAGES/messages.po
@@ -82,3 +82,123 @@ msgstr "Gregorian"
msgid "JALALI"
msgstr "Jalali"
+
+msgid "rowNumber"
+msgstr "Row"
+
+msgid "firstName"
+msgstr "First Name"
+
+msgid "lastName"
+msgstr "Last Name"
+
+msgid "registrationDate"
+msgstr "Registration Date"
+
+msgid "selectedRange"
+msgstr "Selected Range"
+
+msgid "page"
+msgstr "Page"
+
+msgid "equals"
+msgstr "equals"
+
+msgid "greater_than"
+msgstr "greater than"
+
+msgid "greater_equal"
+msgstr "greater or equal"
+
+msgid "less_than"
+msgstr "less than"
+
+msgid "less_equal"
+msgstr "less or equal"
+
+msgid "not_equals"
+msgstr "not equals"
+
+msgid "contains"
+msgstr "contains"
+
+msgid "starts_with"
+msgstr "starts with"
+
+msgid "ends_with"
+msgstr "ends with"
+
+msgid "in_list"
+msgstr "in list"
+
+msgid "active"
+msgstr "Active"
+
+msgid "inactive"
+msgstr "Inactive"
+
+msgid "allFields"
+msgstr "All Fields"
+
+msgid "in"
+msgstr "in"
+
+msgid "reportDate"
+msgstr "Report Date"
+
+msgid "totalRecords"
+msgstr "Total Records"
+
+msgid "displayedRecords"
+msgstr "Displayed Records"
+
+msgid "outputType"
+msgstr "Output Type"
+
+msgid "selectedOnly"
+msgstr "Selected Only"
+
+msgid "reportGeneratedOn"
+msgstr "Report generated on"
+
+msgid "at"
+msgstr "at"
+
+msgid "hesabixAccountingSystem"
+msgstr "Hesabix Accounting System"
+
+msgid "marketingReport"
+msgstr "Marketing Report"
+
+msgid "referralList"
+msgstr "Referral List"
+
+msgid "thisMonth"
+msgstr "This Month"
+
+msgid "today"
+msgstr "Today"
+
+msgid "total"
+msgstr "Total"
+
+msgid "email"
+msgstr "Email"
+
+msgid "ofText"
+msgstr "of"
+
+msgid "noDataFound"
+msgstr "No data found"
+
+msgid "activeFilters"
+msgstr "Active Filters"
+
+msgid "search"
+msgstr "Search"
+
+msgid "referralCode"
+msgstr "Referral Code"
+
+msgid "status"
+msgstr "Status"
diff --git a/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo b/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo
index 5c0ba93..9e82024 100644
Binary files a/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo and b/hesabixAPI/locales/fa/LC_MESSAGES/messages.mo differ
diff --git a/hesabixAPI/locales/fa/LC_MESSAGES/messages.po b/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
index 30bc444..e0098ff 100644
--- a/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
+++ b/hesabixAPI/locales/fa/LC_MESSAGES/messages.po
@@ -83,4 +83,124 @@ msgstr "میلادی"
msgid "JALALI"
msgstr "شمسی"
+msgid "rowNumber"
+msgstr "ردیف"
+
+msgid "firstName"
+msgstr "نام"
+
+msgid "lastName"
+msgstr "نام خانوادگی"
+
+msgid "registrationDate"
+msgstr "تاریخ ثبت"
+
+msgid "selectedRange"
+msgstr "بازه انتخابی"
+
+msgid "page"
+msgstr "صفحه"
+
+msgid "equals"
+msgstr "برابر"
+
+msgid "greater_than"
+msgstr "بزرگتر از"
+
+msgid "greater_equal"
+msgstr "بزرگتر یا برابر"
+
+msgid "less_than"
+msgstr "کوچکتر از"
+
+msgid "less_equal"
+msgstr "کوچکتر یا برابر"
+
+msgid "not_equals"
+msgstr "مخالف"
+
+msgid "contains"
+msgstr "شامل"
+
+msgid "starts_with"
+msgstr "شروع با"
+
+msgid "ends_with"
+msgstr "پایان با"
+
+msgid "in_list"
+msgstr "در لیست"
+
+msgid "active"
+msgstr "فعال"
+
+msgid "inactive"
+msgstr "غیرفعال"
+
+msgid "allFields"
+msgstr "همه فیلدها"
+
+msgid "in"
+msgstr "در"
+
+msgid "reportDate"
+msgstr "تاریخ گزارش"
+
+msgid "totalRecords"
+msgstr "تعداد کل رکوردها"
+
+msgid "displayedRecords"
+msgstr "تعداد نمایش داده شده"
+
+msgid "outputType"
+msgstr "نوع خروجی"
+
+msgid "selectedOnly"
+msgstr "انتخاب شدهها"
+
+msgid "reportGeneratedOn"
+msgstr "گزارش در تاریخ"
+
+msgid "at"
+msgstr "و ساعت"
+
+msgid "hesabixAccountingSystem"
+msgstr "سیستم حسابداری حسابیکس - Hesabix Accounting System"
+
+msgid "marketingReport"
+msgstr "گزارش بازاریابی"
+
+msgid "referralList"
+msgstr "لیست معرفیها"
+
+msgid "thisMonth"
+msgstr "این ماه"
+
+msgid "today"
+msgstr "امروز"
+
+msgid "total"
+msgstr "کل"
+
+msgid "email"
+msgstr "ایمیل"
+
+msgid "ofText"
+msgstr "از"
+
+msgid "noDataFound"
+msgstr "هیچ دادهای برای نمایش وجود ندارد"
+
+msgid "activeFilters"
+msgstr "فیلترهای فعال"
+
+msgid "search"
+msgstr "جستجو"
+
+msgid "referralCode"
+msgstr "کد معرف"
+
+msgid "status"
+msgstr "وضعیت"
+
diff --git a/hesabixAPI/pyproject.toml b/hesabixAPI/pyproject.toml
index 99e931a..cf5767d 100644
--- a/hesabixAPI/pyproject.toml
+++ b/hesabixAPI/pyproject.toml
@@ -22,7 +22,10 @@ dependencies = [
"pillow>=10.3.0",
"phonenumbers>=8.13.40",
"Babel>=2.15.0",
- "jdatetime>=4.1.0"
+ "jdatetime>=4.1.0",
+ "weasyprint>=62.3",
+ "jinja2>=3.1.0",
+ "openpyxl>=3.1.0"
]
[project.optional-dependencies]
diff --git a/hesabixAPI/templates/README.md b/hesabixAPI/templates/README.md
new file mode 100644
index 0000000..09f25c1
--- /dev/null
+++ b/hesabixAPI/templates/README.md
@@ -0,0 +1,72 @@
+# PDF Templates
+
+This directory contains HTML templates for PDF generation using WeasyPrint.
+
+## Structure
+
+```
+templates/
+├── pdf/
+│ ├── marketing_referrals.html # Marketing referrals report template
+│ └── ... # Future templates
+└── README.md # This file
+```
+
+## Template Guidelines
+
+### 1. RTL Support
+- All templates should support RTL (Right-to-Left) layout
+- Use `dir="rtl"` in HTML tag
+- Use `text-align: right` in CSS
+
+### 2. Font Support
+- Use 'Vazirmatn' font for Persian text
+- Fallback to Arial, sans-serif
+- Ensure proper font loading in CSS
+
+### 3. Page Layout
+- Use `@page` CSS rule for page settings
+- Set appropriate margins (2cm recommended)
+- Include page numbers in header/footer
+
+### 4. Styling
+- Use CSS Grid or Flexbox for layouts
+- Ensure print-friendly colors
+- Use appropriate font sizes (12px base)
+- Include proper spacing and padding
+
+### 5. Data Binding
+- Use Jinja2 template syntax
+- Handle null/empty values gracefully
+- Format dates and numbers appropriately
+
+## Adding New Templates
+
+1. Create HTML file in appropriate subdirectory
+2. Follow naming convention: `{feature}_{type}.html`
+3. Include proper CSS styling
+4. Test with sample data
+5. Update PDF service to use new template
+
+## Example Usage
+
+```python
+from app.services.pdf_service import PDFService
+
+pdf_service = PDFService()
+pdf_bytes = pdf_service.generate_marketing_referrals_pdf(
+ db=db,
+ user_id=user_id,
+ query_info=query_info,
+ selected_indices=indices,
+ stats=stats
+)
+```
+
+## Future Enhancements
+
+- Template inheritance system
+- Dynamic template selection
+- Multi-language support
+- Template preview functionality
+- Template versioning
diff --git a/hesabixAPI/templates/pdf/marketing_referrals.html b/hesabixAPI/templates/pdf/marketing_referrals.html
new file mode 100644
index 0000000..8f39666
--- /dev/null
+++ b/hesabixAPI/templates/pdf/marketing_referrals.html
@@ -0,0 +1,331 @@
+
+
+
+
+
+ گزارش بازاریابی - لیست معرفیها
+
+
+
+
+
+ {% if stats %}
+
+
+
{{ stats.today or 0 }}
+
امروز
+
+
+
{{ stats.this_month or 0 }}
+
این ماه
+
+
+
{{ stats.total or 0 }}
+
کل
+
+ {% if stats.range %}
+
+
{{ stats.range }}
+
بازه انتخابی
+
+ {% endif %}
+
+ {% endif %}
+
+
+
+
تاریخ گزارش
+
{{ report_date }}
+
+
+
تعداد کل رکوردها
+
{{ total_count }}
+
+
+
تعداد نمایش داده شده
+
{{ items|length }}
+
+ {% if selected_only %}
+
+
نوع خروجی
+
انتخاب شدهها
+
+ {% endif %}
+
+
+ {% if filters %}
+
+
فیلترهای اعمال شده:
+ {% for filter in filters %}
+ {{ filter }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% if items %}
+
+
+
+ | ردیف |
+ نام |
+ نام خانوادگی |
+ ایمیل |
+ تاریخ ثبت |
+
+
+
+ {% for item in items %}
+
+ | {{ loop.index }} |
+ {{ item.first_name or '-' }} |
+ {{ item.last_name or '-' }} |
+ {{ item.email or '-' }} |
+ {{ item.created_at }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
+
📊
+
هیچ دادهای برای نمایش وجود ندارد
+
+ {% endif %}
+
+
+
+
+
diff --git a/hesabixUI/hesabix_ui/lib/core/api_client.dart b/hesabixUI/hesabix_ui/lib/core/api_client.dart
index 26bf1f1..bb5f5b3 100644
--- a/hesabixUI/hesabix_ui/lib/core/api_client.dart
+++ b/hesabixUI/hesabix_ui/lib/core/api_client.dart
@@ -97,12 +97,20 @@ class ApiClient {
return ApiClient._(dio);
}
- Future> get(String path, {Map? query, Options? options, CancelToken? cancelToken}) {
- return _dio.get(path, queryParameters: query, options: options, cancelToken: cancelToken);
+ Future> get(String path, {Map? query, Options? options, CancelToken? cancelToken, ResponseType? responseType}) {
+ final requestOptions = options ?? Options();
+ if (responseType != null) {
+ requestOptions.responseType = responseType;
+ }
+ return _dio.get(path, queryParameters: query, options: requestOptions, cancelToken: cancelToken);
}
- Future> post(String path, {Object? data, Map? query, Options? options, CancelToken? cancelToken}) {
- return _dio.post(path, data: data, queryParameters: query, options: options, cancelToken: cancelToken);
+ Future> post(String path, {Object? data, Map? query, Options? options, CancelToken? cancelToken, ResponseType? responseType}) {
+ final requestOptions = options ?? Options();
+ if (responseType != null) {
+ requestOptions.responseType = responseType;
+ }
+ return _dio.post(path, data: data, queryParameters: query, options: requestOptions, cancelToken: cancelToken);
}
Future> put(String path, {Object? data, Map? query, Options? options, CancelToken? cancelToken}) {
diff --git a/hesabixUI/hesabix_ui/lib/core/referral_store.dart b/hesabixUI/hesabix_ui/lib/core/referral_store.dart
index ce33153..1aff516 100644
--- a/hesabixUI/hesabix_ui/lib/core/referral_store.dart
+++ b/hesabixUI/hesabix_ui/lib/core/referral_store.dart
@@ -14,7 +14,7 @@ class ReferralStore {
static Future captureFromCurrentUrl() async {
try {
String? ref = Uri.base.queryParameters['ref'];
- // اگر در hash بود (مثلاً #/login?ref=CODE) از fragment بخوان
+ // اگر در hash بود (مثلاً /login?ref=CODE) از fragment بخوان
if (ref == null || ref.trim().isEmpty) {
final frag = Uri.base.fragment; // مثل '/login?ref=CODE'
if (frag.isNotEmpty) {
@@ -58,7 +58,7 @@ class ReferralStore {
static String buildInviteLink(String referralCode) {
final origin = Uri.base.origin; // دامنه پویا
// استفاده از Hash URL Strategy برای سازگاری کامل با Flutter Web
- return '$origin/#/login?ref=$referralCode';
+ return '$origin/login?ref=$referralCode';
}
static Future saveUserReferralCode(String? code) async {
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
index 5ae91f3..1f93042 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb
@@ -47,7 +47,17 @@
"menu": "Menu"
,
"ok": "OK",
- "cancel": "Cancel"
+ "cancel": "Cancel",
+ "columnSettings": "Column Settings",
+ "columnSettingsDescription": "Manage column visibility and order for this table",
+ "columnName": "Column Name",
+ "visibility": "Visibility",
+ "order": "Order",
+ "visible": "Visible",
+ "hidden": "Hidden",
+ "resetToDefaults": "Reset to Defaults",
+ "save": "Save",
+ "error": "Error"
,
"newBusiness": "New business",
"businesses": "Businesses",
@@ -80,6 +90,71 @@
"calendar": "Calendar",
"gregorian": "Gregorian",
"jalali": "Jalali",
- "calendarType": "Calendar Type"
+ "calendarType": "Calendar Type",
+ "dataLoadingError": "Error loading data",
+ "refresh": "Refresh",
+ "yourReferralLink": "Your referral link",
+ "filtersAndSearch": "Filters and search",
+ "hideFilters": "Hide filters",
+ "showFilters": "Show filters",
+ "clear": "Clear",
+ "searchInNameEmail": "Search in name, last name and email...",
+ "recordsPerPage": "Records per page",
+ "records": "records",
+ "test": "Test",
+ "user": "User",
+ "showingRecords": "Showing {start} to {end} of {total} records",
+ "previousPage": "Previous page",
+ "nextPage": "Next page",
+ "pageOf": "{current} of {total}",
+ "referralList": "Referral List",
+ "dateRangeFilter": "Date Range Filter",
+ "columnSearch": "Column Search",
+ "searchInColumn": "Search in {column}",
+ "searchType": "Search Type",
+ "contains": "Contains",
+ "startsWith": "Starts With",
+ "endsWith": "Ends With",
+ "exactMatch": "Exact Match",
+ "searchValue": "Search Value",
+ "applyColumnFilter": "Apply Column Filter",
+ "clearColumnFilter": "Clear Column Filter",
+ "activeFilters": "Active Filters",
+ "selectDate": "Select Date",
+ "noDataFound": "No data found",
+ "marketingReportSubtitle": "Manage and analyze user referrals",
+ "showing": "Showing",
+ "to": "to",
+ "ofText": "of",
+ "results": "results",
+ "recordsPerPage": "Records per page",
+ "firstPage": "First page",
+ "previousPage": "Previous page",
+ "nextPage": "Next page",
+ "lastPage": "Last page",
+ "exportToExcel": "Export to Excel",
+ "exportToPdf": "Export to PDF",
+ "exportSelected": "Export Selected",
+ "exportAll": "Export All",
+ "exporting": "Exporting...",
+ "exportSuccess": "Export completed successfully",
+ "exportError": "Export error",
+ "export": "Export",
+ "rowNumber": "Row",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "registrationDate": "Registration Date",
+ "selectedRange": "Selected Range",
+ "page": "Page",
+ "equals": "equals",
+ "greater_than": "greater than",
+ "greater_equal": "greater or equal",
+ "less_than": "less than",
+ "less_equal": "less or equal",
+ "not_equals": "not equals",
+ "contains": "contains",
+ "starts_with": "starts with",
+ "ends_with": "ends with",
+ "in_list": "in list"
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
index 3c0b126..a1d9697 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb
@@ -52,7 +52,17 @@
"changePassword": "تغییر کلمه عبور",
"marketing": "بازاریابی",
"ok": "تایید",
- "cancel": "انصراف"
+ "cancel": "انصراف",
+ "columnSettings": "تنظیمات ستونها",
+ "columnSettingsDescription": "مدیریت نمایش و ترتیب ستونهای این جدول",
+ "columnName": "نام ستون",
+ "visibility": "نمایش",
+ "order": "ترتیب",
+ "visible": "نمایش",
+ "hidden": "مخفی",
+ "resetToDefaults": "بازگردانی به پیشفرض",
+ "save": "ذخیره",
+ "error": "خطا"
,
"marketingReport": "گزارش بازاریابی",
"today": "امروز",
@@ -79,6 +89,71 @@
"calendar": "تقویم",
"gregorian": "میلادی",
"jalali": "شمسی",
- "calendarType": "نوع تقویم"
+ "calendarType": "نوع تقویم",
+ "dataLoadingError": "خطا در بارگذاری دادهها",
+ "refresh": "بروزرسانی",
+ "yourReferralLink": "لینک معرفی شما",
+ "filtersAndSearch": "فیلترها و جستجو",
+ "hideFilters": "مخفی کردن فیلترها",
+ "showFilters": "نمایش فیلترها",
+ "clear": "پاک کردن",
+ "searchInNameEmail": "جستجو در نام، نام خانوادگی و ایمیل...",
+ "recordsPerPage": "تعداد در صفحه",
+ "records": "رکورد",
+ "test": "تست",
+ "user": "کاربر",
+ "showingRecords": "نمایش {start} تا {end} از {total} رکورد",
+ "previousPage": "صفحه قبل",
+ "nextPage": "صفحه بعد",
+ "pageOf": "{current} از {total}",
+ "referralList": "لیست معرفیها",
+ "dateRangeFilter": "فیلتر بازه زمانی",
+ "columnSearch": "جستجو در ستون",
+ "searchInColumn": "جستجو در {column}",
+ "searchType": "نوع جستجو",
+ "contains": "شامل",
+ "startsWith": "شروع با",
+ "endsWith": "خاتمه با",
+ "exactMatch": "مطابقت دقیق",
+ "searchValue": "مقدار جستجو",
+ "applyColumnFilter": "اعمال فیلتر ستون",
+ "clearColumnFilter": "پاک کردن فیلتر ستون",
+ "activeFilters": "فیلترهای فعال",
+ "selectDate": "انتخاب تاریخ",
+ "noDataFound": "هیچ دادهای یافت نشد",
+ "marketingReportSubtitle": "مدیریت و تحلیل معرفیهای کاربران",
+ "showing": "نمایش",
+ "to": "تا",
+ "ofText": "از",
+ "results": "نتیجه",
+ "recordsPerPage": "سطر در هر صفحه",
+ "firstPage": "صفحه اول",
+ "previousPage": "صفحه قبل",
+ "nextPage": "صفحه بعد",
+ "lastPage": "صفحه آخر",
+ "exportToExcel": "خروجی اکسل",
+ "exportToPdf": "خروجی PDF",
+ "exportSelected": "خروجی انتخاب شدهها",
+ "exportAll": "خروجی همه",
+ "exporting": "در حال خروجی...",
+ "exportSuccess": "خروجی با موفقیت انجام شد",
+ "exportError": "خطا در خروجی",
+ "export": "خروجی",
+ "rowNumber": "ردیف",
+ "firstName": "نام",
+ "lastName": "نام خانوادگی",
+ "registrationDate": "تاریخ ثبت",
+ "selectedRange": "بازه انتخابی",
+ "page": "صفحه",
+ "equals": "برابر",
+ "greater_than": "بزرگتر از",
+ "greater_equal": "بزرگتر یا برابر",
+ "less_than": "کوچکتر از",
+ "less_equal": "کوچکتر یا برابر",
+ "not_equals": "مخالف",
+ "contains": "شامل",
+ "starts_with": "شروع با",
+ "ends_with": "پایان با",
+ "in_list": "در لیست"
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
index cbf5268..7b1963d 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart
@@ -167,13 +167,13 @@ abstract class AppLocalizations {
/// No description provided for @firstName.
///
/// In en, this message translates to:
- /// **'First name'**
+ /// **'First Name'**
String get firstName;
/// No description provided for @lastName.
///
/// In en, this message translates to:
- /// **'Last name'**
+ /// **'Last Name'**
String get lastName;
/// No description provided for @email.
@@ -350,6 +350,66 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get cancel;
+ /// No description provided for @columnSettings.
+ ///
+ /// In en, this message translates to:
+ /// **'Column Settings'**
+ String get columnSettings;
+
+ /// No description provided for @columnSettingsDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Manage column visibility and order for this table'**
+ String get columnSettingsDescription;
+
+ /// No description provided for @columnName.
+ ///
+ /// In en, this message translates to:
+ /// **'Column Name'**
+ String get columnName;
+
+ /// No description provided for @visibility.
+ ///
+ /// In en, this message translates to:
+ /// **'Visibility'**
+ String get visibility;
+
+ /// No description provided for @order.
+ ///
+ /// In en, this message translates to:
+ /// **'Order'**
+ String get order;
+
+ /// No description provided for @visible.
+ ///
+ /// In en, this message translates to:
+ /// **'Visible'**
+ String get visible;
+
+ /// No description provided for @hidden.
+ ///
+ /// In en, this message translates to:
+ /// **'Hidden'**
+ String get hidden;
+
+ /// No description provided for @resetToDefaults.
+ ///
+ /// In en, this message translates to:
+ /// **'Reset to Defaults'**
+ String get resetToDefaults;
+
+ /// No description provided for @save.
+ ///
+ /// In en, this message translates to:
+ /// **'Save'**
+ String get save;
+
+ /// No description provided for @error.
+ ///
+ /// In en, this message translates to:
+ /// **'Error'**
+ String get error;
+
/// No description provided for @newBusiness.
///
/// In en, this message translates to:
@@ -535,6 +595,354 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Calendar Type'**
String get calendarType;
+
+ /// No description provided for @dataLoadingError.
+ ///
+ /// In en, this message translates to:
+ /// **'Error loading data'**
+ String get dataLoadingError;
+
+ /// No description provided for @yourReferralLink.
+ ///
+ /// In en, this message translates to:
+ /// **'Your referral link'**
+ String get yourReferralLink;
+
+ /// No description provided for @filtersAndSearch.
+ ///
+ /// In en, this message translates to:
+ /// **'Filters and search'**
+ String get filtersAndSearch;
+
+ /// No description provided for @hideFilters.
+ ///
+ /// In en, this message translates to:
+ /// **'Hide filters'**
+ String get hideFilters;
+
+ /// No description provided for @showFilters.
+ ///
+ /// In en, this message translates to:
+ /// **'Show filters'**
+ String get showFilters;
+
+ /// No description provided for @clear.
+ ///
+ /// In en, this message translates to:
+ /// **'Clear'**
+ String get clear;
+
+ /// No description provided for @searchInNameEmail.
+ ///
+ /// In en, this message translates to:
+ /// **'Search in name, last name and email...'**
+ String get searchInNameEmail;
+
+ /// No description provided for @recordsPerPage.
+ ///
+ /// In en, this message translates to:
+ /// **'Records per page'**
+ String get recordsPerPage;
+
+ /// No description provided for @records.
+ ///
+ /// In en, this message translates to:
+ /// **'records'**
+ String get records;
+
+ /// No description provided for @test.
+ ///
+ /// In en, this message translates to:
+ /// **'Test'**
+ String get test;
+
+ /// No description provided for @user.
+ ///
+ /// In en, this message translates to:
+ /// **'User'**
+ String get user;
+
+ /// No description provided for @showingRecords.
+ ///
+ /// In en, this message translates to:
+ /// **'Showing {start} to {end} of {total} records'**
+ String showingRecords(Object end, Object start, Object total);
+
+ /// No description provided for @previousPage.
+ ///
+ /// In en, this message translates to:
+ /// **'Previous page'**
+ String get previousPage;
+
+ /// No description provided for @nextPage.
+ ///
+ /// In en, this message translates to:
+ /// **'Next page'**
+ String get nextPage;
+
+ /// No description provided for @pageOf.
+ ///
+ /// In en, this message translates to:
+ /// **'{current} of {total}'**
+ String pageOf(Object current, Object total);
+
+ /// No description provided for @referralList.
+ ///
+ /// In en, this message translates to:
+ /// **'Referral List'**
+ String get referralList;
+
+ /// No description provided for @dateRangeFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Date Range Filter'**
+ String get dateRangeFilter;
+
+ /// No description provided for @columnSearch.
+ ///
+ /// In en, this message translates to:
+ /// **'Column Search'**
+ String get columnSearch;
+
+ /// No description provided for @searchInColumn.
+ ///
+ /// In en, this message translates to:
+ /// **'Search in {column}'**
+ String searchInColumn(Object column);
+
+ /// No description provided for @searchType.
+ ///
+ /// In en, this message translates to:
+ /// **'Search Type'**
+ String get searchType;
+
+ /// No description provided for @contains.
+ ///
+ /// In en, this message translates to:
+ /// **'contains'**
+ String get contains;
+
+ /// No description provided for @startsWith.
+ ///
+ /// In en, this message translates to:
+ /// **'Starts With'**
+ String get startsWith;
+
+ /// No description provided for @endsWith.
+ ///
+ /// In en, this message translates to:
+ /// **'Ends With'**
+ String get endsWith;
+
+ /// No description provided for @exactMatch.
+ ///
+ /// In en, this message translates to:
+ /// **'Exact Match'**
+ String get exactMatch;
+
+ /// No description provided for @searchValue.
+ ///
+ /// In en, this message translates to:
+ /// **'Search Value'**
+ String get searchValue;
+
+ /// No description provided for @applyColumnFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Apply Column Filter'**
+ String get applyColumnFilter;
+
+ /// No description provided for @clearColumnFilter.
+ ///
+ /// In en, this message translates to:
+ /// **'Clear Column Filter'**
+ String get clearColumnFilter;
+
+ /// No description provided for @activeFilters.
+ ///
+ /// In en, this message translates to:
+ /// **'Active Filters'**
+ String get activeFilters;
+
+ /// No description provided for @selectDate.
+ ///
+ /// In en, this message translates to:
+ /// **'Select Date'**
+ String get selectDate;
+
+ /// No description provided for @noDataFound.
+ ///
+ /// In en, this message translates to:
+ /// **'No data found'**
+ String get noDataFound;
+
+ /// No description provided for @marketingReportSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Manage and analyze user referrals'**
+ String get marketingReportSubtitle;
+
+ /// No description provided for @showing.
+ ///
+ /// In en, this message translates to:
+ /// **'Showing'**
+ String get showing;
+
+ /// No description provided for @to.
+ ///
+ /// In en, this message translates to:
+ /// **'to'**
+ String get to;
+
+ /// No description provided for @ofText.
+ ///
+ /// In en, this message translates to:
+ /// **'of'**
+ String get ofText;
+
+ /// No description provided for @results.
+ ///
+ /// In en, this message translates to:
+ /// **'results'**
+ String get results;
+
+ /// No description provided for @firstPage.
+ ///
+ /// In en, this message translates to:
+ /// **'First page'**
+ String get firstPage;
+
+ /// No description provided for @lastPage.
+ ///
+ /// In en, this message translates to:
+ /// **'Last page'**
+ String get lastPage;
+
+ /// No description provided for @exportToExcel.
+ ///
+ /// In en, this message translates to:
+ /// **'Export to Excel'**
+ String get exportToExcel;
+
+ /// No description provided for @exportToPdf.
+ ///
+ /// In en, this message translates to:
+ /// **'Export to PDF'**
+ String get exportToPdf;
+
+ /// No description provided for @exportSelected.
+ ///
+ /// In en, this message translates to:
+ /// **'Export Selected'**
+ String get exportSelected;
+
+ /// No description provided for @exportAll.
+ ///
+ /// In en, this message translates to:
+ /// **'Export All'**
+ String get exportAll;
+
+ /// No description provided for @exporting.
+ ///
+ /// In en, this message translates to:
+ /// **'Exporting...'**
+ String get exporting;
+
+ /// No description provided for @exportSuccess.
+ ///
+ /// In en, this message translates to:
+ /// **'Export completed successfully'**
+ String get exportSuccess;
+
+ /// No description provided for @exportError.
+ ///
+ /// In en, this message translates to:
+ /// **'Export error'**
+ String get exportError;
+
+ /// No description provided for @export.
+ ///
+ /// In en, this message translates to:
+ /// **'Export'**
+ String get export;
+
+ /// No description provided for @rowNumber.
+ ///
+ /// In en, this message translates to:
+ /// **'Row'**
+ String get rowNumber;
+
+ /// No description provided for @registrationDate.
+ ///
+ /// In en, this message translates to:
+ /// **'Registration Date'**
+ String get registrationDate;
+
+ /// No description provided for @selectedRange.
+ ///
+ /// In en, this message translates to:
+ /// **'Selected Range'**
+ String get selectedRange;
+
+ /// No description provided for @page.
+ ///
+ /// In en, this message translates to:
+ /// **'Page'**
+ String get page;
+
+ /// No description provided for @equals.
+ ///
+ /// In en, this message translates to:
+ /// **'equals'**
+ String get equals;
+
+ /// No description provided for @greater_than.
+ ///
+ /// In en, this message translates to:
+ /// **'greater than'**
+ String get greater_than;
+
+ /// No description provided for @greater_equal.
+ ///
+ /// In en, this message translates to:
+ /// **'greater or equal'**
+ String get greater_equal;
+
+ /// No description provided for @less_than.
+ ///
+ /// In en, this message translates to:
+ /// **'less than'**
+ String get less_than;
+
+ /// No description provided for @less_equal.
+ ///
+ /// In en, this message translates to:
+ /// **'less or equal'**
+ String get less_equal;
+
+ /// No description provided for @not_equals.
+ ///
+ /// In en, this message translates to:
+ /// **'not equals'**
+ String get not_equals;
+
+ /// No description provided for @starts_with.
+ ///
+ /// In en, this message translates to:
+ /// **'starts with'**
+ String get starts_with;
+
+ /// No description provided for @ends_with.
+ ///
+ /// In en, this message translates to:
+ /// **'ends with'**
+ String get ends_with;
+
+ /// No description provided for @in_list.
+ ///
+ /// In en, this message translates to:
+ /// **'in list'**
+ String get in_list;
}
class _AppLocalizationsDelegate
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
index 834f399..2510aaf 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart
@@ -42,10 +42,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get forgotPassword => 'Forgot password';
@override
- String get firstName => 'First name';
+ String get firstName => 'First Name';
@override
- String get lastName => 'Last name';
+ String get lastName => 'Last Name';
@override
String get email => 'Email';
@@ -136,6 +136,37 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get cancel => 'Cancel';
+ @override
+ String get columnSettings => 'Column Settings';
+
+ @override
+ String get columnSettingsDescription =>
+ 'Manage column visibility and order for this table';
+
+ @override
+ String get columnName => 'Column Name';
+
+ @override
+ String get visibility => 'Visibility';
+
+ @override
+ String get order => 'Order';
+
+ @override
+ String get visible => 'Visible';
+
+ @override
+ String get hidden => 'Hidden';
+
+ @override
+ String get resetToDefaults => 'Reset to Defaults';
+
+ @override
+ String get save => 'Save';
+
+ @override
+ String get error => 'Error';
+
@override
String get newBusiness => 'New business';
@@ -232,4 +263,184 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get calendarType => 'Calendar Type';
+
+ @override
+ String get dataLoadingError => 'Error loading data';
+
+ @override
+ String get yourReferralLink => 'Your referral link';
+
+ @override
+ String get filtersAndSearch => 'Filters and search';
+
+ @override
+ String get hideFilters => 'Hide filters';
+
+ @override
+ String get showFilters => 'Show filters';
+
+ @override
+ String get clear => 'Clear';
+
+ @override
+ String get searchInNameEmail => 'Search in name, last name and email...';
+
+ @override
+ String get recordsPerPage => 'Records per page';
+
+ @override
+ String get records => 'records';
+
+ @override
+ String get test => 'Test';
+
+ @override
+ String get user => 'User';
+
+ @override
+ String showingRecords(Object end, Object start, Object total) {
+ return 'Showing $start to $end of $total records';
+ }
+
+ @override
+ String get previousPage => 'Previous page';
+
+ @override
+ String get nextPage => 'Next page';
+
+ @override
+ String pageOf(Object current, Object total) {
+ return '$current of $total';
+ }
+
+ @override
+ String get referralList => 'Referral List';
+
+ @override
+ String get dateRangeFilter => 'Date Range Filter';
+
+ @override
+ String get columnSearch => 'Column Search';
+
+ @override
+ String searchInColumn(Object column) {
+ return 'Search in $column';
+ }
+
+ @override
+ String get searchType => 'Search Type';
+
+ @override
+ String get contains => 'contains';
+
+ @override
+ String get startsWith => 'Starts With';
+
+ @override
+ String get endsWith => 'Ends With';
+
+ @override
+ String get exactMatch => 'Exact Match';
+
+ @override
+ String get searchValue => 'Search Value';
+
+ @override
+ String get applyColumnFilter => 'Apply Column Filter';
+
+ @override
+ String get clearColumnFilter => 'Clear Column Filter';
+
+ @override
+ String get activeFilters => 'Active Filters';
+
+ @override
+ String get selectDate => 'Select Date';
+
+ @override
+ String get noDataFound => 'No data found';
+
+ @override
+ String get marketingReportSubtitle => 'Manage and analyze user referrals';
+
+ @override
+ String get showing => 'Showing';
+
+ @override
+ String get to => 'to';
+
+ @override
+ String get ofText => 'of';
+
+ @override
+ String get results => 'results';
+
+ @override
+ String get firstPage => 'First page';
+
+ @override
+ String get lastPage => 'Last page';
+
+ @override
+ String get exportToExcel => 'Export to Excel';
+
+ @override
+ String get exportToPdf => 'Export to PDF';
+
+ @override
+ String get exportSelected => 'Export Selected';
+
+ @override
+ String get exportAll => 'Export All';
+
+ @override
+ String get exporting => 'Exporting...';
+
+ @override
+ String get exportSuccess => 'Export completed successfully';
+
+ @override
+ String get exportError => 'Export error';
+
+ @override
+ String get export => 'Export';
+
+ @override
+ String get rowNumber => 'Row';
+
+ @override
+ String get registrationDate => 'Registration Date';
+
+ @override
+ String get selectedRange => 'Selected Range';
+
+ @override
+ String get page => 'Page';
+
+ @override
+ String get equals => 'equals';
+
+ @override
+ String get greater_than => 'greater than';
+
+ @override
+ String get greater_equal => 'greater or equal';
+
+ @override
+ String get less_than => 'less than';
+
+ @override
+ String get less_equal => 'less or equal';
+
+ @override
+ String get not_equals => 'not equals';
+
+ @override
+ String get starts_with => 'starts with';
+
+ @override
+ String get ends_with => 'ends with';
+
+ @override
+ String get in_list => 'in list';
}
diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
index 1a780db..65cbee0 100644
--- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
+++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart
@@ -88,7 +88,7 @@ class AppLocalizationsFa extends AppLocalizations {
String get captcha => 'کد امنیتی';
@override
- String get refresh => 'تازهسازی';
+ String get refresh => 'بروزرسانی';
@override
String get captchaRequired => 'کد امنیتی الزامی است.';
@@ -136,6 +136,37 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get cancel => 'انصراف';
+ @override
+ String get columnSettings => 'تنظیمات ستونها';
+
+ @override
+ String get columnSettingsDescription =>
+ 'مدیریت نمایش و ترتیب ستونهای این جدول';
+
+ @override
+ String get columnName => 'نام ستون';
+
+ @override
+ String get visibility => 'نمایش';
+
+ @override
+ String get order => 'ترتیب';
+
+ @override
+ String get visible => 'نمایش';
+
+ @override
+ String get hidden => 'مخفی';
+
+ @override
+ String get resetToDefaults => 'بازگردانی به پیشفرض';
+
+ @override
+ String get save => 'ذخیره';
+
+ @override
+ String get error => 'خطا';
+
@override
String get newBusiness => 'کسبوکار جدید';
@@ -231,4 +262,184 @@ class AppLocalizationsFa extends AppLocalizations {
@override
String get calendarType => 'نوع تقویم';
+
+ @override
+ String get dataLoadingError => 'خطا در بارگذاری دادهها';
+
+ @override
+ String get yourReferralLink => 'لینک معرفی شما';
+
+ @override
+ String get filtersAndSearch => 'فیلترها و جستجو';
+
+ @override
+ String get hideFilters => 'مخفی کردن فیلترها';
+
+ @override
+ String get showFilters => 'نمایش فیلترها';
+
+ @override
+ String get clear => 'پاک کردن';
+
+ @override
+ String get searchInNameEmail => 'جستجو در نام، نام خانوادگی و ایمیل...';
+
+ @override
+ String get recordsPerPage => 'سطر در هر صفحه';
+
+ @override
+ String get records => 'رکورد';
+
+ @override
+ String get test => 'تست';
+
+ @override
+ String get user => 'کاربر';
+
+ @override
+ String showingRecords(Object end, Object start, Object total) {
+ return 'نمایش $start تا $end از $total رکورد';
+ }
+
+ @override
+ String get previousPage => 'صفحه قبل';
+
+ @override
+ String get nextPage => 'صفحه بعد';
+
+ @override
+ String pageOf(Object current, Object total) {
+ return '$current از $total';
+ }
+
+ @override
+ String get referralList => 'لیست معرفیها';
+
+ @override
+ String get dateRangeFilter => 'فیلتر بازه زمانی';
+
+ @override
+ String get columnSearch => 'جستجو در ستون';
+
+ @override
+ String searchInColumn(Object column) {
+ return 'جستجو در $column';
+ }
+
+ @override
+ String get searchType => 'نوع جستجو';
+
+ @override
+ String get contains => 'شامل';
+
+ @override
+ String get startsWith => 'شروع با';
+
+ @override
+ String get endsWith => 'خاتمه با';
+
+ @override
+ String get exactMatch => 'مطابقت دقیق';
+
+ @override
+ String get searchValue => 'مقدار جستجو';
+
+ @override
+ String get applyColumnFilter => 'اعمال فیلتر ستون';
+
+ @override
+ String get clearColumnFilter => 'پاک کردن فیلتر ستون';
+
+ @override
+ String get activeFilters => 'فیلترهای فعال';
+
+ @override
+ String get selectDate => 'انتخاب تاریخ';
+
+ @override
+ String get noDataFound => 'هیچ دادهای یافت نشد';
+
+ @override
+ String get marketingReportSubtitle => 'مدیریت و تحلیل معرفیهای کاربران';
+
+ @override
+ String get showing => 'نمایش';
+
+ @override
+ String get to => 'تا';
+
+ @override
+ String get ofText => 'از';
+
+ @override
+ String get results => 'نتیجه';
+
+ @override
+ String get firstPage => 'صفحه اول';
+
+ @override
+ String get lastPage => 'صفحه آخر';
+
+ @override
+ String get exportToExcel => 'خروجی اکسل';
+
+ @override
+ String get exportToPdf => 'خروجی PDF';
+
+ @override
+ String get exportSelected => 'خروجی انتخاب شدهها';
+
+ @override
+ String get exportAll => 'خروجی همه';
+
+ @override
+ String get exporting => 'در حال خروجی...';
+
+ @override
+ String get exportSuccess => 'خروجی با موفقیت انجام شد';
+
+ @override
+ String get exportError => 'خطا در خروجی';
+
+ @override
+ String get export => 'خروجی';
+
+ @override
+ String get rowNumber => 'ردیف';
+
+ @override
+ String get registrationDate => 'تاریخ ثبت';
+
+ @override
+ String get selectedRange => 'بازه انتخابی';
+
+ @override
+ String get page => 'صفحه';
+
+ @override
+ String get equals => 'برابر';
+
+ @override
+ String get greater_than => 'بزرگتر از';
+
+ @override
+ String get greater_equal => 'بزرگتر یا برابر';
+
+ @override
+ String get less_than => 'کوچکتر از';
+
+ @override
+ String get less_equal => 'کوچکتر یا برابر';
+
+ @override
+ String get not_equals => 'مخالف';
+
+ @override
+ String get starts_with => 'شروع با';
+
+ @override
+ String get ends_with => 'پایان با';
+
+ @override
+ String get in_list => 'در لیست';
}
diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart
index b4a86af..2045102 100644
--- a/hesabixUI/hesabix_ui/lib/main.dart
+++ b/hesabixUI/hesabix_ui/lib/main.dart
@@ -4,7 +4,6 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'pages/login_page.dart';
-import 'pages/home_page.dart';
import 'pages/profile/profile_shell.dart';
import 'pages/profile/profile_dashboard_page.dart';
import 'pages/profile/new_business_page.dart';
@@ -89,20 +88,164 @@ class _MyAppState extends State {
// Root of application with GoRouter
@override
Widget build(BuildContext context) {
+ // اگر هنوز loading است، یک router ساده با loading page بساز
if (_controller == null || _calendarController == null || _themeController == null || _authStore == null) {
- return const MaterialApp(
- home: Scaffold(
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- CircularProgressIndicator(),
- SizedBox(height: 16),
- Text('Loading...'),
- ],
+ final loadingRouter = GoRouter(
+ redirect: (context, state) {
+ // در حین loading، هیچ redirect نکن - URL را حفظ کن
+ return null;
+ },
+ routes: [
+ GoRoute(
+ path: '/',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
),
),
- ),
+ // برای سایر مسیرها هم loading page نمایش بده
+ GoRoute(
+ path: '/login',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/dashboard',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/marketing',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/new-business',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/businesses',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/support',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ GoRoute(
+ path: '/user/profile/change-password',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ // Catch-all route برای هر URL دیگر
+ GoRoute(
+ path: '/:path(.*)',
+ builder: (context, state) => const Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ CircularProgressIndicator(),
+ SizedBox(height: 16),
+ Text('Loading...'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+
+ return MaterialApp.router(
+ title: 'Hesabix',
+ routerConfig: loadingRouter,
+ locale: const Locale('en'),
+ supportedLocales: const [Locale('en'), Locale('fa')],
+ localizationsDelegates: const [
+ GlobalMaterialLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ ],
);
}
@@ -143,6 +286,7 @@ class _MyAppState extends State {
}
// برای سایر صفحات (شامل صفحات profile)، redirect نکن (بماند)
+ // این مهم است: اگر کاربر در صفحات profile است، بماند
return null;
},
routes: [
diff --git a/hesabixUI/hesabix_ui/lib/pages/login_page.dart b/hesabixUI/hesabix_ui/lib/pages/login_page.dart
index 3beff54..4e7cd04 100644
--- a/hesabixUI/hesabix_ui/lib/pages/login_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/login_page.dart
@@ -86,8 +86,8 @@ class _LoginPageState extends State with SingleTickerProviderStateMix
if (body is! Map) return;
final data = body['data'];
if (data is! Map) return;
- final String? id = data['captcha_id'] as String?;
- final String? imgB64 = data['image_base64'] as String?;
+ final String? id = data['captcha_id']?.toString();
+ final String? imgB64 = data['image_base64']?.toString();
final int? ttl = (data['ttl_seconds'] as num?)?.toInt();
if (id == null || imgB64 == null) return;
Uint8List bytes;
@@ -236,12 +236,12 @@ class _LoginPageState extends State with SingleTickerProviderStateMix
final inner = body['data'];
if (inner is Map) data = inner;
}
- final apiKey = data != null ? data['api_key'] as String? : null;
+ final apiKey = data != null ? data['api_key']?.toString() : null;
if (apiKey != null && apiKey.isNotEmpty) {
await widget.authStore.saveApiKey(apiKey);
// ذخیره کد بازاریابی کاربر برای صفحه Marketing
final user = data?['user'] as Map?;
- final String? myRef = user != null ? user['referral_code'] as String? : null;
+ final String? myRef = user != null ? user['referral_code']?.toString() : null;
unawaited(ReferralStore.saveUserReferralCode(myRef));
}
@@ -323,7 +323,7 @@ class _LoginPageState extends State with SingleTickerProviderStateMix
final inner = body['data'];
if (inner is Map) data = inner;
}
- final apiKey = data != null ? data['api_key'] as String? : null;
+ final apiKey = data != null ? data['api_key']?.toString() : null;
if (apiKey != null && apiKey.isNotEmpty) {
await widget.authStore.saveApiKey(apiKey);
}
diff --git a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
index a91139e..10c2e1a 100644
--- a/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
+++ b/hesabixUI/hesabix_ui/lib/pages/profile/marketing_page.dart
@@ -1,14 +1,10 @@
-import 'dart:async';
-
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hesabix_ui/l10n/app_localizations.dart';
import '../../core/referral_store.dart';
import '../../core/api_client.dart';
import '../../core/calendar_controller.dart';
-import '../../core/date_utils.dart';
-import '../../widgets/jalali_date_picker.dart';
-import '../../widgets/date_input_field.dart';
+import '../../widgets/data_table/data_table.dart';
class MarketingPage extends StatefulWidget {
final CalendarController calendarController;
@@ -27,35 +23,13 @@ class _MarketingPageState extends State {
int? _rangeCount;
DateTime? _fromDate;
DateTime? _toDate;
- // list state
- bool _loadingList = false;
- int _page = 1;
- int _limit = 10;
- int _total = 0;
- List