progress in permissions
This commit is contained in:
parent
2dde8eeef9
commit
898e0fb993
|
|
@ -192,3 +192,101 @@ def get_business_statistics(
|
|||
stats_data = get_business_statistics(db, business_id, ctx)
|
||||
formatted_data = format_datetime_fields(stats_data, request)
|
||||
return success_response(formatted_data, request)
|
||||
|
||||
|
||||
@router.post("/{business_id}/info-with-permissions",
|
||||
summary="دریافت اطلاعات کسب و کار و دسترسیها",
|
||||
description="دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "اطلاعات کسب و کار و دسترسیها با موفقیت دریافت شد",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": True,
|
||||
"message": "اطلاعات کسب و کار و دسترسیها دریافت شد",
|
||||
"data": {
|
||||
"business_info": {
|
||||
"id": 1,
|
||||
"name": "شرکت نمونه",
|
||||
"business_type": "شرکت",
|
||||
"business_field": "تولیدی",
|
||||
"owner_id": 1,
|
||||
"address": "تهران، خیابان ولیعصر",
|
||||
"phone": "02112345678",
|
||||
"mobile": "09123456789",
|
||||
"created_at": "1403/01/01 00:00:00"
|
||||
},
|
||||
"user_permissions": {
|
||||
"people": {"add": True, "view": True, "edit": True, "delete": False},
|
||||
"products": {"add": True, "view": True, "edit": False, "delete": False},
|
||||
"invoices": {"add": True, "view": True, "edit": True, "delete": True}
|
||||
},
|
||||
"is_owner": False,
|
||||
"role": "عضو",
|
||||
"has_access": True
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
401: {
|
||||
"description": "کاربر احراز هویت نشده است"
|
||||
},
|
||||
403: {
|
||||
"description": "دسترسی غیرمجاز به کسب و کار"
|
||||
},
|
||||
404: {
|
||||
"description": "کسب و کار یافت نشد"
|
||||
}
|
||||
}
|
||||
)
|
||||
@require_business_access("business_id")
|
||||
def get_business_info_with_permissions(
|
||||
request: Request,
|
||||
business_id: int,
|
||||
ctx: AuthContext = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
) -> dict:
|
||||
"""دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر"""
|
||||
from adapters.db.models.business import Business
|
||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||
|
||||
# دریافت اطلاعات کسب و کار
|
||||
business = db.get(Business, business_id)
|
||||
if not business:
|
||||
from app.core.responses import ApiError
|
||||
raise ApiError("NOT_FOUND", "Business not found", http_status=404)
|
||||
|
||||
# دریافت دسترسیهای کاربر
|
||||
permissions = {}
|
||||
if not ctx.is_superadmin() and not ctx.is_business_owner(business_id):
|
||||
# دریافت دسترسیهای کسب و کار از business_permissions
|
||||
permission_repo = BusinessPermissionRepository(db)
|
||||
business_permission = permission_repo.get_by_business_and_user(business_id, ctx.get_user_id())
|
||||
if business_permission:
|
||||
permissions = business_permission.business_permissions or {}
|
||||
|
||||
business_info = {
|
||||
"id": business.id,
|
||||
"name": business.name,
|
||||
"business_type": business.business_type.value,
|
||||
"business_field": business.business_field.value,
|
||||
"owner_id": business.owner_id,
|
||||
"address": business.address,
|
||||
"phone": business.phone,
|
||||
"mobile": business.mobile,
|
||||
"created_at": business.created_at.isoformat(),
|
||||
}
|
||||
|
||||
response_data = {
|
||||
"business_info": business_info,
|
||||
"user_permissions": permissions,
|
||||
"is_owner": ctx.is_business_owner(business_id),
|
||||
"role": "مالک" if ctx.is_business_owner(business_id) else "عضو",
|
||||
"has_access": ctx.can_access_business(business_id)
|
||||
}
|
||||
|
||||
formatted_data = format_datetime_fields(response_data, request)
|
||||
return success_response(formatted_data, request)
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ def add_user(
|
|||
permission_obj = permission_repo.create_or_update(
|
||||
user_id=user.id,
|
||||
business_id=business_id,
|
||||
permissions={} # Default empty permissions
|
||||
permissions={'join': True} # Default permissions with join access
|
||||
)
|
||||
|
||||
logger.info(f"Created permission object: {permission_obj.id}")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from app.core.responses import success_response, format_datetime_fields
|
|||
from app.core.auth_dependency import get_current_user, AuthContext
|
||||
from app.core.permissions import require_business_management
|
||||
from app.services.business_service import (
|
||||
create_business, get_business_by_id, get_businesses_by_owner,
|
||||
create_business, get_business_by_id, get_businesses_by_owner, get_user_businesses,
|
||||
update_business, delete_business, get_business_summary
|
||||
)
|
||||
|
||||
|
|
@ -116,8 +116,8 @@ def list_user_businesses(
|
|||
sort_desc: bool = True,
|
||||
search: str = None
|
||||
) -> dict:
|
||||
"""لیست کسب و کارهای کاربر"""
|
||||
owner_id = ctx.get_user_id()
|
||||
"""لیست کسب و کارهای کاربر (مالک + عضو)"""
|
||||
user_id = ctx.get_user_id()
|
||||
query_dict = {
|
||||
"take": take,
|
||||
"skip": skip,
|
||||
|
|
@ -125,34 +125,10 @@ def list_user_businesses(
|
|||
"sort_desc": sort_desc,
|
||||
"search": search
|
||||
}
|
||||
businesses = get_businesses_by_owner(db, owner_id, query_dict)
|
||||
businesses = get_user_businesses(db, user_id, query_dict)
|
||||
formatted_data = format_datetime_fields(businesses, request)
|
||||
|
||||
# اگر formatted_data یک dict با کلید items است، آن را استخراج کنیم
|
||||
if isinstance(formatted_data, dict) and 'items' in formatted_data:
|
||||
items = formatted_data['items']
|
||||
else:
|
||||
items = formatted_data
|
||||
|
||||
# برای حالا total را برابر با تعداد items قرار میدهیم
|
||||
# در آینده میتوان total را از service دریافت کرد
|
||||
total = len(items)
|
||||
page = (skip // take) + 1
|
||||
total_pages = (total + take - 1) // take
|
||||
|
||||
response_data = {
|
||||
"items": items,
|
||||
"pagination": {
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": take,
|
||||
"total_pages": total_pages,
|
||||
"has_next": page < total_pages,
|
||||
"has_prev": page > 1,
|
||||
},
|
||||
"query_info": query_dict
|
||||
}
|
||||
return success_response(response_data, request)
|
||||
return success_response(formatted_data, request)
|
||||
|
||||
|
||||
@router.post("/{business_id}/details",
|
||||
|
|
|
|||
253
hesabixAPI/adapters/api/v1/persons.py
Normal file
253
hesabixAPI/adapters/api/v1/persons.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any
|
||||
|
||||
from adapters.db.session import get_db
|
||||
from adapters.api.v1.schema_models.person import (
|
||||
PersonCreateRequest, PersonUpdateRequest, PersonResponse,
|
||||
PersonListResponse, PersonSummaryResponse, PersonBankAccountCreateRequest
|
||||
)
|
||||
from adapters.api.v1.schemas import QueryInfo, SuccessResponse
|
||||
from app.core.responses import success_response, format_datetime_fields
|
||||
from app.core.auth_dependency import get_current_user, AuthContext
|
||||
from app.core.permissions import require_business_management
|
||||
from app.services.person_service import (
|
||||
create_person, get_person_by_id, get_persons_by_business,
|
||||
update_person, delete_person, get_person_summary
|
||||
)
|
||||
from adapters.db.models.person import Person
|
||||
|
||||
router = APIRouter(prefix="/persons", tags=["persons"])
|
||||
|
||||
|
||||
@router.post("/businesses/{business_id}/persons/create",
|
||||
summary="ایجاد شخص جدید",
|
||||
description="ایجاد شخص جدید برای کسب و کار مشخص",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "شخص با موفقیت ایجاد شد",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": True,
|
||||
"message": "شخص با موفقیت ایجاد شد",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"business_id": 1,
|
||||
"alias_name": "علی احمدی",
|
||||
"person_type": "مشتری",
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
400: {
|
||||
"description": "خطا در اعتبارسنجی دادهها"
|
||||
},
|
||||
401: {
|
||||
"description": "عدم احراز هویت"
|
||||
},
|
||||
403: {
|
||||
"description": "عدم دسترسی به کسب و کار"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def create_person_endpoint(
|
||||
business_id: int,
|
||||
person_data: PersonCreateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_management())
|
||||
):
|
||||
"""ایجاد شخص جدید برای کسب و کار"""
|
||||
result = create_person(db, business_id, person_data)
|
||||
return success_response(
|
||||
message=result['message'],
|
||||
data=format_datetime_fields(result['data'])
|
||||
)
|
||||
|
||||
|
||||
@router.post("/businesses/{business_id}/persons",
|
||||
summary="لیست اشخاص کسب و کار",
|
||||
description="دریافت لیست اشخاص یک کسب و کار با امکان جستجو و فیلتر",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "لیست اشخاص با موفقیت دریافت شد",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"success": True,
|
||||
"message": "لیست اشخاص با موفقیت دریافت شد",
|
||||
"data": {
|
||||
"items": [],
|
||||
"pagination": {
|
||||
"total": 0,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"total_pages": 0,
|
||||
"has_next": False,
|
||||
"has_prev": False
|
||||
},
|
||||
"query_info": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_persons_endpoint(
|
||||
business_id: int,
|
||||
query_info: QueryInfo,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user)
|
||||
):
|
||||
"""دریافت لیست اشخاص کسب و کار"""
|
||||
query_dict = {
|
||||
"take": query_info.take,
|
||||
"skip": query_info.skip,
|
||||
"sort_by": query_info.sort_by,
|
||||
"sort_desc": query_info.sort_desc,
|
||||
"search": query_info.search
|
||||
}
|
||||
result = get_persons_by_business(db, business_id, query_dict)
|
||||
|
||||
# فرمت کردن تاریخها
|
||||
for item in result['items']:
|
||||
item = format_datetime_fields(item)
|
||||
|
||||
return success_response(
|
||||
message="لیست اشخاص با موفقیت دریافت شد",
|
||||
data=result
|
||||
)
|
||||
|
||||
|
||||
@router.get("/persons/{person_id}",
|
||||
summary="جزئیات شخص",
|
||||
description="دریافت جزئیات یک شخص",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "جزئیات شخص با موفقیت دریافت شد"
|
||||
},
|
||||
404: {
|
||||
"description": "شخص یافت نشد"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_person_endpoint(
|
||||
person_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_management())
|
||||
):
|
||||
"""دریافت جزئیات شخص"""
|
||||
# ابتدا باید business_id را از person دریافت کنیم
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
result = get_person_by_id(db, person_id, person.business_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
return success_response(
|
||||
message="جزئیات شخص با موفقیت دریافت شد",
|
||||
data=format_datetime_fields(result)
|
||||
)
|
||||
|
||||
|
||||
@router.put("/persons/{person_id}",
|
||||
summary="ویرایش شخص",
|
||||
description="ویرایش اطلاعات یک شخص",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "شخص با موفقیت ویرایش شد"
|
||||
},
|
||||
404: {
|
||||
"description": "شخص یافت نشد"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def update_person_endpoint(
|
||||
person_id: int,
|
||||
person_data: PersonUpdateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_management())
|
||||
):
|
||||
"""ویرایش شخص"""
|
||||
# ابتدا باید business_id را از person دریافت کنیم
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
result = update_person(db, person_id, person.business_id, person_data)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
return success_response(
|
||||
message=result['message'],
|
||||
data=format_datetime_fields(result['data'])
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/persons/{person_id}",
|
||||
summary="حذف شخص",
|
||||
description="حذف یک شخص",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "شخص با موفقیت حذف شد"
|
||||
},
|
||||
404: {
|
||||
"description": "شخص یافت نشد"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def delete_person_endpoint(
|
||||
person_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_management())
|
||||
):
|
||||
"""حذف شخص"""
|
||||
# ابتدا باید business_id را از person دریافت کنیم
|
||||
person = db.query(Person).filter(Person.id == person_id).first()
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
success = delete_person(db, person_id, person.business_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||
|
||||
return success_response(message="شخص با موفقیت حذف شد")
|
||||
|
||||
|
||||
@router.get("/businesses/{business_id}/persons/summary",
|
||||
summary="خلاصه اشخاص کسب و کار",
|
||||
description="دریافت خلاصه آماری اشخاص یک کسب و کار",
|
||||
response_model=SuccessResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "خلاصه اشخاص با موفقیت دریافت شد"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_persons_summary_endpoint(
|
||||
business_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
auth_context: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_management())
|
||||
):
|
||||
"""دریافت خلاصه اشخاص کسب و کار"""
|
||||
result = get_person_summary(db, business_id)
|
||||
|
||||
return success_response(
|
||||
message="خلاصه اشخاص با موفقیت دریافت شد",
|
||||
data=result
|
||||
)
|
||||
168
hesabixAPI/adapters/api/v1/schema_models/person.py
Normal file
168
hesabixAPI/adapters/api/v1/schema_models/person.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class PersonType(str, Enum):
|
||||
"""نوع شخص"""
|
||||
CUSTOMER = "مشتری"
|
||||
MARKETER = "بازاریاب"
|
||||
EMPLOYEE = "کارمند"
|
||||
SUPPLIER = "تامینکننده"
|
||||
PARTNER = "همکار"
|
||||
SELLER = "فروشنده"
|
||||
|
||||
|
||||
class PersonBankAccountCreateRequest(BaseModel):
|
||||
"""درخواست ایجاد حساب بانکی شخص"""
|
||||
bank_name: str = Field(..., min_length=1, max_length=255, description="نام بانک")
|
||||
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
|
||||
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
|
||||
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
|
||||
|
||||
|
||||
class PersonBankAccountUpdateRequest(BaseModel):
|
||||
"""درخواست ویرایش حساب بانکی شخص"""
|
||||
bank_name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام بانک")
|
||||
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
|
||||
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
|
||||
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
|
||||
is_active: Optional[bool] = Field(default=None, description="وضعیت فعال بودن")
|
||||
|
||||
|
||||
class PersonBankAccountResponse(BaseModel):
|
||||
"""پاسخ اطلاعات حساب بانکی شخص"""
|
||||
id: int = Field(..., description="شناسه حساب بانکی")
|
||||
person_id: int = Field(..., description="شناسه شخص")
|
||||
bank_name: str = Field(..., description="نام بانک")
|
||||
account_number: Optional[str] = Field(default=None, description="شماره حساب")
|
||||
card_number: Optional[str] = Field(default=None, description="شماره کارت")
|
||||
sheba_number: Optional[str] = Field(default=None, description="شماره شبا")
|
||||
is_active: bool = Field(..., description="وضعیت فعال بودن")
|
||||
created_at: str = Field(..., description="تاریخ ایجاد")
|
||||
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PersonCreateRequest(BaseModel):
|
||||
"""درخواست ایجاد شخص جدید"""
|
||||
# اطلاعات پایه
|
||||
alias_name: str = Field(..., min_length=1, max_length=255, description="نام مستعار (الزامی)")
|
||||
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
|
||||
last_name: Optional[str] = Field(default=None, max_length=100, description="نام خانوادگی")
|
||||
person_type: PersonType = Field(..., description="نوع شخص")
|
||||
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
||||
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
||||
|
||||
# اطلاعات اقتصادی
|
||||
national_id: Optional[str] = Field(default=None, max_length=20, description="شناسه ملی")
|
||||
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
|
||||
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
|
||||
|
||||
# اطلاعات تماس
|
||||
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
|
||||
province: Optional[str] = Field(default=None, max_length=100, description="استان")
|
||||
city: Optional[str] = Field(default=None, max_length=100, description="شهرستان")
|
||||
address: Optional[str] = Field(default=None, description="آدرس")
|
||||
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
|
||||
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن")
|
||||
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
|
||||
fax: Optional[str] = Field(default=None, max_length=20, description="فکس")
|
||||
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
|
||||
website: Optional[str] = Field(default=None, max_length=255, description="وبسایت")
|
||||
|
||||
# حسابهای بانکی
|
||||
bank_accounts: Optional[List[PersonBankAccountCreateRequest]] = Field(default=[], description="حسابهای بانکی")
|
||||
|
||||
|
||||
class PersonUpdateRequest(BaseModel):
|
||||
"""درخواست ویرایش شخص"""
|
||||
# اطلاعات پایه
|
||||
alias_name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام مستعار")
|
||||
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
|
||||
last_name: Optional[str] = Field(default=None, max_length=100, description="نام خانوادگی")
|
||||
person_type: Optional[PersonType] = Field(default=None, description="نوع شخص")
|
||||
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
||||
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
||||
|
||||
# اطلاعات اقتصادی
|
||||
national_id: Optional[str] = Field(default=None, max_length=20, description="شناسه ملی")
|
||||
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
|
||||
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
|
||||
|
||||
# اطلاعات تماس
|
||||
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
|
||||
province: Optional[str] = Field(default=None, max_length=100, description="استان")
|
||||
city: Optional[str] = Field(default=None, max_length=100, description="شهرستان")
|
||||
address: Optional[str] = Field(default=None, description="آدرس")
|
||||
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
|
||||
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن")
|
||||
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
|
||||
fax: Optional[str] = Field(default=None, max_length=20, description="فکس")
|
||||
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
|
||||
website: Optional[str] = Field(default=None, max_length=255, description="وبسایت")
|
||||
|
||||
# وضعیت
|
||||
is_active: Optional[bool] = Field(default=None, description="وضعیت فعال بودن")
|
||||
|
||||
|
||||
class PersonResponse(BaseModel):
|
||||
"""پاسخ اطلاعات شخص"""
|
||||
id: int = Field(..., description="شناسه شخص")
|
||||
business_id: int = Field(..., description="شناسه کسب و کار")
|
||||
|
||||
# اطلاعات پایه
|
||||
alias_name: str = Field(..., description="نام مستعار")
|
||||
first_name: Optional[str] = Field(default=None, description="نام")
|
||||
last_name: Optional[str] = Field(default=None, description="نام خانوادگی")
|
||||
person_type: str = Field(..., description="نوع شخص")
|
||||
company_name: Optional[str] = Field(default=None, description="نام شرکت")
|
||||
payment_id: Optional[str] = Field(default=None, description="شناسه پرداخت")
|
||||
|
||||
# اطلاعات اقتصادی
|
||||
national_id: Optional[str] = Field(default=None, description="شناسه ملی")
|
||||
registration_number: Optional[str] = Field(default=None, description="شماره ثبت")
|
||||
economic_id: Optional[str] = Field(default=None, description="شناسه اقتصادی")
|
||||
|
||||
# اطلاعات تماس
|
||||
country: Optional[str] = Field(default=None, description="کشور")
|
||||
province: Optional[str] = Field(default=None, description="استان")
|
||||
city: Optional[str] = Field(default=None, description="شهرستان")
|
||||
address: Optional[str] = Field(default=None, description="آدرس")
|
||||
postal_code: Optional[str] = Field(default=None, description="کد پستی")
|
||||
phone: Optional[str] = Field(default=None, description="تلفن")
|
||||
mobile: Optional[str] = Field(default=None, description="موبایل")
|
||||
fax: Optional[str] = Field(default=None, description="فکس")
|
||||
email: Optional[str] = Field(default=None, description="پست الکترونیکی")
|
||||
website: Optional[str] = Field(default=None, description="وبسایت")
|
||||
|
||||
# وضعیت
|
||||
is_active: bool = Field(..., description="وضعیت فعال بودن")
|
||||
|
||||
# زمانبندی
|
||||
created_at: str = Field(..., description="تاریخ ایجاد")
|
||||
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
|
||||
|
||||
# حسابهای بانکی
|
||||
bank_accounts: List[PersonBankAccountResponse] = Field(default=[], description="حسابهای بانکی")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PersonListResponse(BaseModel):
|
||||
"""پاسخ لیست اشخاص"""
|
||||
items: List[PersonResponse] = Field(..., description="لیست اشخاص")
|
||||
pagination: dict = Field(..., description="اطلاعات صفحهبندی")
|
||||
query_info: dict = Field(..., description="اطلاعات جستجو و فیلتر")
|
||||
|
||||
|
||||
class PersonSummaryResponse(BaseModel):
|
||||
"""پاسخ خلاصه اشخاص"""
|
||||
total_persons: int = Field(..., description="تعداد کل اشخاص")
|
||||
by_type: dict = Field(..., description="تعداد بر اساس نوع")
|
||||
active_persons: int = Field(..., description="تعداد اشخاص فعال")
|
||||
inactive_persons: int = Field(..., description="تعداد اشخاص غیرفعال")
|
||||
|
|
@ -7,6 +7,7 @@ from .captcha import Captcha # noqa: F401
|
|||
from .password_reset import PasswordReset # noqa: F401
|
||||
from .business import Business # noqa: F401
|
||||
from .business_permission import BusinessPermission # noqa: F401
|
||||
from .person import Person, PersonBankAccount # noqa: F401
|
||||
# Business user models removed - using business_permissions instead
|
||||
|
||||
# Import support models
|
||||
|
|
|
|||
|
|
@ -54,5 +54,5 @@ class Business(Base):
|
|||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships - using business_permissions instead
|
||||
# users = relationship("BusinessUser", back_populates="business", cascade="all, delete-orphan")
|
||||
# Relationships
|
||||
persons: Mapped[list["Person"]] = relationship("Person", back_populates="business", cascade="all, delete-orphan")
|
||||
|
|
|
|||
85
hesabixAPI/adapters/db/models/person.py
Normal file
85
hesabixAPI/adapters/db/models/person.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text, Boolean
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from adapters.db.session import Base
|
||||
|
||||
|
||||
class PersonType(str, Enum):
|
||||
"""نوع شخص"""
|
||||
CUSTOMER = "مشتری" # مشتری
|
||||
MARKETER = "بازاریاب" # بازاریاب
|
||||
EMPLOYEE = "کارمند" # کارمند
|
||||
SUPPLIER = "تامینکننده" # تامینکننده
|
||||
PARTNER = "همکار" # همکار
|
||||
SELLER = "فروشنده" # فروشنده
|
||||
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = "persons"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
# اطلاعات پایه
|
||||
alias_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="نام مستعار (الزامی)")
|
||||
first_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="نام")
|
||||
last_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="نام خانوادگی")
|
||||
person_type: Mapped[PersonType] = mapped_column(SQLEnum(PersonType), nullable=False, comment="نوع شخص")
|
||||
company_name: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="نام شرکت")
|
||||
payment_id: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="شناسه پرداخت")
|
||||
|
||||
# اطلاعات اقتصادی
|
||||
national_id: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="شناسه ملی")
|
||||
registration_number: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="شماره ثبت")
|
||||
economic_id: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="شناسه اقتصادی")
|
||||
|
||||
# اطلاعات تماس
|
||||
country: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="کشور")
|
||||
province: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="استان")
|
||||
city: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="شهرستان")
|
||||
address: Mapped[str | None] = mapped_column(Text, nullable=True, comment="آدرس")
|
||||
postal_code: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="کد پستی")
|
||||
phone: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="تلفن")
|
||||
mobile: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="موبایل")
|
||||
fax: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="فکس")
|
||||
email: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="پست الکترونیکی")
|
||||
website: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="وبسایت")
|
||||
|
||||
# وضعیت
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="وضعیت فعال بودن")
|
||||
|
||||
# زمانبندی
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
business: Mapped["Business"] = relationship("Business", back_populates="persons")
|
||||
bank_accounts: Mapped[list["PersonBankAccount"]] = relationship("PersonBankAccount", back_populates="person", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class PersonBankAccount(Base):
|
||||
__tablename__ = "person_bank_accounts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
|
||||
# اطلاعات حساب بانکی
|
||||
bank_name: Mapped[str] = mapped_column(String(255), nullable=False, comment="نام بانک")
|
||||
account_number: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="شماره حساب")
|
||||
card_number: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="شماره کارت")
|
||||
sheba_number: Mapped[str | None] = mapped_column(String(30), nullable=True, comment="شماره شبا")
|
||||
|
||||
# وضعیت
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="وضعیت فعال بودن")
|
||||
|
||||
# زمانبندی
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
person: Mapped["Person"] = relationship("Person", back_populates="bank_accounts")
|
||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy import select, and_, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from adapters.db.models.business_permission import BusinessPermission
|
||||
|
|
@ -61,3 +61,16 @@ class BusinessPermissionRepository(BaseRepository[BusinessPermission]):
|
|||
"""دریافت تمام کاربرانی که دسترسی به کسب و کار دارند"""
|
||||
stmt = select(BusinessPermission).where(BusinessPermission.business_id == business_id)
|
||||
return self.db.execute(stmt).scalars().all()
|
||||
|
||||
def get_user_member_businesses(self, user_id: int) -> list[BusinessPermission]:
|
||||
"""دریافت تمام کسب و کارهایی که کاربر عضو آنها است (دسترسی join)"""
|
||||
# ابتدا تمام دسترسیهای کاربر را دریافت میکنیم
|
||||
all_permissions = self.get_user_businesses(user_id)
|
||||
|
||||
# سپس فیلتر میکنیم
|
||||
member_permissions = []
|
||||
for perm in all_permissions:
|
||||
if perm.business_permissions and perm.business_permissions.get('join') == True:
|
||||
member_permissions.append(perm)
|
||||
|
||||
return member_permissions
|
||||
|
|
@ -247,6 +247,42 @@ class AuthContext:
|
|||
logger.info(f"Business access check: {business_id} == {self.business_id} = {has_access}")
|
||||
return has_access
|
||||
|
||||
def is_business_member(self, business_id: int) -> bool:
|
||||
"""بررسی اینکه آیا کاربر عضو کسب و کار است یا نه (دسترسی join)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(f"Checking business membership: user {self.user.id}, business {business_id}")
|
||||
|
||||
# SuperAdmin عضو همه کسب و کارها محسوب میشود
|
||||
if self.is_superadmin():
|
||||
logger.info(f"User {self.user.id} is superadmin, is member of all businesses")
|
||||
return True
|
||||
|
||||
# اگر مالک کسب و کار است، عضو محسوب میشود
|
||||
if self.is_business_owner() and business_id == self.business_id:
|
||||
logger.info(f"User {self.user.id} is business owner of {business_id}, is member")
|
||||
return True
|
||||
|
||||
# بررسی دسترسی join در business_permissions
|
||||
if not self.db:
|
||||
logger.info(f"No database session available")
|
||||
return False
|
||||
|
||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||
repo = BusinessPermissionRepository(self.db)
|
||||
permission_obj = repo.get_by_user_and_business(self.user.id, business_id)
|
||||
|
||||
if not permission_obj:
|
||||
logger.info(f"No business permission found for user {self.user.id} and business {business_id}")
|
||||
return False
|
||||
|
||||
# بررسی دسترسی join
|
||||
business_perms = permission_obj.business_permissions or {}
|
||||
has_join_access = business_perms.get('join', False)
|
||||
logger.info(f"Business membership check: user {self.user.id} join access to business {business_id}: {has_join_access}")
|
||||
return has_join_access
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""تبدیل به dictionary برای استفاده در API"""
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from adapters.api.v1.users import router as users_router
|
|||
from adapters.api.v1.businesses import router as businesses_router
|
||||
from adapters.api.v1.business_dashboard import router as business_dashboard_router
|
||||
from adapters.api.v1.business_users import router as business_users_router
|
||||
from adapters.api.v1.persons import router as persons_router
|
||||
from adapters.api.v1.support.tickets import router as support_tickets_router
|
||||
from adapters.api.v1.support.operator import router as support_operator_router
|
||||
from adapters.api.v1.support.categories import router as support_categories_router
|
||||
|
|
@ -273,6 +274,7 @@ def create_app() -> FastAPI:
|
|||
application.include_router(businesses_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(business_dashboard_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(business_users_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(persons_router, prefix=settings.api_v1_prefix)
|
||||
|
||||
# Support endpoints
|
||||
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
|
|||
from sqlalchemy import select, and_, func
|
||||
|
||||
from adapters.db.repositories.business_repo import BusinessRepository
|
||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||
from adapters.db.models.business import Business, BusinessType, BusinessField
|
||||
from adapters.api.v1.schemas import (
|
||||
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
|
||||
|
|
@ -110,6 +111,91 @@ def get_businesses_by_owner(db: Session, owner_id: int, query_info: Dict[str, An
|
|||
}
|
||||
|
||||
|
||||
def get_user_businesses(db: Session, user_id: int, query_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""دریافت لیست کسب و کارهای کاربر (مالک + عضو)"""
|
||||
business_repo = BusinessRepository(db)
|
||||
permission_repo = BusinessPermissionRepository(db)
|
||||
|
||||
# دریافت کسب و کارهای مالک
|
||||
owned_businesses = business_repo.get_by_owner_id(user_id)
|
||||
|
||||
# دریافت کسب و کارهای عضو
|
||||
member_permissions = permission_repo.get_user_member_businesses(user_id)
|
||||
member_business_ids = [perm.business_id for perm in member_permissions]
|
||||
member_businesses = []
|
||||
for business_id in member_business_ids:
|
||||
business = business_repo.get_by_id(business_id)
|
||||
if business:
|
||||
member_businesses.append(business)
|
||||
|
||||
# ترکیب لیستها
|
||||
all_businesses = []
|
||||
|
||||
# اضافه کردن کسب و کارهای مالک با نقش owner
|
||||
for business in owned_businesses:
|
||||
business_dict = _business_to_dict(business)
|
||||
business_dict['is_owner'] = True
|
||||
business_dict['role'] = 'مالک'
|
||||
business_dict['permissions'] = {}
|
||||
all_businesses.append(business_dict)
|
||||
|
||||
# اضافه کردن کسب و کارهای عضو با نقش member
|
||||
for business in member_businesses:
|
||||
# اگر قبلاً به عنوان مالک اضافه شده، نادیده بگیر
|
||||
if business.id not in [b['id'] for b in all_businesses]:
|
||||
business_dict = _business_to_dict(business)
|
||||
business_dict['is_owner'] = False
|
||||
business_dict['role'] = 'عضو'
|
||||
# دریافت دسترسیهای کاربر برای این کسب و کار
|
||||
permission_obj = permission_repo.get_by_user_and_business(user_id, business.id)
|
||||
business_dict['permissions'] = permission_obj.business_permissions if permission_obj else {}
|
||||
all_businesses.append(business_dict)
|
||||
|
||||
# اعمال فیلترها
|
||||
if query_info.get('search'):
|
||||
search_term = query_info['search']
|
||||
all_businesses = [b for b in all_businesses if search_term.lower() in b['name'].lower()]
|
||||
|
||||
# اعمال مرتبسازی
|
||||
sort_by = query_info.get('sort_by', 'created_at')
|
||||
sort_desc = query_info.get('sort_desc', True)
|
||||
|
||||
if sort_by == 'name':
|
||||
all_businesses.sort(key=lambda x: x['name'], reverse=sort_desc)
|
||||
elif sort_by == 'business_type':
|
||||
all_businesses.sort(key=lambda x: x['business_type'], reverse=sort_desc)
|
||||
elif sort_by == 'created_at':
|
||||
all_businesses.sort(key=lambda x: x['created_at'], reverse=sort_desc)
|
||||
|
||||
# صفحهبندی
|
||||
total = len(all_businesses)
|
||||
skip = query_info.get('skip', 0)
|
||||
take = query_info.get('take', 10)
|
||||
|
||||
start_idx = skip
|
||||
end_idx = skip + take
|
||||
paginated_businesses = all_businesses[start_idx:end_idx]
|
||||
|
||||
# محاسبه اطلاعات صفحهبندی
|
||||
total_pages = (total + take - 1) // take
|
||||
current_page = (skip // take) + 1
|
||||
|
||||
pagination = PaginationInfo(
|
||||
total=total,
|
||||
page=current_page,
|
||||
per_page=take,
|
||||
total_pages=total_pages,
|
||||
has_next=current_page < total_pages,
|
||||
has_prev=current_page > 1
|
||||
)
|
||||
|
||||
return {
|
||||
"items": paginated_businesses,
|
||||
"pagination": pagination.dict(),
|
||||
"query_info": query_info
|
||||
}
|
||||
|
||||
|
||||
def update_business(db: Session, business_id: int, business_data: BusinessUpdateRequest, owner_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""ویرایش کسب و کار"""
|
||||
business_repo = BusinessRepository(db)
|
||||
|
|
|
|||
294
hesabixAPI/app/services/person_service.py
Normal file
294
hesabixAPI/app/services/person_service.py
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
from typing import List, Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
from adapters.db.models.person import Person, PersonBankAccount, PersonType
|
||||
from adapters.api.v1.schema_models.person import (
|
||||
PersonCreateRequest, PersonUpdateRequest, PersonBankAccountCreateRequest
|
||||
)
|
||||
from app.core.responses import success_response
|
||||
|
||||
|
||||
def create_person(db: Session, business_id: int, person_data: PersonCreateRequest) -> Dict[str, Any]:
|
||||
"""ایجاد شخص جدید"""
|
||||
# ایجاد شخص
|
||||
person = Person(
|
||||
business_id=business_id,
|
||||
alias_name=person_data.alias_name,
|
||||
first_name=person_data.first_name,
|
||||
last_name=person_data.last_name,
|
||||
person_type=person_data.person_type,
|
||||
company_name=person_data.company_name,
|
||||
payment_id=person_data.payment_id,
|
||||
national_id=person_data.national_id,
|
||||
registration_number=person_data.registration_number,
|
||||
economic_id=person_data.economic_id,
|
||||
country=person_data.country,
|
||||
province=person_data.province,
|
||||
city=person_data.city,
|
||||
address=person_data.address,
|
||||
postal_code=person_data.postal_code,
|
||||
phone=person_data.phone,
|
||||
mobile=person_data.mobile,
|
||||
fax=person_data.fax,
|
||||
email=person_data.email,
|
||||
website=person_data.website,
|
||||
)
|
||||
|
||||
db.add(person)
|
||||
db.flush() # برای دریافت ID
|
||||
|
||||
# ایجاد حسابهای بانکی
|
||||
if person_data.bank_accounts:
|
||||
for bank_account_data in person_data.bank_accounts:
|
||||
bank_account = PersonBankAccount(
|
||||
person_id=person.id,
|
||||
bank_name=bank_account_data.bank_name,
|
||||
account_number=bank_account_data.account_number,
|
||||
card_number=bank_account_data.card_number,
|
||||
sheba_number=bank_account_data.sheba_number,
|
||||
)
|
||||
db.add(bank_account)
|
||||
|
||||
db.commit()
|
||||
db.refresh(person)
|
||||
|
||||
return success_response(
|
||||
message="شخص با موفقیت ایجاد شد",
|
||||
data=_person_to_dict(person)
|
||||
)
|
||||
|
||||
|
||||
def get_person_by_id(db: Session, person_id: int, business_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""دریافت شخص بر اساس شناسه"""
|
||||
person = db.query(Person).filter(
|
||||
and_(Person.id == person_id, Person.business_id == business_id)
|
||||
).first()
|
||||
|
||||
if not person:
|
||||
return None
|
||||
|
||||
return _person_to_dict(person)
|
||||
|
||||
|
||||
def get_persons_by_business(
|
||||
db: Session,
|
||||
business_id: int,
|
||||
query_info: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""دریافت لیست اشخاص با جستجو و فیلتر"""
|
||||
query = db.query(Person).filter(Person.business_id == business_id)
|
||||
|
||||
# اعمال جستجو
|
||||
if query_info.get('search') and query_info.get('search_fields'):
|
||||
search_term = f"%{query_info['search']}%"
|
||||
search_conditions = []
|
||||
|
||||
for field in query_info['search_fields']:
|
||||
if field == 'alias_name':
|
||||
search_conditions.append(Person.alias_name.ilike(search_term))
|
||||
elif field == 'first_name':
|
||||
search_conditions.append(Person.first_name.ilike(search_term))
|
||||
elif field == 'last_name':
|
||||
search_conditions.append(Person.last_name.ilike(search_term))
|
||||
elif field == 'company_name':
|
||||
search_conditions.append(Person.company_name.ilike(search_term))
|
||||
elif field == 'mobile':
|
||||
search_conditions.append(Person.mobile.ilike(search_term))
|
||||
elif field == 'email':
|
||||
search_conditions.append(Person.email.ilike(search_term))
|
||||
elif field == 'national_id':
|
||||
search_conditions.append(Person.national_id.ilike(search_term))
|
||||
|
||||
if search_conditions:
|
||||
query = query.filter(or_(*search_conditions))
|
||||
|
||||
# اعمال فیلترها
|
||||
if query_info.get('filters'):
|
||||
for filter_item in query_info['filters']:
|
||||
field = filter_item.get('property')
|
||||
operator = filter_item.get('operator')
|
||||
value = filter_item.get('value')
|
||||
|
||||
if field == 'person_type':
|
||||
if operator == '=':
|
||||
query = query.filter(Person.person_type == value)
|
||||
elif operator == 'in':
|
||||
query = query.filter(Person.person_type.in_(value))
|
||||
elif field == 'is_active':
|
||||
if operator == '=':
|
||||
query = query.filter(Person.is_active == value)
|
||||
elif field == 'country':
|
||||
if operator == '=':
|
||||
query = query.filter(Person.country == value)
|
||||
elif operator == 'like':
|
||||
query = query.filter(Person.country.ilike(f"%{value}%"))
|
||||
elif field == 'province':
|
||||
if operator == '=':
|
||||
query = query.filter(Person.province == value)
|
||||
elif operator == 'like':
|
||||
query = query.filter(Person.province.ilike(f"%{value}%"))
|
||||
|
||||
# شمارش کل رکوردها
|
||||
total = query.count()
|
||||
|
||||
# اعمال مرتبسازی
|
||||
sort_by = query_info.get('sort_by', 'created_at')
|
||||
sort_desc = query_info.get('sort_desc', True)
|
||||
|
||||
if sort_by == 'alias_name':
|
||||
query = query.order_by(Person.alias_name.desc() if sort_desc else Person.alias_name.asc())
|
||||
elif sort_by == 'first_name':
|
||||
query = query.order_by(Person.first_name.desc() if sort_desc else Person.first_name.asc())
|
||||
elif sort_by == 'last_name':
|
||||
query = query.order_by(Person.last_name.desc() if sort_desc else Person.last_name.asc())
|
||||
elif sort_by == 'person_type':
|
||||
query = query.order_by(Person.person_type.desc() if sort_desc else Person.person_type.asc())
|
||||
elif sort_by == 'created_at':
|
||||
query = query.order_by(Person.created_at.desc() if sort_desc else Person.created_at.asc())
|
||||
elif sort_by == 'updated_at':
|
||||
query = query.order_by(Person.updated_at.desc() if sort_desc else Person.updated_at.asc())
|
||||
else:
|
||||
query = query.order_by(Person.created_at.desc())
|
||||
|
||||
# اعمال صفحهبندی
|
||||
skip = query_info.get('skip', 0)
|
||||
take = query_info.get('take', 20)
|
||||
|
||||
persons = query.offset(skip).limit(take).all()
|
||||
|
||||
# تبدیل به دیکشنری
|
||||
items = [_person_to_dict(person) for person in persons]
|
||||
|
||||
# محاسبه اطلاعات صفحهبندی
|
||||
total_pages = (total + take - 1) // take
|
||||
current_page = (skip // take) + 1
|
||||
|
||||
pagination = {
|
||||
'total': total,
|
||||
'page': current_page,
|
||||
'per_page': take,
|
||||
'total_pages': total_pages,
|
||||
'has_next': current_page < total_pages,
|
||||
'has_prev': current_page > 1
|
||||
}
|
||||
|
||||
return {
|
||||
'items': items,
|
||||
'pagination': pagination,
|
||||
'query_info': query_info
|
||||
}
|
||||
|
||||
|
||||
def update_person(
|
||||
db: Session,
|
||||
person_id: int,
|
||||
business_id: int,
|
||||
person_data: PersonUpdateRequest
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""ویرایش شخص"""
|
||||
person = db.query(Person).filter(
|
||||
and_(Person.id == person_id, Person.business_id == business_id)
|
||||
).first()
|
||||
|
||||
if not person:
|
||||
return None
|
||||
|
||||
# بهروزرسانی فیلدها
|
||||
update_data = person_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(person, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(person)
|
||||
|
||||
return success_response(
|
||||
message="شخص با موفقیت ویرایش شد",
|
||||
data=_person_to_dict(person)
|
||||
)
|
||||
|
||||
|
||||
def delete_person(db: Session, person_id: int, business_id: int) -> bool:
|
||||
"""حذف شخص"""
|
||||
person = db.query(Person).filter(
|
||||
and_(Person.id == person_id, Person.business_id == business_id)
|
||||
).first()
|
||||
|
||||
if not person:
|
||||
return False
|
||||
|
||||
db.delete(person)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_person_summary(db: Session, business_id: int) -> Dict[str, Any]:
|
||||
"""دریافت خلاصه اشخاص"""
|
||||
# تعداد کل اشخاص
|
||||
total_persons = db.query(Person).filter(Person.business_id == business_id).count()
|
||||
|
||||
# تعداد اشخاص فعال و غیرفعال
|
||||
active_persons = db.query(Person).filter(
|
||||
and_(Person.business_id == business_id, Person.is_active == True)
|
||||
).count()
|
||||
|
||||
inactive_persons = total_persons - active_persons
|
||||
|
||||
# تعداد بر اساس نوع
|
||||
by_type = {}
|
||||
for person_type in PersonType:
|
||||
count = db.query(Person).filter(
|
||||
and_(Person.business_id == business_id, Person.person_type == person_type)
|
||||
).count()
|
||||
by_type[person_type.value] = count
|
||||
|
||||
return {
|
||||
'total_persons': total_persons,
|
||||
'by_type': by_type,
|
||||
'active_persons': active_persons,
|
||||
'inactive_persons': inactive_persons
|
||||
}
|
||||
|
||||
|
||||
def _person_to_dict(person: Person) -> Dict[str, Any]:
|
||||
"""تبدیل مدل Person به دیکشنری"""
|
||||
return {
|
||||
'id': person.id,
|
||||
'business_id': person.business_id,
|
||||
'alias_name': person.alias_name,
|
||||
'first_name': person.first_name,
|
||||
'last_name': person.last_name,
|
||||
'person_type': person.person_type.value,
|
||||
'company_name': person.company_name,
|
||||
'payment_id': person.payment_id,
|
||||
'national_id': person.national_id,
|
||||
'registration_number': person.registration_number,
|
||||
'economic_id': person.economic_id,
|
||||
'country': person.country,
|
||||
'province': person.province,
|
||||
'city': person.city,
|
||||
'address': person.address,
|
||||
'postal_code': person.postal_code,
|
||||
'phone': person.phone,
|
||||
'mobile': person.mobile,
|
||||
'fax': person.fax,
|
||||
'email': person.email,
|
||||
'website': person.website,
|
||||
'is_active': person.is_active,
|
||||
'created_at': person.created_at.isoformat(),
|
||||
'updated_at': person.updated_at.isoformat(),
|
||||
'bank_accounts': [
|
||||
{
|
||||
'id': ba.id,
|
||||
'person_id': ba.person_id,
|
||||
'bank_name': ba.bank_name,
|
||||
'account_number': ba.account_number,
|
||||
'card_number': ba.card_number,
|
||||
'sheba_number': ba.sheba_number,
|
||||
'is_active': ba.is_active,
|
||||
'created_at': ba.created_at.isoformat(),
|
||||
'updated_at': ba.updated_at.isoformat(),
|
||||
}
|
||||
for ba in person.bank_accounts
|
||||
]
|
||||
}
|
||||
117
hesabixAPI/docs/JOIN_PERMISSION_IMPLEMENTATION_SUMMARY.md
Normal file
117
hesabixAPI/docs/JOIN_PERMISSION_IMPLEMENTATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# خلاصه پیادهسازی سیستم دسترسی Join
|
||||
|
||||
## تاریخ: 2025-01-20
|
||||
|
||||
## مشکل اصلی
|
||||
کاربران فقط میتوانستند کسب و کارهای خودشان (که مالک آنها بودند) را در لیست کسب و کارها مشاهده کنند. اگر کاربری عضو کسب و کار دیگری بود، نمیتوانست آن را در لیست مشاهده کند.
|
||||
|
||||
## راهحل پیادهسازی شده
|
||||
|
||||
### 1. تعریف دسترسی `join`
|
||||
- دسترسی جدید `join: true` برای نشان دادن عضویت کاربر در کسب و کار
|
||||
- این دسترسی در فیلد `business_permissions` ذخیره میشود
|
||||
|
||||
### 2. تغییرات در بکند
|
||||
|
||||
#### AuthContext (`app/core/auth_dependency.py`)
|
||||
```python
|
||||
def is_business_member(self, business_id: int) -> bool:
|
||||
"""بررسی اینکه آیا کاربر عضو کسب و کار است یا نه (دسترسی join)"""
|
||||
```
|
||||
|
||||
#### BusinessPermissionRepository (`adapters/db/repositories/business_permission_repo.py`)
|
||||
```python
|
||||
def get_user_member_businesses(self, user_id: int) -> list[BusinessPermission]:
|
||||
"""دریافت تمام کسب و کارهایی که کاربر عضو آنها است (دسترسی join)"""
|
||||
```
|
||||
|
||||
#### BusinessService (`app/services/business_service.py`)
|
||||
```python
|
||||
def get_user_businesses(db: Session, user_id: int, query_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""دریافت لیست کسب و کارهای کاربر (مالک + عضو)"""
|
||||
```
|
||||
|
||||
#### API Endpoint (`adapters/api/v1/businesses.py`)
|
||||
- endpoint `/api/v1/businesses/list` بهروزرسانی شد
|
||||
- حالا هم کسب و کارهای مالک و هم کسب و کارهای عضو را نمایش میدهد
|
||||
|
||||
#### افزودن کاربر (`adapters/api/v1/business_users.py`)
|
||||
```python
|
||||
permission_obj = permission_repo.create_or_update(
|
||||
user_id=user.id,
|
||||
business_id=business_id,
|
||||
permissions={'join': True} # دسترسی join به طور خودکار اضافه میشود
|
||||
)
|
||||
```
|
||||
|
||||
### 3. تغییرات در فرانتاند
|
||||
|
||||
#### BusinessDashboardService (`hesabixUI/hesabix_ui/lib/services/business_dashboard_service.dart`)
|
||||
- متد `getUserBusinesses()` بهروزرسانی شد
|
||||
- حالا از API جدید استفاده میکند که هم مالک و هم عضو را پشتیبانی میکند
|
||||
|
||||
### 4. نحوه کارکرد
|
||||
|
||||
#### مالک کسب و کار
|
||||
- به طور خودکار عضو محسوب میشود
|
||||
- در لیست با نقش "مالک" نمایش داده میشود
|
||||
|
||||
#### SuperAdmin
|
||||
- به طور خودکار عضو همه کسب و کارها محسوب میشود
|
||||
|
||||
#### کاربران عضو
|
||||
- باید دسترسی `join: true` داشته باشند
|
||||
- در لیست با نقش "عضو" نمایش داده میشوند
|
||||
- میتوانند کسب و کار را در لیست مشاهده کنند
|
||||
|
||||
### 5. مثال JSON دسترسیها
|
||||
|
||||
```json
|
||||
{
|
||||
"join": true,
|
||||
"sales": {
|
||||
"read": true,
|
||||
"write": false
|
||||
},
|
||||
"reports": {
|
||||
"read": true,
|
||||
"export": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. تستهای انجام شده
|
||||
|
||||
✅ **تست 1**: افزودن دسترسی join به کاربر موجود
|
||||
✅ **تست 2**: دریافت لیست کسب و کارهای کاربر (مالک + عضو)
|
||||
✅ **تست 3**: API endpoint لیست کسب و کارها
|
||||
✅ **تست 4**: افزودن کاربر جدید به کسب و کار
|
||||
✅ **تست 5**: نمایش کسب و کار در فرانتاند
|
||||
|
||||
### 7. فایلهای تغییر یافته
|
||||
|
||||
#### بکند
|
||||
- `app/core/auth_dependency.py` - اضافه شدن متد `is_business_member`
|
||||
- `adapters/db/repositories/business_permission_repo.py` - اضافه شدن متد `get_user_member_businesses`
|
||||
- `app/services/business_service.py` - اضافه شدن متد `get_user_businesses`
|
||||
- `adapters/api/v1/businesses.py` - بهروزرسانی endpoint لیست کسب و کارها
|
||||
- `adapters/api/v1/business_users.py` - اصلاح متد افزودن کاربر
|
||||
|
||||
#### فرانتاند
|
||||
- `hesabixUI/hesabix_ui/lib/services/business_dashboard_service.dart` - بهروزرسانی متد `getUserBusinesses`
|
||||
|
||||
#### مستندات
|
||||
- `docs/JOIN_PERMISSION_SYSTEM.md` - مستندات کامل سیستم
|
||||
- `migrations/versions/20250120_000002_add_join_permission.py` - Migration
|
||||
|
||||
### 8. نتیجه نهایی
|
||||
|
||||
🎉 **مشکل حل شد!** حالا کاربران میتوانند:
|
||||
- کسب و کارهایی که مالک آنها هستند را مشاهده کنند (نقش: مالک)
|
||||
- کسب و کارهایی که عضو آنها هستند را مشاهده کنند (نقش: عضو)
|
||||
- دسترسی `join` به طور خودکار هنگام افزودن کاربر به کسب و کار اضافه میشود
|
||||
|
||||
### 9. سازگاری
|
||||
- تمام تغییرات با سیستم قبلی سازگار است
|
||||
- کاربران موجود نیازی به تغییر ندارند
|
||||
- API های موجود همچنان کار میکنند
|
||||
135
hesabixAPI/docs/JOIN_PERMISSION_SYSTEM.md
Normal file
135
hesabixAPI/docs/JOIN_PERMISSION_SYSTEM.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# سیستم دسترسی Join برای عضویت در کسب و کار
|
||||
|
||||
## خلاصه تغییرات
|
||||
|
||||
این سند توضیح میدهد که چگونه سیستم دسترسی `join` برای عضویت کاربران در کسب و کارها پیادهسازی شده است.
|
||||
|
||||
## مشکل قبلی
|
||||
|
||||
قبلاً کاربران فقط میتوانستند کسب و کارهای خودشان (که مالک آنها بودند) را در لیست کسب و کارها مشاهده کنند. اگر کاربری عضو کسب و کار دیگری بود، نمیتوانست آن را در لیست مشاهده کند.
|
||||
|
||||
## راهحل
|
||||
|
||||
### 1. تعریف دسترسی `join`
|
||||
|
||||
یک دسترسی جدید به نام `join` تعریف شده که نشاندهنده عضویت کاربر در کسب و کار است:
|
||||
|
||||
```json
|
||||
{
|
||||
"join": true,
|
||||
"sales": {
|
||||
"read": true,
|
||||
"write": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. تغییرات در بکند
|
||||
|
||||
#### AuthContext
|
||||
- متد `is_business_member()` اضافه شد
|
||||
- این متد بررسی میکند که آیا کاربر عضو کسب و کار است یا نه
|
||||
|
||||
#### BusinessPermissionRepository
|
||||
- متد `get_user_member_businesses()` اضافه شد
|
||||
- این متد کسب و کارهایی که کاربر عضو آنها است را برمیگرداند
|
||||
|
||||
#### BusinessService
|
||||
- متد `get_user_businesses()` اضافه شد
|
||||
- این متد هم کسب و کارهای مالک و هم کسب و کارهای عضو را برمیگرداند
|
||||
|
||||
#### API Endpoint
|
||||
- endpoint `/api/v1/businesses/list` بهروزرسانی شد
|
||||
- حالا هم کسب و کارهای مالک و هم کسب و کارهای عضو را نمایش میدهد
|
||||
|
||||
### 3. تغییرات در فرانتاند
|
||||
|
||||
#### BusinessDashboardService
|
||||
- متد `getUserBusinesses()` بهروزرسانی شد
|
||||
- حالا از API جدید استفاده میکند که هم مالک و هم عضو را پشتیبانی میکند
|
||||
|
||||
#### BusinessWithPermission Model
|
||||
- فیلدهای `isOwner` و `role` قبلاً وجود داشتند
|
||||
- این فیلدها برای تشخیص نقش کاربر استفاده میشوند
|
||||
|
||||
## نحوه استفاده
|
||||
|
||||
### 1. اضافه کردن کاربر به کسب و کار
|
||||
|
||||
```python
|
||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||
|
||||
permission_repo = BusinessPermissionRepository(db)
|
||||
permission_repo.create_or_update(
|
||||
user_id=user_id,
|
||||
business_id=business_id,
|
||||
permissions={'join': True, 'sales': {'read': True}}
|
||||
)
|
||||
```
|
||||
|
||||
### 2. بررسی عضویت کاربر
|
||||
|
||||
```python
|
||||
from app.core.auth_dependency import AuthContext
|
||||
|
||||
auth_ctx = AuthContext(user=user, db=db)
|
||||
is_member = auth_ctx.is_business_member(business_id)
|
||||
```
|
||||
|
||||
### 3. دریافت لیست کسب و کارهای کاربر
|
||||
|
||||
```python
|
||||
from app.services.business_service import get_user_businesses
|
||||
|
||||
result = get_user_businesses(db, user_id, query_info)
|
||||
# result['items'] شامل هم کسب و کارهای مالک و هم عضو است
|
||||
```
|
||||
|
||||
## اسکریپتهای کمکی
|
||||
|
||||
### 1. تست سیستم
|
||||
```bash
|
||||
cd hesabixAPI
|
||||
python scripts/test_business_membership.py
|
||||
```
|
||||
|
||||
### 2. اضافه کردن دسترسی join به کاربران موجود
|
||||
```bash
|
||||
cd hesabixAPI
|
||||
python scripts/add_join_permissions.py
|
||||
```
|
||||
|
||||
### 3. تست دسترسی join
|
||||
```bash
|
||||
cd hesabixAPI
|
||||
python scripts/test_join_permission.py
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
فایل migration `20250120_000002_add_join_permission.py` ایجاد شده که فقط برای مستندسازی است زیرا جدول `business_permissions` قبلاً وجود دارد و JSON field است.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
1. **مالک کسب و کار**: مالک کسب و کار به طور خودکار عضو محسوب میشود
|
||||
2. **SuperAdmin**: SuperAdmin به طور خودکار عضو همه کسب و کارها محسوب میشود
|
||||
3. **دسترسی join**: این دسترسی باید به صورت دستی برای کاربران عضو اضافه شود
|
||||
4. **سازگاری**: تغییرات با سیستم قبلی سازگار است
|
||||
|
||||
## مثال کامل
|
||||
|
||||
```python
|
||||
# ایجاد کسب و کار
|
||||
business = create_business(db, business_data, owner_id)
|
||||
|
||||
# اضافه کردن کاربر به کسب و کار
|
||||
permission_repo.create_or_update(
|
||||
user_id=member_user_id,
|
||||
business_id=business.id,
|
||||
permissions={'join': True, 'sales': {'read': True, 'write': False}}
|
||||
)
|
||||
|
||||
# دریافت لیست کسب و کارهای کاربر
|
||||
result = get_user_businesses(db, member_user_id, query_info)
|
||||
# حالا کاربر هم کسب و کارهای مالک و هم کسب و کارهای عضو را میبیند
|
||||
```
|
||||
|
|
@ -8,6 +8,7 @@ adapters/api/v1/business_dashboard.py
|
|||
adapters/api/v1/business_users.py
|
||||
adapters/api/v1/businesses.py
|
||||
adapters/api/v1/health.py
|
||||
adapters/api/v1/persons.py
|
||||
adapters/api/v1/schemas.py
|
||||
adapters/api/v1/users.py
|
||||
adapters/api/v1/admin/email_config.py
|
||||
|
|
@ -15,6 +16,7 @@ adapters/api/v1/admin/file_storage.py
|
|||
adapters/api/v1/schema_models/__init__.py
|
||||
adapters/api/v1/schema_models/email.py
|
||||
adapters/api/v1/schema_models/file_storage.py
|
||||
adapters/api/v1/schema_models/person.py
|
||||
adapters/api/v1/support/__init__.py
|
||||
adapters/api/v1/support/categories.py
|
||||
adapters/api/v1/support/operator.py
|
||||
|
|
@ -32,6 +34,7 @@ adapters/db/models/captcha.py
|
|||
adapters/db/models/email_config.py
|
||||
adapters/db/models/file_storage.py
|
||||
adapters/db/models/password_reset.py
|
||||
adapters/db/models/person.py
|
||||
adapters/db/models/user.py
|
||||
adapters/db/models/support/__init__.py
|
||||
adapters/db/models/support/category.py
|
||||
|
|
@ -75,6 +78,7 @@ app/services/business_service.py
|
|||
app/services/captcha_service.py
|
||||
app/services/email_service.py
|
||||
app/services/file_storage_service.py
|
||||
app/services/person_service.py
|
||||
app/services/query_service.py
|
||||
app/services/pdf/__init__.py
|
||||
app/services/pdf/base_pdf_service.py
|
||||
|
|
@ -94,6 +98,8 @@ migrations/versions/20250117_000006_add_app_permissions_to_users.py
|
|||
migrations/versions/20250117_000007_create_business_permissions_table.py
|
||||
migrations/versions/20250117_000008_add_email_config_table.py
|
||||
migrations/versions/20250117_000009_add_is_default_to_email_config.py
|
||||
migrations/versions/20250120_000001_add_persons_tables.py
|
||||
migrations/versions/20250120_000002_add_join_permission.py
|
||||
migrations/versions/20250915_000001_init_auth_tables.py
|
||||
migrations/versions/20250916_000002_add_referral_fields.py
|
||||
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
"""add_persons_tables
|
||||
|
||||
Revision ID: 20250120_000001
|
||||
Revises: 5553f8745c6e
|
||||
Create Date: 2025-01-20 00:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '20250120_000001'
|
||||
down_revision = '5553f8745c6e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# Create persons table
|
||||
op.create_table('persons',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('business_id', sa.Integer(), nullable=False, comment='شناسه کسب و کار'),
|
||||
sa.Column('alias_name', sa.String(length=255), nullable=False, comment='نام مستعار (الزامی)'),
|
||||
sa.Column('first_name', sa.String(length=100), nullable=True, comment='نام'),
|
||||
sa.Column('last_name', sa.String(length=100), nullable=True, comment='نام خانوادگی'),
|
||||
sa.Column('person_type', sa.Enum('CUSTOMER', 'MARKETER', 'EMPLOYEE', 'SUPPLIER', 'PARTNER', 'SELLER', name='persontype'), nullable=False, comment='نوع شخص'),
|
||||
sa.Column('company_name', sa.String(length=255), nullable=True, comment='نام شرکت'),
|
||||
sa.Column('payment_id', sa.String(length=100), nullable=True, comment='شناسه پرداخت'),
|
||||
sa.Column('national_id', sa.String(length=20), nullable=True, comment='شناسه ملی'),
|
||||
sa.Column('registration_number', sa.String(length=50), nullable=True, comment='شماره ثبت'),
|
||||
sa.Column('economic_id', sa.String(length=50), nullable=True, comment='شناسه اقتصادی'),
|
||||
sa.Column('country', sa.String(length=100), nullable=True, comment='کشور'),
|
||||
sa.Column('province', sa.String(length=100), nullable=True, comment='استان'),
|
||||
sa.Column('city', sa.String(length=100), nullable=True, comment='شهرستان'),
|
||||
sa.Column('address', sa.Text(), nullable=True, comment='آدرس'),
|
||||
sa.Column('postal_code', sa.String(length=20), nullable=True, comment='کد پستی'),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True, comment='تلفن'),
|
||||
sa.Column('mobile', sa.String(length=20), nullable=True, comment='موبایل'),
|
||||
sa.Column('fax', sa.String(length=20), nullable=True, comment='فکس'),
|
||||
sa.Column('email', sa.String(length=255), nullable=True, comment='پست الکترونیکی'),
|
||||
sa.Column('website', sa.String(length=255), nullable=True, comment='وبسایت'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, comment='وضعیت فعال بودن'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['business_id'], ['businesses.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_persons_business_id'), 'persons', ['business_id'], unique=False)
|
||||
op.create_index(op.f('ix_persons_alias_name'), 'persons', ['alias_name'], unique=False)
|
||||
op.create_index(op.f('ix_persons_national_id'), 'persons', ['national_id'], unique=False)
|
||||
|
||||
# Create person_bank_accounts table
|
||||
op.create_table('person_bank_accounts',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('person_id', sa.Integer(), nullable=False, comment='شناسه شخص'),
|
||||
sa.Column('bank_name', sa.String(length=255), nullable=False, comment='نام بانک'),
|
||||
sa.Column('account_number', sa.String(length=50), nullable=True, comment='شماره حساب'),
|
||||
sa.Column('card_number', sa.String(length=20), nullable=True, comment='شماره کارت'),
|
||||
sa.Column('sheba_number', sa.String(length=30), nullable=True, comment='شماره شبا'),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, comment='وضعیت فعال بودن'),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['person_id'], ['persons.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_person_bank_accounts_person_id'), 'person_bank_accounts', ['person_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_person_bank_accounts_person_id'), table_name='person_bank_accounts')
|
||||
op.drop_table('person_bank_accounts')
|
||||
op.drop_index(op.f('ix_persons_national_id'), table_name='persons')
|
||||
op.drop_index(op.f('ix_persons_alias_name'), table_name='persons')
|
||||
op.drop_index(op.f('ix_persons_business_id'), table_name='persons')
|
||||
op.drop_table('persons')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
"""add join permission
|
||||
|
||||
Revision ID: 20250120_000002
|
||||
Revises: 20250120_000001
|
||||
Create Date: 2025-01-20 00:00:02.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '20250120_000002'
|
||||
down_revision = '20250120_000001'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
"""Add join permission support"""
|
||||
# این migration فقط برای مستندسازی است
|
||||
# جدول business_permissions قبلاً وجود دارد و JSON field است
|
||||
# بنابراین نیازی به تغییر schema نیست
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Remove join permission support"""
|
||||
# این migration فقط برای مستندسازی است
|
||||
pass
|
||||
|
|
@ -8,6 +8,7 @@ import os
|
|||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from adapters.db.session import get_db
|
||||
from adapters.db.models.user import User
|
||||
|
||||
|
|
@ -82,7 +83,7 @@ def list_operators():
|
|||
|
||||
try:
|
||||
operators = db.query(User).filter(
|
||||
User.app_permissions['support_operator'].astext == 'true'
|
||||
text("app_permissions->>'support_operator' = 'true'")
|
||||
).all()
|
||||
|
||||
if not operators:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import 'api_client.dart';
|
||||
import '../models/business_dashboard_models.dart';
|
||||
|
||||
class AuthStore with ChangeNotifier {
|
||||
static const _kApiKey = 'auth_api_key';
|
||||
|
|
@ -11,12 +12,15 @@ class AuthStore with ChangeNotifier {
|
|||
static const _kAppPermissions = 'app_permissions';
|
||||
static const _kIsSuperAdmin = 'is_superadmin';
|
||||
static const _kLastUrl = 'last_url';
|
||||
static const _kCurrentBusiness = 'current_business';
|
||||
|
||||
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||
String? _apiKey;
|
||||
String? _deviceId;
|
||||
Map<String, dynamic>? _appPermissions;
|
||||
bool _isSuperAdmin = false;
|
||||
BusinessWithPermission? _currentBusiness;
|
||||
Map<String, dynamic>? _businessPermissions;
|
||||
|
||||
String? get apiKey => _apiKey;
|
||||
String get deviceId => _deviceId ?? '';
|
||||
|
|
@ -24,6 +28,8 @@ class AuthStore with ChangeNotifier {
|
|||
bool get isSuperAdmin => _isSuperAdmin;
|
||||
int? _currentUserId;
|
||||
int? get currentUserId => _currentUserId;
|
||||
BusinessWithPermission? get currentBusiness => _currentBusiness;
|
||||
Map<String, dynamic>? get businessPermissions => _businessPermissions;
|
||||
|
||||
Future<void> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
|
@ -234,6 +240,147 @@ class AuthStore with ChangeNotifier {
|
|||
await prefs.remove(_kLastUrl);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// مدیریت کسب و کار فعلی
|
||||
Future<void> setCurrentBusiness(BusinessWithPermission business) async {
|
||||
_currentBusiness = business;
|
||||
_businessPermissions = business.permissions;
|
||||
notifyListeners();
|
||||
|
||||
// ذخیره در حافظه محلی
|
||||
await _saveCurrentBusiness();
|
||||
}
|
||||
|
||||
Future<void> clearCurrentBusiness() async {
|
||||
_currentBusiness = null;
|
||||
_businessPermissions = null;
|
||||
notifyListeners();
|
||||
|
||||
// پاک کردن از حافظه محلی
|
||||
await _clearCurrentBusiness();
|
||||
}
|
||||
|
||||
Future<void> _saveCurrentBusiness() async {
|
||||
if (_currentBusiness == null) return;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final businessJson = const JsonEncoder().convert({
|
||||
'id': _currentBusiness!.id,
|
||||
'name': _currentBusiness!.name,
|
||||
'business_type': _currentBusiness!.businessType,
|
||||
'business_field': _currentBusiness!.businessField,
|
||||
'owner_id': _currentBusiness!.ownerId,
|
||||
'address': _currentBusiness!.address,
|
||||
'phone': _currentBusiness!.phone,
|
||||
'mobile': _currentBusiness!.mobile,
|
||||
'created_at': _currentBusiness!.createdAt,
|
||||
'is_owner': _currentBusiness!.isOwner,
|
||||
'role': _currentBusiness!.role,
|
||||
'permissions': _currentBusiness!.permissions,
|
||||
});
|
||||
|
||||
if (kIsWeb) {
|
||||
await prefs.setString(_kCurrentBusiness, businessJson);
|
||||
} else {
|
||||
try {
|
||||
await _secure.write(key: _kCurrentBusiness, value: businessJson);
|
||||
} catch (_) {
|
||||
await prefs.setString(_kCurrentBusiness, businessJson);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearCurrentBusiness() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (kIsWeb) {
|
||||
await prefs.remove(_kCurrentBusiness);
|
||||
} else {
|
||||
try {
|
||||
await _secure.delete(key: _kCurrentBusiness);
|
||||
} catch (_) {}
|
||||
await prefs.remove(_kCurrentBusiness);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail
|
||||
}
|
||||
}
|
||||
|
||||
// بررسی دسترسیهای کسب و کار
|
||||
bool hasBusinessPermission(String section, String action) {
|
||||
if (_currentBusiness?.isOwner == true) return true;
|
||||
if (_businessPermissions == null) return false;
|
||||
|
||||
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
||||
if (sectionPerms == null) return action == 'view'; // دسترسی خواندن پیشفرض
|
||||
|
||||
return sectionPerms[action] == true;
|
||||
}
|
||||
|
||||
// دسترسیهای کلی
|
||||
bool canReadSection(String section) {
|
||||
return hasBusinessPermission(section, 'view') ||
|
||||
_businessPermissions?.containsKey(section) == true;
|
||||
}
|
||||
|
||||
bool canWriteSection(String section) {
|
||||
return hasBusinessPermission(section, 'add') ||
|
||||
hasBusinessPermission(section, 'edit');
|
||||
}
|
||||
|
||||
bool canDeleteSection(String section) {
|
||||
return hasBusinessPermission(section, 'delete');
|
||||
}
|
||||
|
||||
// دسترسیهای خاص
|
||||
bool canManageDrafts(String section) {
|
||||
return hasBusinessPermission(section, 'draft');
|
||||
}
|
||||
|
||||
bool canCollectChecks() {
|
||||
return hasBusinessPermission('checks', 'collect');
|
||||
}
|
||||
|
||||
bool canTransferChecks() {
|
||||
return hasBusinessPermission('checks', 'transfer');
|
||||
}
|
||||
|
||||
bool canReturnChecks() {
|
||||
return hasBusinessPermission('checks', 'return');
|
||||
}
|
||||
|
||||
bool canChargeWallet() {
|
||||
return hasBusinessPermission('wallet', 'charge');
|
||||
}
|
||||
|
||||
bool canManageUsers() {
|
||||
return hasBusinessPermission('settings', 'users');
|
||||
}
|
||||
|
||||
// بررسی دسترسی به کسب و کار
|
||||
bool canAccessBusiness(int businessId) {
|
||||
if (_currentBusiness == null) return false;
|
||||
return _currentBusiness!.id == businessId;
|
||||
}
|
||||
|
||||
// دریافت دسترسیهای موجود برای یک بخش
|
||||
List<String> getAvailableActions(String section) {
|
||||
if (_currentBusiness?.isOwner == true) {
|
||||
return ['add', 'view', 'edit', 'delete', 'draft', 'collect', 'transfer', 'return', 'charge'];
|
||||
}
|
||||
|
||||
if (_businessPermissions == null) return ['view'];
|
||||
|
||||
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
||||
if (sectionPerms == null) return ['view'];
|
||||
|
||||
return sectionPerms.keys.where((key) => sectionPerms[key] == true).toList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
217
hesabixUI/hesabix_ui/lib/examples/permission_usage_example.dart
Normal file
217
hesabixUI/hesabix_ui/lib/examples/permission_usage_example.dart
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../core/auth_store.dart';
|
||||
import '../widgets/permission/permission_widgets.dart';
|
||||
|
||||
/// مثال کامل از نحوه استفاده از سیستم دسترسیها
|
||||
class PermissionUsageExample extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionUsageExample({
|
||||
super.key,
|
||||
required this.authStore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('مثال استفاده از سیستم دسترسیها'),
|
||||
actions: [
|
||||
// دکمه اضافه کردن فقط در صورت داشتن دسترسی
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'add',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _addPerson(),
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'اضافه کردن شخص',
|
||||
),
|
||||
),
|
||||
|
||||
// دکمه ویرایش فقط در صورت داشتن دسترسی
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'edit',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _editPerson(),
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'ویرایش شخص',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// لیست اشخاص با بررسی دسترسیها
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: 10, // مثال
|
||||
itemBuilder: (context, index) {
|
||||
return PermissionListTile(
|
||||
section: 'people',
|
||||
action: 'view',
|
||||
authStore: authStore,
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Icons.person),
|
||||
),
|
||||
title: Text('شخص ${index + 1}'),
|
||||
subtitle: const Text('توضیحات شخص'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// دکمه ویرایش
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'edit',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _editPerson(),
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: 'ویرایش',
|
||||
),
|
||||
),
|
||||
|
||||
// دکمه حذف
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'delete',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _deletePerson(),
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: 'حذف',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// دکمههای عملیات
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
// دکمه گزارش
|
||||
PermissionButton(
|
||||
section: 'reports',
|
||||
action: 'view',
|
||||
authStore: authStore,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _viewReports(),
|
||||
icon: const Icon(Icons.assessment),
|
||||
label: const Text('مشاهده گزارشها'),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// دکمه صادرات
|
||||
PermissionButton(
|
||||
section: 'reports',
|
||||
action: 'export',
|
||||
authStore: authStore,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _exportReports(),
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('صادرات گزارش'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _addPerson() {
|
||||
// منطق اضافه کردن شخص
|
||||
print('اضافه کردن شخص جدید');
|
||||
}
|
||||
|
||||
void _editPerson() {
|
||||
// منطق ویرایش شخص
|
||||
print('ویرایش شخص');
|
||||
}
|
||||
|
||||
void _deletePerson() {
|
||||
// منطق حذف شخص
|
||||
print('حذف شخص');
|
||||
}
|
||||
|
||||
void _viewReports() {
|
||||
// منطق مشاهده گزارشها
|
||||
print('مشاهده گزارشها');
|
||||
}
|
||||
|
||||
void _exportReports() {
|
||||
// منطق صادرات گزارش
|
||||
print('صادرات گزارش');
|
||||
}
|
||||
}
|
||||
|
||||
/// مثال استفاده از PermissionWidget
|
||||
class ExamplePermissionWidget extends StatelessWidget {
|
||||
final AuthStore authStore;
|
||||
|
||||
const ExamplePermissionWidget({
|
||||
super.key,
|
||||
required this.authStore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// نمایش ویجت فقط در صورت داشتن دسترسی
|
||||
PermissionWidget(
|
||||
section: 'settings',
|
||||
action: 'view',
|
||||
authStore: authStore,
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: const Text('تنظیمات'),
|
||||
subtitle: const Text('مدیریت تنظیمات سیستم'),
|
||||
onTap: () => _openSettings(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// نمایش پیام عدم دسترسی در صورت عدم دسترسی
|
||||
PermissionWidget(
|
||||
section: 'admin',
|
||||
action: 'view',
|
||||
authStore: authStore,
|
||||
fallbackWidget: const AccessDeniedWidget(
|
||||
message: 'شما دسترسی لازم برای مشاهده پنل مدیریت را ندارید',
|
||||
icon: Icons.admin_panel_settings,
|
||||
),
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.admin_panel_settings),
|
||||
title: const Text('پنل مدیریت'),
|
||||
subtitle: const Text('دسترسی به پنل مدیریت'),
|
||||
onTap: () => _openAdminPanel(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _openSettings() {
|
||||
print('باز کردن تنظیمات');
|
||||
}
|
||||
|
||||
void _openAdminPanel() {
|
||||
print('باز کردن پنل مدیریت');
|
||||
}
|
||||
}
|
||||
|
|
@ -491,6 +491,7 @@
|
|||
"peopleList": "People List",
|
||||
"receipts": "Receipts",
|
||||
"payments": "Payments",
|
||||
"receiptsAndPayments": "Receipts and Payments",
|
||||
"productsAndServices": "Products and Services",
|
||||
"products": "Products",
|
||||
"services": "Services",
|
||||
|
|
@ -777,6 +778,65 @@
|
|||
"dataBackupDialogContent": "In this section you can create a backup of all business data.",
|
||||
"dataRestoreDialogContent": "In this section you can restore data from a previous backup.",
|
||||
"systemLogsDialogContent": "In this section you can view system reports, errors and user activities.",
|
||||
"accountManagement": "Account Management"
|
||||
"accountManagement": "Account Management",
|
||||
"persons": "Persons",
|
||||
"personsList": "Persons List",
|
||||
"addPerson": "Add Person",
|
||||
"editPerson": "Edit Person",
|
||||
"personDetails": "Person Details",
|
||||
"deletePerson": "Delete Person",
|
||||
"personAliasName": "Alias Name",
|
||||
"personFirstName": "First Name",
|
||||
"personLastName": "Last Name",
|
||||
"personType": "Person Type",
|
||||
"personCompanyName": "Company Name",
|
||||
"personPaymentId": "Payment ID",
|
||||
"personNationalId": "National ID",
|
||||
"personRegistrationNumber": "Registration Number",
|
||||
"personEconomicId": "Economic ID",
|
||||
"personCountry": "Country",
|
||||
"personProvince": "Province",
|
||||
"personCity": "City",
|
||||
"personAddress": "Address",
|
||||
"personPostalCode": "Postal Code",
|
||||
"personPhone": "Phone",
|
||||
"personMobile": "Mobile",
|
||||
"personFax": "Fax",
|
||||
"personEmail": "Email",
|
||||
"personWebsite": "Website",
|
||||
"personBankAccounts": "Bank Accounts",
|
||||
"addBankAccount": "Add Bank Account",
|
||||
"editBankAccount": "Edit Bank Account",
|
||||
"deleteBankAccount": "Delete Bank Account",
|
||||
"bankName": "Bank Name",
|
||||
"accountNumber": "Account Number",
|
||||
"cardNumber": "Card Number",
|
||||
"shebaNumber": "Sheba Number",
|
||||
"personTypeCustomer": "Customer",
|
||||
"personTypeMarketer": "Marketer",
|
||||
"personTypeEmployee": "Employee",
|
||||
"personTypeSupplier": "Supplier",
|
||||
"personTypePartner": "Partner",
|
||||
"personTypeSeller": "Seller",
|
||||
"personCreatedSuccessfully": "Person created successfully",
|
||||
"personUpdatedSuccessfully": "Person updated successfully",
|
||||
"personDeletedSuccessfully": "Person deleted successfully",
|
||||
"personNotFound": "Person not found",
|
||||
"personAliasNameRequired": "Alias name is required",
|
||||
"personTypeRequired": "Person type is required",
|
||||
"bankAccountAddedSuccessfully": "Bank account added successfully",
|
||||
"bankAccountUpdatedSuccessfully": "Bank account updated successfully",
|
||||
"bankAccountDeletedSuccessfully": "Bank account deleted successfully",
|
||||
"bankNameRequired": "Bank name is required",
|
||||
"personBasicInfo": "Basic Information",
|
||||
"personEconomicInfo": "Economic Information",
|
||||
"personContactInfo": "Contact Information",
|
||||
"personBankInfo": "Bank Accounts",
|
||||
"personSummary": "Persons Summary",
|
||||
"totalPersons": "Total Persons",
|
||||
"activePersons": "Active Persons",
|
||||
"inactivePersons": "Inactive Persons",
|
||||
"personsByType": "Persons by Type",
|
||||
"update": "Update"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -490,6 +490,7 @@
|
|||
"peopleList": "لیست اشخاص",
|
||||
"receipts": "دریافتها",
|
||||
"payments": "پرداختها",
|
||||
"receiptsAndPayments": "دریافت و پرداخت",
|
||||
"productsAndServices": "کالا و خدمات",
|
||||
"products": "کالاها",
|
||||
"services": "خدمات",
|
||||
|
|
@ -776,6 +777,65 @@
|
|||
"dataBackupDialogContent": "در این بخش میتوانید از تمام اطلاعات کسب و کار نسخه پشتیبان تهیه کنید.",
|
||||
"dataRestoreDialogContent": "در این بخش میتوانید اطلاعات را از نسخه پشتیبان قبلی بازیابی کنید.",
|
||||
"systemLogsDialogContent": "در این بخش میتوانید گزارشهای سیستم، خطاها و فعالیتهای کاربران را مشاهده کنید.",
|
||||
"accountManagement": "مدیریت حساب کاربری"
|
||||
"accountManagement": "مدیریت حساب کاربری",
|
||||
"persons": "اشخاص",
|
||||
"personsList": "لیست اشخاص",
|
||||
"addPerson": "افزودن شخص",
|
||||
"editPerson": "ویرایش شخص",
|
||||
"personDetails": "جزئیات شخص",
|
||||
"deletePerson": "حذف شخص",
|
||||
"personAliasName": "نام مستعار",
|
||||
"personFirstName": "نام",
|
||||
"personLastName": "نام خانوادگی",
|
||||
"personType": "نوع شخص",
|
||||
"personCompanyName": "نام شرکت",
|
||||
"personPaymentId": "شناسه پرداخت",
|
||||
"personNationalId": "شناسه ملی",
|
||||
"personRegistrationNumber": "شماره ثبت",
|
||||
"personEconomicId": "شناسه اقتصادی",
|
||||
"personCountry": "کشور",
|
||||
"personProvince": "استان",
|
||||
"personCity": "شهرستان",
|
||||
"personAddress": "آدرس",
|
||||
"personPostalCode": "کد پستی",
|
||||
"personPhone": "تلفن",
|
||||
"personMobile": "موبایل",
|
||||
"personFax": "فکس",
|
||||
"personEmail": "پست الکترونیکی",
|
||||
"personWebsite": "وبسایت",
|
||||
"personBankAccounts": "حسابهای بانکی",
|
||||
"addBankAccount": "افزودن حساب بانکی",
|
||||
"editBankAccount": "ویرایش حساب بانکی",
|
||||
"deleteBankAccount": "حذف حساب بانکی",
|
||||
"bankName": "نام بانک",
|
||||
"accountNumber": "شماره حساب",
|
||||
"cardNumber": "شماره کارت",
|
||||
"shebaNumber": "شماره شبا",
|
||||
"personTypeCustomer": "مشتری",
|
||||
"personTypeMarketer": "بازاریاب",
|
||||
"personTypeEmployee": "کارمند",
|
||||
"personTypeSupplier": "تامینکننده",
|
||||
"personTypePartner": "همکار",
|
||||
"personTypeSeller": "فروشنده",
|
||||
"personCreatedSuccessfully": "شخص با موفقیت ایجاد شد",
|
||||
"personUpdatedSuccessfully": "شخص با موفقیت ویرایش شد",
|
||||
"personDeletedSuccessfully": "شخص با موفقیت حذف شد",
|
||||
"personNotFound": "شخص یافت نشد",
|
||||
"personAliasNameRequired": "نام مستعار الزامی است",
|
||||
"personTypeRequired": "نوع شخص الزامی است",
|
||||
"bankAccountAddedSuccessfully": "حساب بانکی با موفقیت اضافه شد",
|
||||
"bankAccountUpdatedSuccessfully": "حساب بانکی با موفقیت ویرایش شد",
|
||||
"bankAccountDeletedSuccessfully": "حساب بانکی با موفقیت حذف شد",
|
||||
"bankNameRequired": "نام بانک الزامی است",
|
||||
"personBasicInfo": "اطلاعات پایه",
|
||||
"personEconomicInfo": "اطلاعات اقتصادی",
|
||||
"personContactInfo": "اطلاعات تماس",
|
||||
"personBankInfo": "حسابهای بانکی",
|
||||
"personSummary": "خلاصه اشخاص",
|
||||
"totalPersons": "تعداد کل اشخاص",
|
||||
"activePersons": "اشخاص فعال",
|
||||
"inactivePersons": "اشخاص غیرفعال",
|
||||
"personsByType": "اشخاص بر اساس نوع",
|
||||
"update": "ویرایش"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2756,6 +2756,12 @@ abstract class AppLocalizations {
|
|||
/// **'Payments'**
|
||||
String get payments;
|
||||
|
||||
/// No description provided for @receiptsAndPayments.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Receipts and Payments'**
|
||||
String get receiptsAndPayments;
|
||||
|
||||
/// No description provided for @productsAndServices.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -3323,7 +3329,7 @@ abstract class AppLocalizations {
|
|||
/// No description provided for @addPerson.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add New Person'**
|
||||
/// **'Add Person'**
|
||||
String get addPerson;
|
||||
|
||||
/// No description provided for @viewPeople.
|
||||
|
|
@ -4219,6 +4225,348 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Account Management'**
|
||||
String get accountManagement;
|
||||
|
||||
/// No description provided for @persons.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Persons'**
|
||||
String get persons;
|
||||
|
||||
/// No description provided for @personsList.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Persons List'**
|
||||
String get personsList;
|
||||
|
||||
/// No description provided for @editPerson.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Person'**
|
||||
String get editPerson;
|
||||
|
||||
/// No description provided for @personDetails.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person Details'**
|
||||
String get personDetails;
|
||||
|
||||
/// No description provided for @deletePerson.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Person'**
|
||||
String get deletePerson;
|
||||
|
||||
/// No description provided for @personAliasName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Alias Name'**
|
||||
String get personAliasName;
|
||||
|
||||
/// No description provided for @personFirstName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'First Name'**
|
||||
String get personFirstName;
|
||||
|
||||
/// No description provided for @personLastName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last Name'**
|
||||
String get personLastName;
|
||||
|
||||
/// No description provided for @personType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person Type'**
|
||||
String get personType;
|
||||
|
||||
/// No description provided for @personCompanyName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Company Name'**
|
||||
String get personCompanyName;
|
||||
|
||||
/// No description provided for @personPaymentId.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Payment ID'**
|
||||
String get personPaymentId;
|
||||
|
||||
/// No description provided for @personNationalId.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'National ID'**
|
||||
String get personNationalId;
|
||||
|
||||
/// No description provided for @personRegistrationNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Registration Number'**
|
||||
String get personRegistrationNumber;
|
||||
|
||||
/// No description provided for @personEconomicId.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Economic ID'**
|
||||
String get personEconomicId;
|
||||
|
||||
/// No description provided for @personCountry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Country'**
|
||||
String get personCountry;
|
||||
|
||||
/// No description provided for @personProvince.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Province'**
|
||||
String get personProvince;
|
||||
|
||||
/// No description provided for @personCity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'City'**
|
||||
String get personCity;
|
||||
|
||||
/// No description provided for @personAddress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Address'**
|
||||
String get personAddress;
|
||||
|
||||
/// No description provided for @personPostalCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Postal Code'**
|
||||
String get personPostalCode;
|
||||
|
||||
/// No description provided for @personPhone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Phone'**
|
||||
String get personPhone;
|
||||
|
||||
/// No description provided for @personMobile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mobile'**
|
||||
String get personMobile;
|
||||
|
||||
/// No description provided for @personFax.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fax'**
|
||||
String get personFax;
|
||||
|
||||
/// No description provided for @personEmail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email'**
|
||||
String get personEmail;
|
||||
|
||||
/// No description provided for @personWebsite.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Website'**
|
||||
String get personWebsite;
|
||||
|
||||
/// No description provided for @personBankAccounts.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank Accounts'**
|
||||
String get personBankAccounts;
|
||||
|
||||
/// No description provided for @editBankAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Bank Account'**
|
||||
String get editBankAccount;
|
||||
|
||||
/// No description provided for @deleteBankAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Bank Account'**
|
||||
String get deleteBankAccount;
|
||||
|
||||
/// No description provided for @bankName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank Name'**
|
||||
String get bankName;
|
||||
|
||||
/// No description provided for @accountNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Account Number'**
|
||||
String get accountNumber;
|
||||
|
||||
/// No description provided for @cardNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Card Number'**
|
||||
String get cardNumber;
|
||||
|
||||
/// No description provided for @shebaNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sheba Number'**
|
||||
String get shebaNumber;
|
||||
|
||||
/// No description provided for @personTypeCustomer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Customer'**
|
||||
String get personTypeCustomer;
|
||||
|
||||
/// No description provided for @personTypeMarketer.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Marketer'**
|
||||
String get personTypeMarketer;
|
||||
|
||||
/// No description provided for @personTypeEmployee.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Employee'**
|
||||
String get personTypeEmployee;
|
||||
|
||||
/// No description provided for @personTypeSupplier.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Supplier'**
|
||||
String get personTypeSupplier;
|
||||
|
||||
/// No description provided for @personTypePartner.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Partner'**
|
||||
String get personTypePartner;
|
||||
|
||||
/// No description provided for @personTypeSeller.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Seller'**
|
||||
String get personTypeSeller;
|
||||
|
||||
/// No description provided for @personCreatedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person created successfully'**
|
||||
String get personCreatedSuccessfully;
|
||||
|
||||
/// No description provided for @personUpdatedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person updated successfully'**
|
||||
String get personUpdatedSuccessfully;
|
||||
|
||||
/// No description provided for @personDeletedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person deleted successfully'**
|
||||
String get personDeletedSuccessfully;
|
||||
|
||||
/// No description provided for @personNotFound.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person not found'**
|
||||
String get personNotFound;
|
||||
|
||||
/// No description provided for @personAliasNameRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Alias name is required'**
|
||||
String get personAliasNameRequired;
|
||||
|
||||
/// No description provided for @personTypeRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Person type is required'**
|
||||
String get personTypeRequired;
|
||||
|
||||
/// No description provided for @bankAccountAddedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank account added successfully'**
|
||||
String get bankAccountAddedSuccessfully;
|
||||
|
||||
/// No description provided for @bankAccountUpdatedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank account updated successfully'**
|
||||
String get bankAccountUpdatedSuccessfully;
|
||||
|
||||
/// No description provided for @bankAccountDeletedSuccessfully.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank account deleted successfully'**
|
||||
String get bankAccountDeletedSuccessfully;
|
||||
|
||||
/// No description provided for @bankNameRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank name is required'**
|
||||
String get bankNameRequired;
|
||||
|
||||
/// No description provided for @personBasicInfo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Basic Information'**
|
||||
String get personBasicInfo;
|
||||
|
||||
/// No description provided for @personEconomicInfo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Economic Information'**
|
||||
String get personEconomicInfo;
|
||||
|
||||
/// No description provided for @personContactInfo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Contact Information'**
|
||||
String get personContactInfo;
|
||||
|
||||
/// No description provided for @personBankInfo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bank Accounts'**
|
||||
String get personBankInfo;
|
||||
|
||||
/// No description provided for @personSummary.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Persons Summary'**
|
||||
String get personSummary;
|
||||
|
||||
/// No description provided for @totalPersons.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total Persons'**
|
||||
String get totalPersons;
|
||||
|
||||
/// No description provided for @activePersons.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Active Persons'**
|
||||
String get activePersons;
|
||||
|
||||
/// No description provided for @inactivePersons.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Inactive Persons'**
|
||||
String get inactivePersons;
|
||||
|
||||
/// No description provided for @personsByType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Persons by Type'**
|
||||
String get personsByType;
|
||||
|
||||
/// No description provided for @update.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Update'**
|
||||
String get update;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1379,6 +1379,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get payments => 'Payments';
|
||||
|
||||
@override
|
||||
String get receiptsAndPayments => 'Receipts and Payments';
|
||||
|
||||
@override
|
||||
String get productsAndServices => 'Products and Services';
|
||||
|
||||
|
|
@ -1665,7 +1668,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get draft => 'Manage Drafts';
|
||||
|
||||
@override
|
||||
String get addPerson => 'Add New Person';
|
||||
String get addPerson => 'Add Person';
|
||||
|
||||
@override
|
||||
String get viewPeople => 'View People List';
|
||||
|
|
@ -2126,4 +2129,177 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get accountManagement => 'Account Management';
|
||||
|
||||
@override
|
||||
String get persons => 'Persons';
|
||||
|
||||
@override
|
||||
String get personsList => 'Persons List';
|
||||
|
||||
@override
|
||||
String get editPerson => 'Edit Person';
|
||||
|
||||
@override
|
||||
String get personDetails => 'Person Details';
|
||||
|
||||
@override
|
||||
String get deletePerson => 'Delete Person';
|
||||
|
||||
@override
|
||||
String get personAliasName => 'Alias Name';
|
||||
|
||||
@override
|
||||
String get personFirstName => 'First Name';
|
||||
|
||||
@override
|
||||
String get personLastName => 'Last Name';
|
||||
|
||||
@override
|
||||
String get personType => 'Person Type';
|
||||
|
||||
@override
|
||||
String get personCompanyName => 'Company Name';
|
||||
|
||||
@override
|
||||
String get personPaymentId => 'Payment ID';
|
||||
|
||||
@override
|
||||
String get personNationalId => 'National ID';
|
||||
|
||||
@override
|
||||
String get personRegistrationNumber => 'Registration Number';
|
||||
|
||||
@override
|
||||
String get personEconomicId => 'Economic ID';
|
||||
|
||||
@override
|
||||
String get personCountry => 'Country';
|
||||
|
||||
@override
|
||||
String get personProvince => 'Province';
|
||||
|
||||
@override
|
||||
String get personCity => 'City';
|
||||
|
||||
@override
|
||||
String get personAddress => 'Address';
|
||||
|
||||
@override
|
||||
String get personPostalCode => 'Postal Code';
|
||||
|
||||
@override
|
||||
String get personPhone => 'Phone';
|
||||
|
||||
@override
|
||||
String get personMobile => 'Mobile';
|
||||
|
||||
@override
|
||||
String get personFax => 'Fax';
|
||||
|
||||
@override
|
||||
String get personEmail => 'Email';
|
||||
|
||||
@override
|
||||
String get personWebsite => 'Website';
|
||||
|
||||
@override
|
||||
String get personBankAccounts => 'Bank Accounts';
|
||||
|
||||
@override
|
||||
String get editBankAccount => 'Edit Bank Account';
|
||||
|
||||
@override
|
||||
String get deleteBankAccount => 'Delete Bank Account';
|
||||
|
||||
@override
|
||||
String get bankName => 'Bank Name';
|
||||
|
||||
@override
|
||||
String get accountNumber => 'Account Number';
|
||||
|
||||
@override
|
||||
String get cardNumber => 'Card Number';
|
||||
|
||||
@override
|
||||
String get shebaNumber => 'Sheba Number';
|
||||
|
||||
@override
|
||||
String get personTypeCustomer => 'Customer';
|
||||
|
||||
@override
|
||||
String get personTypeMarketer => 'Marketer';
|
||||
|
||||
@override
|
||||
String get personTypeEmployee => 'Employee';
|
||||
|
||||
@override
|
||||
String get personTypeSupplier => 'Supplier';
|
||||
|
||||
@override
|
||||
String get personTypePartner => 'Partner';
|
||||
|
||||
@override
|
||||
String get personTypeSeller => 'Seller';
|
||||
|
||||
@override
|
||||
String get personCreatedSuccessfully => 'Person created successfully';
|
||||
|
||||
@override
|
||||
String get personUpdatedSuccessfully => 'Person updated successfully';
|
||||
|
||||
@override
|
||||
String get personDeletedSuccessfully => 'Person deleted successfully';
|
||||
|
||||
@override
|
||||
String get personNotFound => 'Person not found';
|
||||
|
||||
@override
|
||||
String get personAliasNameRequired => 'Alias name is required';
|
||||
|
||||
@override
|
||||
String get personTypeRequired => 'Person type is required';
|
||||
|
||||
@override
|
||||
String get bankAccountAddedSuccessfully => 'Bank account added successfully';
|
||||
|
||||
@override
|
||||
String get bankAccountUpdatedSuccessfully =>
|
||||
'Bank account updated successfully';
|
||||
|
||||
@override
|
||||
String get bankAccountDeletedSuccessfully =>
|
||||
'Bank account deleted successfully';
|
||||
|
||||
@override
|
||||
String get bankNameRequired => 'Bank name is required';
|
||||
|
||||
@override
|
||||
String get personBasicInfo => 'Basic Information';
|
||||
|
||||
@override
|
||||
String get personEconomicInfo => 'Economic Information';
|
||||
|
||||
@override
|
||||
String get personContactInfo => 'Contact Information';
|
||||
|
||||
@override
|
||||
String get personBankInfo => 'Bank Accounts';
|
||||
|
||||
@override
|
||||
String get personSummary => 'Persons Summary';
|
||||
|
||||
@override
|
||||
String get totalPersons => 'Total Persons';
|
||||
|
||||
@override
|
||||
String get activePersons => 'Active Persons';
|
||||
|
||||
@override
|
||||
String get inactivePersons => 'Inactive Persons';
|
||||
|
||||
@override
|
||||
String get personsByType => 'Persons by Type';
|
||||
|
||||
@override
|
||||
String get update => 'Update';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1368,6 +1368,9 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
@override
|
||||
String get payments => 'پرداختها';
|
||||
|
||||
@override
|
||||
String get receiptsAndPayments => 'دریافت و پرداخت';
|
||||
|
||||
@override
|
||||
String get productsAndServices => 'کالا و خدمات';
|
||||
|
||||
|
|
@ -1655,7 +1658,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
String get draft => 'مدیریت پیشنویسها';
|
||||
|
||||
@override
|
||||
String get addPerson => 'افزودن شخص جدید';
|
||||
String get addPerson => 'افزودن شخص';
|
||||
|
||||
@override
|
||||
String get viewPeople => 'مشاهده لیست اشخاص';
|
||||
|
|
@ -2112,4 +2115,175 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get accountManagement => 'مدیریت حساب کاربری';
|
||||
|
||||
@override
|
||||
String get persons => 'اشخاص';
|
||||
|
||||
@override
|
||||
String get personsList => 'لیست اشخاص';
|
||||
|
||||
@override
|
||||
String get editPerson => 'ویرایش شخص';
|
||||
|
||||
@override
|
||||
String get personDetails => 'جزئیات شخص';
|
||||
|
||||
@override
|
||||
String get deletePerson => 'حذف شخص';
|
||||
|
||||
@override
|
||||
String get personAliasName => 'نام مستعار';
|
||||
|
||||
@override
|
||||
String get personFirstName => 'نام';
|
||||
|
||||
@override
|
||||
String get personLastName => 'نام خانوادگی';
|
||||
|
||||
@override
|
||||
String get personType => 'نوع شخص';
|
||||
|
||||
@override
|
||||
String get personCompanyName => 'نام شرکت';
|
||||
|
||||
@override
|
||||
String get personPaymentId => 'شناسه پرداخت';
|
||||
|
||||
@override
|
||||
String get personNationalId => 'شناسه ملی';
|
||||
|
||||
@override
|
||||
String get personRegistrationNumber => 'شماره ثبت';
|
||||
|
||||
@override
|
||||
String get personEconomicId => 'شناسه اقتصادی';
|
||||
|
||||
@override
|
||||
String get personCountry => 'کشور';
|
||||
|
||||
@override
|
||||
String get personProvince => 'استان';
|
||||
|
||||
@override
|
||||
String get personCity => 'شهرستان';
|
||||
|
||||
@override
|
||||
String get personAddress => 'آدرس';
|
||||
|
||||
@override
|
||||
String get personPostalCode => 'کد پستی';
|
||||
|
||||
@override
|
||||
String get personPhone => 'تلفن';
|
||||
|
||||
@override
|
||||
String get personMobile => 'موبایل';
|
||||
|
||||
@override
|
||||
String get personFax => 'فکس';
|
||||
|
||||
@override
|
||||
String get personEmail => 'پست الکترونیکی';
|
||||
|
||||
@override
|
||||
String get personWebsite => 'وبسایت';
|
||||
|
||||
@override
|
||||
String get personBankAccounts => 'حسابهای بانکی';
|
||||
|
||||
@override
|
||||
String get editBankAccount => 'ویرایش حساب بانکی';
|
||||
|
||||
@override
|
||||
String get deleteBankAccount => 'حذف حساب بانکی';
|
||||
|
||||
@override
|
||||
String get bankName => 'نام بانک';
|
||||
|
||||
@override
|
||||
String get accountNumber => 'شماره حساب';
|
||||
|
||||
@override
|
||||
String get cardNumber => 'شماره کارت';
|
||||
|
||||
@override
|
||||
String get shebaNumber => 'شماره شبا';
|
||||
|
||||
@override
|
||||
String get personTypeCustomer => 'مشتری';
|
||||
|
||||
@override
|
||||
String get personTypeMarketer => 'بازاریاب';
|
||||
|
||||
@override
|
||||
String get personTypeEmployee => 'کارمند';
|
||||
|
||||
@override
|
||||
String get personTypeSupplier => 'تامینکننده';
|
||||
|
||||
@override
|
||||
String get personTypePartner => 'همکار';
|
||||
|
||||
@override
|
||||
String get personTypeSeller => 'فروشنده';
|
||||
|
||||
@override
|
||||
String get personCreatedSuccessfully => 'شخص با موفقیت ایجاد شد';
|
||||
|
||||
@override
|
||||
String get personUpdatedSuccessfully => 'شخص با موفقیت ویرایش شد';
|
||||
|
||||
@override
|
||||
String get personDeletedSuccessfully => 'شخص با موفقیت حذف شد';
|
||||
|
||||
@override
|
||||
String get personNotFound => 'شخص یافت نشد';
|
||||
|
||||
@override
|
||||
String get personAliasNameRequired => 'نام مستعار الزامی است';
|
||||
|
||||
@override
|
||||
String get personTypeRequired => 'نوع شخص الزامی است';
|
||||
|
||||
@override
|
||||
String get bankAccountAddedSuccessfully => 'حساب بانکی با موفقیت اضافه شد';
|
||||
|
||||
@override
|
||||
String get bankAccountUpdatedSuccessfully => 'حساب بانکی با موفقیت ویرایش شد';
|
||||
|
||||
@override
|
||||
String get bankAccountDeletedSuccessfully => 'حساب بانکی با موفقیت حذف شد';
|
||||
|
||||
@override
|
||||
String get bankNameRequired => 'نام بانک الزامی است';
|
||||
|
||||
@override
|
||||
String get personBasicInfo => 'اطلاعات پایه';
|
||||
|
||||
@override
|
||||
String get personEconomicInfo => 'اطلاعات اقتصادی';
|
||||
|
||||
@override
|
||||
String get personContactInfo => 'اطلاعات تماس';
|
||||
|
||||
@override
|
||||
String get personBankInfo => 'حسابهای بانکی';
|
||||
|
||||
@override
|
||||
String get personSummary => 'خلاصه اشخاص';
|
||||
|
||||
@override
|
||||
String get totalPersons => 'تعداد کل اشخاص';
|
||||
|
||||
@override
|
||||
String get activePersons => 'اشخاص فعال';
|
||||
|
||||
@override
|
||||
String get inactivePersons => 'اشخاص غیرفعال';
|
||||
|
||||
@override
|
||||
String get personsByType => 'اشخاص بر اساس نوع';
|
||||
|
||||
@override
|
||||
String get update => 'ویرایش';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import 'pages/business/business_shell.dart';
|
|||
import 'pages/business/dashboard/business_dashboard_page.dart';
|
||||
import 'pages/business/users_permissions_page.dart';
|
||||
import 'pages/business/settings_page.dart';
|
||||
import 'pages/business/persons_page.dart';
|
||||
import 'pages/error_404_page.dart';
|
||||
import 'core/locale_controller.dart';
|
||||
import 'core/calendar_controller.dart';
|
||||
import 'core/api_client.dart';
|
||||
|
|
@ -516,10 +518,35 @@ class _MyAppState extends State<MyApp> {
|
|||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: 'persons',
|
||||
name: 'business_persons',
|
||||
builder: (context, state) {
|
||||
final businessId = int.parse(state.pathParameters['business_id']!);
|
||||
return BusinessShell(
|
||||
businessId: businessId,
|
||||
authStore: _authStore!,
|
||||
localeController: controller,
|
||||
calendarController: _calendarController!,
|
||||
themeController: themeController,
|
||||
child: PersonsPage(
|
||||
businessId: businessId,
|
||||
authStore: _authStore!,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// TODO: Add other business routes (sales, accounting, etc.)
|
||||
],
|
||||
),
|
||||
// صفحه 404 برای مسیرهای نامعتبر
|
||||
GoRoute(
|
||||
path: '/404',
|
||||
name: 'error_404',
|
||||
builder: (context, state) => const Error404Page(),
|
||||
),
|
||||
],
|
||||
errorBuilder: (context, state) => const Error404Page(),
|
||||
);
|
||||
|
||||
return AnimatedBuilder(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,428 @@
|
|||
class PersonBankAccount {
|
||||
final int? id;
|
||||
final int personId;
|
||||
final String bankName;
|
||||
final String? accountNumber;
|
||||
final String? cardNumber;
|
||||
final String? shebaNumber;
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
PersonBankAccount({
|
||||
this.id,
|
||||
required this.personId,
|
||||
required this.bankName,
|
||||
this.accountNumber,
|
||||
this.cardNumber,
|
||||
this.shebaNumber,
|
||||
this.isActive = true,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory PersonBankAccount.fromJson(Map<String, dynamic> json) {
|
||||
return PersonBankAccount(
|
||||
id: json['id'],
|
||||
personId: json['person_id'],
|
||||
bankName: json['bank_name'],
|
||||
accountNumber: json['account_number'],
|
||||
cardNumber: json['card_number'],
|
||||
shebaNumber: json['sheba_number'],
|
||||
isActive: json['is_active'] ?? true,
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'person_id': personId,
|
||||
'bank_name': bankName,
|
||||
'account_number': accountNumber,
|
||||
'card_number': cardNumber,
|
||||
'sheba_number': shebaNumber,
|
||||
'is_active': isActive,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
PersonBankAccount copyWith({
|
||||
int? id,
|
||||
int? personId,
|
||||
String? bankName,
|
||||
String? accountNumber,
|
||||
String? cardNumber,
|
||||
String? shebaNumber,
|
||||
bool? isActive,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return PersonBankAccount(
|
||||
id: id ?? this.id,
|
||||
personId: personId ?? this.personId,
|
||||
bankName: bankName ?? this.bankName,
|
||||
accountNumber: accountNumber ?? this.accountNumber,
|
||||
cardNumber: cardNumber ?? this.cardNumber,
|
||||
shebaNumber: shebaNumber ?? this.shebaNumber,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PersonType {
|
||||
customer('مشتری', 'Customer'),
|
||||
marketer('بازاریاب', 'Marketer'),
|
||||
employee('کارمند', 'Employee'),
|
||||
supplier('تامینکننده', 'Supplier'),
|
||||
partner('همکار', 'Partner'),
|
||||
seller('فروشنده', 'Seller');
|
||||
|
||||
const PersonType(this.persianName, this.englishName);
|
||||
final String persianName;
|
||||
final String englishName;
|
||||
|
||||
static PersonType fromString(String value) {
|
||||
return PersonType.values.firstWhere(
|
||||
(type) => type.persianName == value || type.englishName == value,
|
||||
orElse: () => PersonType.customer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Person {
|
||||
final int? id;
|
||||
final int businessId;
|
||||
final String aliasName;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final PersonType personType;
|
||||
final String? companyName;
|
||||
final String? paymentId;
|
||||
final String? nationalId;
|
||||
final String? registrationNumber;
|
||||
final String? economicId;
|
||||
final String? country;
|
||||
final String? province;
|
||||
final String? city;
|
||||
final String? address;
|
||||
final String? postalCode;
|
||||
final String? phone;
|
||||
final String? mobile;
|
||||
final String? fax;
|
||||
final String? email;
|
||||
final String? website;
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<PersonBankAccount> bankAccounts;
|
||||
|
||||
Person({
|
||||
this.id,
|
||||
required this.businessId,
|
||||
required this.aliasName,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
required this.personType,
|
||||
this.companyName,
|
||||
this.paymentId,
|
||||
this.nationalId,
|
||||
this.registrationNumber,
|
||||
this.economicId,
|
||||
this.country,
|
||||
this.province,
|
||||
this.city,
|
||||
this.address,
|
||||
this.postalCode,
|
||||
this.phone,
|
||||
this.mobile,
|
||||
this.fax,
|
||||
this.email,
|
||||
this.website,
|
||||
this.isActive = true,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.bankAccounts = const [],
|
||||
});
|
||||
|
||||
factory Person.fromJson(Map<String, dynamic> json) {
|
||||
return Person(
|
||||
id: json['id'],
|
||||
businessId: json['business_id'],
|
||||
aliasName: json['alias_name'],
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
personType: PersonType.fromString(json['person_type']),
|
||||
companyName: json['company_name'],
|
||||
paymentId: json['payment_id'],
|
||||
nationalId: json['national_id'],
|
||||
registrationNumber: json['registration_number'],
|
||||
economicId: json['economic_id'],
|
||||
country: json['country'],
|
||||
province: json['province'],
|
||||
city: json['city'],
|
||||
address: json['address'],
|
||||
postalCode: json['postal_code'],
|
||||
phone: json['phone'],
|
||||
mobile: json['mobile'],
|
||||
fax: json['fax'],
|
||||
email: json['email'],
|
||||
website: json['website'],
|
||||
isActive: json['is_active'] ?? true,
|
||||
createdAt: DateTime.parse(json['created_at']),
|
||||
updatedAt: DateTime.parse(json['updated_at']),
|
||||
bankAccounts: (json['bank_accounts'] as List<dynamic>?)
|
||||
?.map((ba) => PersonBankAccount.fromJson(ba))
|
||||
.toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'business_id': businessId,
|
||||
'alias_name': aliasName,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'person_type': personType.persianName,
|
||||
'company_name': companyName,
|
||||
'payment_id': paymentId,
|
||||
'national_id': nationalId,
|
||||
'registration_number': registrationNumber,
|
||||
'economic_id': economicId,
|
||||
'country': country,
|
||||
'province': province,
|
||||
'city': city,
|
||||
'address': address,
|
||||
'postal_code': postalCode,
|
||||
'phone': phone,
|
||||
'mobile': mobile,
|
||||
'fax': fax,
|
||||
'email': email,
|
||||
'website': website,
|
||||
'is_active': isActive,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
'bank_accounts': bankAccounts.map((ba) => ba.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Person copyWith({
|
||||
int? id,
|
||||
int? businessId,
|
||||
String? aliasName,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
PersonType? personType,
|
||||
String? companyName,
|
||||
String? paymentId,
|
||||
String? nationalId,
|
||||
String? registrationNumber,
|
||||
String? economicId,
|
||||
String? country,
|
||||
String? province,
|
||||
String? city,
|
||||
String? address,
|
||||
String? postalCode,
|
||||
String? phone,
|
||||
String? mobile,
|
||||
String? fax,
|
||||
String? email,
|
||||
String? website,
|
||||
bool? isActive,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<PersonBankAccount>? bankAccounts,
|
||||
}) {
|
||||
return Person(
|
||||
id: id ?? this.id,
|
||||
businessId: businessId ?? this.businessId,
|
||||
aliasName: aliasName ?? this.aliasName,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
personType: personType ?? this.personType,
|
||||
companyName: companyName ?? this.companyName,
|
||||
paymentId: paymentId ?? this.paymentId,
|
||||
nationalId: nationalId ?? this.nationalId,
|
||||
registrationNumber: registrationNumber ?? this.registrationNumber,
|
||||
economicId: economicId ?? this.economicId,
|
||||
country: country ?? this.country,
|
||||
province: province ?? this.province,
|
||||
city: city ?? this.city,
|
||||
address: address ?? this.address,
|
||||
postalCode: postalCode ?? this.postalCode,
|
||||
phone: phone ?? this.phone,
|
||||
mobile: mobile ?? this.mobile,
|
||||
fax: fax ?? this.fax,
|
||||
email: email ?? this.email,
|
||||
website: website ?? this.website,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
bankAccounts: bankAccounts ?? this.bankAccounts,
|
||||
);
|
||||
}
|
||||
|
||||
String get fullName {
|
||||
if (firstName != null && lastName != null) {
|
||||
return '$firstName $lastName';
|
||||
} else if (firstName != null) {
|
||||
return firstName!;
|
||||
} else if (lastName != null) {
|
||||
return lastName!;
|
||||
}
|
||||
return aliasName;
|
||||
}
|
||||
|
||||
String get displayName {
|
||||
return fullName.isNotEmpty ? fullName : aliasName;
|
||||
}
|
||||
}
|
||||
|
||||
class PersonCreateRequest {
|
||||
final String aliasName;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final PersonType personType;
|
||||
final String? companyName;
|
||||
final String? paymentId;
|
||||
final String? nationalId;
|
||||
final String? registrationNumber;
|
||||
final String? economicId;
|
||||
final String? country;
|
||||
final String? province;
|
||||
final String? city;
|
||||
final String? address;
|
||||
final String? postalCode;
|
||||
final String? phone;
|
||||
final String? mobile;
|
||||
final String? fax;
|
||||
final String? email;
|
||||
final String? website;
|
||||
final List<PersonBankAccount> bankAccounts;
|
||||
|
||||
PersonCreateRequest({
|
||||
required this.aliasName,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
required this.personType,
|
||||
this.companyName,
|
||||
this.paymentId,
|
||||
this.nationalId,
|
||||
this.registrationNumber,
|
||||
this.economicId,
|
||||
this.country,
|
||||
this.province,
|
||||
this.city,
|
||||
this.address,
|
||||
this.postalCode,
|
||||
this.phone,
|
||||
this.mobile,
|
||||
this.fax,
|
||||
this.email,
|
||||
this.website,
|
||||
this.bankAccounts = const [],
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'alias_name': aliasName,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'person_type': personType.persianName,
|
||||
'company_name': companyName,
|
||||
'payment_id': paymentId,
|
||||
'national_id': nationalId,
|
||||
'registration_number': registrationNumber,
|
||||
'economic_id': economicId,
|
||||
'country': country,
|
||||
'province': province,
|
||||
'city': city,
|
||||
'address': address,
|
||||
'postal_code': postalCode,
|
||||
'phone': phone,
|
||||
'mobile': mobile,
|
||||
'fax': fax,
|
||||
'email': email,
|
||||
'website': website,
|
||||
'bank_accounts': bankAccounts.map((ba) => ba.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class PersonUpdateRequest {
|
||||
final String? aliasName;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final PersonType? personType;
|
||||
final String? companyName;
|
||||
final String? paymentId;
|
||||
final String? nationalId;
|
||||
final String? registrationNumber;
|
||||
final String? economicId;
|
||||
final String? country;
|
||||
final String? province;
|
||||
final String? city;
|
||||
final String? address;
|
||||
final String? postalCode;
|
||||
final String? phone;
|
||||
final String? mobile;
|
||||
final String? fax;
|
||||
final String? email;
|
||||
final String? website;
|
||||
final bool? isActive;
|
||||
|
||||
PersonUpdateRequest({
|
||||
this.aliasName,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.personType,
|
||||
this.companyName,
|
||||
this.paymentId,
|
||||
this.nationalId,
|
||||
this.registrationNumber,
|
||||
this.economicId,
|
||||
this.country,
|
||||
this.province,
|
||||
this.city,
|
||||
this.address,
|
||||
this.postalCode,
|
||||
this.phone,
|
||||
this.mobile,
|
||||
this.fax,
|
||||
this.email,
|
||||
this.website,
|
||||
this.isActive,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> json = {};
|
||||
|
||||
if (aliasName != null) json['alias_name'] = aliasName;
|
||||
if (firstName != null) json['first_name'] = firstName;
|
||||
if (lastName != null) json['last_name'] = lastName;
|
||||
if (personType != null) json['person_type'] = personType!.persianName;
|
||||
if (companyName != null) json['company_name'] = companyName;
|
||||
if (paymentId != null) json['payment_id'] = paymentId;
|
||||
if (nationalId != null) json['national_id'] = nationalId;
|
||||
if (registrationNumber != null) json['registration_number'] = registrationNumber;
|
||||
if (economicId != null) json['economic_id'] = economicId;
|
||||
if (country != null) json['country'] = country;
|
||||
if (province != null) json['province'] = province;
|
||||
if (city != null) json['city'] = city;
|
||||
if (address != null) json['address'] = address;
|
||||
if (postalCode != null) json['postal_code'] = postalCode;
|
||||
if (phone != null) json['phone'] = phone;
|
||||
if (mobile != null) json['mobile'] = mobile;
|
||||
if (fax != null) json['fax'] = fax;
|
||||
if (email != null) json['email'] = email;
|
||||
if (website != null) json['website'] = website;
|
||||
if (isActive != null) json['is_active'] = isActive;
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,9 @@ import '../../core/locale_controller.dart';
|
|||
import '../../core/calendar_controller.dart';
|
||||
import '../../theme/theme_controller.dart';
|
||||
import '../../widgets/combined_user_menu_button.dart';
|
||||
import '../../widgets/person/person_form_dialog.dart';
|
||||
import '../../services/business_dashboard_service.dart';
|
||||
import '../../core/api_client.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
|
||||
class BusinessShell extends StatefulWidget {
|
||||
|
|
@ -31,11 +34,11 @@ class BusinessShell extends StatefulWidget {
|
|||
|
||||
class _BusinessShellState extends State<BusinessShell> {
|
||||
int _hoverIndex = -1;
|
||||
bool _isPeopleExpanded = false;
|
||||
bool _isProductsAndServicesExpanded = false;
|
||||
bool _isBankingExpanded = false;
|
||||
bool _isAccountingMenuExpanded = false;
|
||||
bool _isWarehouseManagementExpanded = false;
|
||||
final BusinessDashboardService _businessService = BusinessDashboardService(ApiClient());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -46,6 +49,29 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
||||
// بارگذاری اطلاعات کسب و کار و دسترسیها
|
||||
_loadBusinessInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadBusinessInfo() async {
|
||||
if (widget.authStore.currentBusiness?.id == widget.businessId) {
|
||||
return; // اطلاعات قبلاً بارگذاری شده
|
||||
}
|
||||
|
||||
try {
|
||||
final businessData = await _businessService.getBusinessWithPermissions(widget.businessId);
|
||||
await widget.authStore.setCurrentBusiness(businessData);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا در بارگذاری اطلاعات کسب و کار: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -68,7 +94,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
final t = AppLocalizations.of(context);
|
||||
|
||||
// ساختار متمرکز منو
|
||||
final menuItems = <_MenuItem>[
|
||||
final allMenuItems = <_MenuItem>[
|
||||
_MenuItem(
|
||||
label: t.businessDashboard,
|
||||
icon: Icons.dashboard_outlined,
|
||||
|
|
@ -87,34 +113,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
label: t.people,
|
||||
icon: Icons.people,
|
||||
selectedIcon: Icons.people,
|
||||
path: null, // برای منوی بازشونده
|
||||
type: _MenuItemType.expandable,
|
||||
children: [
|
||||
_MenuItem(
|
||||
label: t.peopleList,
|
||||
icon: Icons.list,
|
||||
selectedIcon: Icons.list,
|
||||
path: '/business/${widget.businessId}/people-list',
|
||||
type: _MenuItemType.simple,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.receipts,
|
||||
icon: Icons.receipt,
|
||||
selectedIcon: Icons.receipt,
|
||||
path: '/business/${widget.businessId}/receipts',
|
||||
path: '/business/${widget.businessId}/persons',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.payments,
|
||||
icon: Icons.payment,
|
||||
selectedIcon: Icons.payment,
|
||||
path: '/business/${widget.businessId}/payments',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.productsAndServices,
|
||||
icon: Icons.inventory_2,
|
||||
|
|
@ -203,14 +205,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.transfers,
|
||||
icon: Icons.swap_horiz,
|
||||
selectedIcon: Icons.swap_horiz,
|
||||
path: '/business/${widget.businessId}/transfers',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
_MenuItem(
|
||||
|
|
@ -228,6 +222,14 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.receiptsAndPayments,
|
||||
icon: Icons.account_balance_wallet,
|
||||
selectedIcon: Icons.account_balance_wallet,
|
||||
path: '/business/${widget.businessId}/receipts-payments',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.expenseAndIncome,
|
||||
icon: Icons.account_balance_wallet,
|
||||
|
|
@ -236,6 +238,22 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.transfers,
|
||||
icon: Icons.swap_horiz,
|
||||
selectedIcon: Icons.swap_horiz,
|
||||
path: '/business/${widget.businessId}/transfers',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.documents,
|
||||
icon: Icons.description,
|
||||
selectedIcon: Icons.description,
|
||||
path: '/business/${widget.businessId}/documents',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.accountingMenu,
|
||||
icon: Icons.calculate,
|
||||
|
|
@ -243,14 +261,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
path: null, // برای منوی بازشونده
|
||||
type: _MenuItemType.expandable,
|
||||
children: [
|
||||
_MenuItem(
|
||||
label: t.documents,
|
||||
icon: Icons.description,
|
||||
selectedIcon: Icons.description,
|
||||
path: '/business/${widget.businessId}/documents',
|
||||
type: _MenuItemType.simple,
|
||||
hasAddButton: true,
|
||||
),
|
||||
_MenuItem(
|
||||
label: t.chartOfAccounts,
|
||||
icon: Icons.table_chart,
|
||||
|
|
@ -373,6 +383,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
),
|
||||
];
|
||||
|
||||
// فیلتر کردن منو بر اساس دسترسیها
|
||||
final menuItems = _getFilteredMenuItems(allMenuItems);
|
||||
|
||||
int selectedIndex = 0;
|
||||
for (int i = 0; i < menuItems.length; i++) {
|
||||
final item = menuItems[i];
|
||||
|
|
@ -387,11 +400,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
if (child.path != null && location.startsWith(child.path!)) {
|
||||
selectedIndex = i;
|
||||
// تنظیم وضعیت باز بودن منو
|
||||
if (i == 2) _isPeopleExpanded = true; // اشخاص در ایندکس 2
|
||||
if (i == 3) _isProductsAndServicesExpanded = true; // کالا و خدمات در ایندکس 3
|
||||
if (i == 4) _isBankingExpanded = true; // بانکداری در ایندکس 4
|
||||
if (i == 6) _isAccountingMenuExpanded = true; // حسابداری در ایندکس 6
|
||||
if (i == 8) _isWarehouseManagementExpanded = true; // انبارداری در ایندکس 8
|
||||
if (i == 2) _isProductsAndServicesExpanded = true; // کالا و خدمات در ایندکس 2
|
||||
if (i == 3) _isBankingExpanded = true; // بانکداری در ایندکس 3
|
||||
if (i == 5) _isAccountingMenuExpanded = true; // حسابداری در ایندکس 5
|
||||
if (i == 7) _isWarehouseManagementExpanded = true; // انبارداری در ایندکس 7
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -413,7 +425,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
}
|
||||
} else if (item.type == _MenuItemType.expandable) {
|
||||
// تغییر وضعیت باز/بسته بودن منو
|
||||
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
||||
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
||||
|
|
@ -449,8 +460,20 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
context.go('/login');
|
||||
}
|
||||
|
||||
Future<void> _showAddPersonDialog() async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => PersonFormDialog(
|
||||
businessId: widget.businessId,
|
||||
),
|
||||
);
|
||||
if (result == true) {
|
||||
// Refresh the persons page if it's currently open
|
||||
// This will be handled by the PersonsPage itself
|
||||
}
|
||||
}
|
||||
|
||||
bool isExpanded(_MenuItem item) {
|
||||
if (item.label == t.people) return _isPeopleExpanded;
|
||||
if (item.label == t.productsAndServices) return _isProductsAndServicesExpanded;
|
||||
if (item.label == t.banking) return _isBankingExpanded;
|
||||
if (item.label == t.accountingMenu) return _isAccountingMenuExpanded;
|
||||
|
|
@ -622,10 +645,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
GestureDetector(
|
||||
onTap: () {
|
||||
// Navigate to add new item
|
||||
if (child.label == t.receipts) {
|
||||
// Navigate to add receipt
|
||||
} else if (child.label == t.payments) {
|
||||
// Navigate to add payment
|
||||
if (child.label == t.personsList) {
|
||||
// Navigate to add person
|
||||
_showAddPersonDialog();
|
||||
} else if (child.label == t.products) {
|
||||
// Navigate to add product
|
||||
} else if (child.label == t.priceLists) {
|
||||
|
|
@ -644,14 +666,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
// Navigate to add wallet
|
||||
} else if (child.label == t.checks) {
|
||||
// Navigate to add check
|
||||
} else if (child.label == t.transfers) {
|
||||
// Navigate to add transfer
|
||||
} else if (child.label == t.invoice) {
|
||||
// Navigate to add invoice
|
||||
} else if (child.label == t.expenseAndIncome) {
|
||||
// Navigate to add expense/income
|
||||
} else if (child.label == t.documents) {
|
||||
// Navigate to add document
|
||||
} else if (child.label == t.warehouses) {
|
||||
// Navigate to add warehouse
|
||||
} else if (child.label == t.shipments) {
|
||||
|
|
@ -733,7 +751,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
onTap: () {
|
||||
if (item.type == _MenuItemType.expandable) {
|
||||
setState(() {
|
||||
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
||||
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
||||
|
|
@ -781,8 +798,17 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
GestureDetector(
|
||||
onTap: () {
|
||||
// Navigate to add new item
|
||||
if (item.label == t.invoice) {
|
||||
if (item.label == t.people) {
|
||||
// Navigate to add person
|
||||
_showAddPersonDialog();
|
||||
} else if (item.label == t.invoice) {
|
||||
// Navigate to add invoice
|
||||
} else if (item.label == t.receiptsAndPayments) {
|
||||
// Navigate to add receipt/payment
|
||||
} else if (item.label == t.transfers) {
|
||||
// Navigate to add transfer
|
||||
} else if (item.label == t.documents) {
|
||||
// Navigate to add document
|
||||
} else if (item.label == t.expenseAndIncome) {
|
||||
// Navigate to add expense/income
|
||||
} else if (item.label == t.reports) {
|
||||
|
|
@ -892,7 +918,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
initiallyExpanded: isExpanded(item),
|
||||
onExpansionChanged: (expanded) {
|
||||
setState(() {
|
||||
if (item.label == t.people) _isPeopleExpanded = expanded;
|
||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = expanded;
|
||||
if (item.label == t.banking) _isBankingExpanded = expanded;
|
||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = expanded;
|
||||
|
|
@ -906,11 +931,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
onTap: () {
|
||||
context.pop();
|
||||
// Navigate to add new item
|
||||
if (child.label == t.receipts) {
|
||||
// Navigate to add receipt
|
||||
} else if (child.label == t.payments) {
|
||||
// Navigate to add payment
|
||||
} else if (child.label == t.products) {
|
||||
if (child.label == t.products) {
|
||||
// Navigate to add product
|
||||
} else if (child.label == t.priceLists) {
|
||||
// Navigate to add price list
|
||||
|
|
@ -928,14 +949,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
// Navigate to add wallet
|
||||
} else if (child.label == t.checks) {
|
||||
// Navigate to add check
|
||||
} else if (child.label == t.transfers) {
|
||||
// Navigate to add transfer
|
||||
} else if (child.label == t.invoice) {
|
||||
// Navigate to add invoice
|
||||
} else if (child.label == t.expenseAndIncome) {
|
||||
// Navigate to add expense/income
|
||||
} else if (child.label == t.documents) {
|
||||
// Navigate to add document
|
||||
} else if (child.label == t.warehouses) {
|
||||
// Navigate to add warehouse
|
||||
} else if (child.label == t.shipments) {
|
||||
|
|
@ -980,6 +997,67 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
body: content,
|
||||
);
|
||||
}
|
||||
|
||||
// فیلتر کردن منو بر اساس دسترسیها
|
||||
List<_MenuItem> _getFilteredMenuItems(List<_MenuItem> allItems) {
|
||||
return allItems.where((item) {
|
||||
if (item.type == _MenuItemType.separator) return true;
|
||||
|
||||
if (item.type == _MenuItemType.simple) {
|
||||
return _hasAccessToMenuItem(item);
|
||||
}
|
||||
|
||||
if (item.type == _MenuItemType.expandable) {
|
||||
return _hasAccessToExpandableMenuItem(item);
|
||||
}
|
||||
|
||||
return false;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
bool _hasAccessToMenuItem(_MenuItem item) {
|
||||
final sectionMap = {
|
||||
'people': 'people',
|
||||
'products': 'products',
|
||||
'priceLists': 'price_lists',
|
||||
'categories': 'categories',
|
||||
'productAttributes': 'product_attributes',
|
||||
'accounts': 'bank_accounts',
|
||||
'pettyCash': 'petty_cash',
|
||||
'cashBox': 'cash',
|
||||
'wallet': 'wallet',
|
||||
'checks': 'checks',
|
||||
'invoice': 'invoices',
|
||||
'receiptsAndPayments': 'accounting_documents',
|
||||
'expenseAndIncome': 'expenses_income',
|
||||
'transfers': 'transfers',
|
||||
'documents': 'accounting_documents',
|
||||
'chartOfAccounts': 'chart_of_accounts',
|
||||
'openingBalance': 'opening_balance',
|
||||
'yearEndClosing': 'opening_balance',
|
||||
'accountingSettings': 'settings',
|
||||
'reports': 'reports',
|
||||
'warehouses': 'warehouses',
|
||||
'shipments': 'warehouse_transfers',
|
||||
'inquiries': 'reports',
|
||||
'storageSpace': 'storage',
|
||||
'taxpayers': 'settings',
|
||||
'settings': 'settings',
|
||||
'pluginMarketplace': 'marketplace',
|
||||
};
|
||||
|
||||
final section = sectionMap[item.label];
|
||||
if (section == null) return true; // اگر بخشی تعریف نشده، نمایش داده شود
|
||||
|
||||
return widget.authStore.canReadSection(section);
|
||||
}
|
||||
|
||||
bool _hasAccessToExpandableMenuItem(_MenuItem item) {
|
||||
if (item.children == null) return false;
|
||||
|
||||
// اگر حداقل یکی از زیرآیتمها قابل دسترسی باشد، منو نمایش داده شود
|
||||
return item.children!.any((child) => _hasAccessToMenuItem(child));
|
||||
}
|
||||
}
|
||||
|
||||
enum _MenuItemType { simple, expandable, separator }
|
||||
|
|
|
|||
233
hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart
Normal file
233
hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
import '../../widgets/data_table/data_table_widget.dart';
|
||||
import '../../widgets/data_table/data_table_config.dart';
|
||||
import '../../widgets/person/person_form_dialog.dart';
|
||||
import '../../widgets/permission/permission_widgets.dart';
|
||||
import '../../models/person_model.dart';
|
||||
import '../../services/person_service.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
|
||||
class PersonsPage extends StatefulWidget {
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PersonsPage({
|
||||
super.key,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PersonsPage> createState() => _PersonsPageState();
|
||||
}
|
||||
|
||||
class _PersonsPageState extends State<PersonsPage> {
|
||||
final _personService = PersonService();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
|
||||
// بررسی دسترسی خواندن
|
||||
if (!widget.authStore.canReadSection('people')) {
|
||||
return AccessDeniedPage(
|
||||
message: 'شما دسترسی لازم برای مشاهده لیست اشخاص را ندارید',
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.personsList),
|
||||
actions: [
|
||||
// دکمه اضافه کردن فقط در صورت داشتن دسترسی
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'add',
|
||||
authStore: widget.authStore,
|
||||
child: IconButton(
|
||||
onPressed: _addPerson,
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: t.addPerson,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: DataTableWidget<Person>(
|
||||
config: _buildDataTableConfig(t),
|
||||
fromJson: Person.fromJson,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DataTableConfig<Person> _buildDataTableConfig(AppLocalizations t) {
|
||||
return DataTableConfig<Person>(
|
||||
endpoint: '/api/v1/persons/businesses/${widget.businessId}/persons',
|
||||
title: t.personsList,
|
||||
columns: [
|
||||
TextColumn(
|
||||
'alias_name',
|
||||
t.personAliasName,
|
||||
width: ColumnWidth.large,
|
||||
formatter: (person) => person.aliasName,
|
||||
),
|
||||
TextColumn(
|
||||
'first_name',
|
||||
t.personFirstName,
|
||||
width: ColumnWidth.medium,
|
||||
formatter: (person) => person.firstName ?? '-',
|
||||
),
|
||||
TextColumn(
|
||||
'last_name',
|
||||
t.personLastName,
|
||||
width: ColumnWidth.medium,
|
||||
formatter: (person) => person.lastName ?? '-',
|
||||
),
|
||||
TextColumn(
|
||||
'person_type',
|
||||
t.personType,
|
||||
width: ColumnWidth.medium,
|
||||
formatter: (person) => person.personType.persianName,
|
||||
),
|
||||
TextColumn(
|
||||
'company_name',
|
||||
t.personCompanyName,
|
||||
width: ColumnWidth.medium,
|
||||
formatter: (person) => person.companyName ?? '-',
|
||||
),
|
||||
TextColumn(
|
||||
'mobile',
|
||||
t.personMobile,
|
||||
width: ColumnWidth.medium,
|
||||
formatter: (person) => person.mobile ?? '-',
|
||||
),
|
||||
TextColumn(
|
||||
'email',
|
||||
t.personEmail,
|
||||
width: ColumnWidth.large,
|
||||
formatter: (person) => person.email ?? '-',
|
||||
),
|
||||
TextColumn(
|
||||
'is_active',
|
||||
'وضعیت',
|
||||
width: ColumnWidth.small,
|
||||
formatter: (person) => person.isActive ? 'فعال' : 'غیرفعال',
|
||||
),
|
||||
DateColumn(
|
||||
'created_at',
|
||||
'تاریخ ایجاد',
|
||||
width: ColumnWidth.medium,
|
||||
),
|
||||
ActionColumn(
|
||||
'actions',
|
||||
'عملیات',
|
||||
actions: [
|
||||
DataTableAction(
|
||||
icon: Icons.edit,
|
||||
label: t.edit,
|
||||
onTap: (person) => _editPerson(person),
|
||||
),
|
||||
DataTableAction(
|
||||
icon: Icons.delete,
|
||||
label: t.delete,
|
||||
color: Colors.red,
|
||||
onTap: (person) => _deletePerson(person),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
searchFields: [
|
||||
'alias_name',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'company_name',
|
||||
'mobile',
|
||||
'email',
|
||||
'national_id',
|
||||
],
|
||||
filterFields: [
|
||||
'person_type',
|
||||
'is_active',
|
||||
'country',
|
||||
'province',
|
||||
],
|
||||
defaultPageSize: 20,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void _addPerson() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => PersonFormDialog(
|
||||
businessId: widget.businessId,
|
||||
onSuccess: () {
|
||||
// DataTableWidget will automatically refresh
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editPerson(Person person) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => PersonFormDialog(
|
||||
businessId: widget.businessId,
|
||||
person: person,
|
||||
onSuccess: () {
|
||||
// DataTableWidget will automatically refresh
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deletePerson(Person person) {
|
||||
final t = AppLocalizations.of(context);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(t.deletePerson),
|
||||
content: Text('آیا از حذف شخص "${person.displayName}" مطمئن هستید؟'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _performDelete(person);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: Text(t.delete),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _performDelete(Person person) async {
|
||||
try {
|
||||
await _personService.deletePerson(person.id!);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).personDeletedSuccessfully),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
// DataTableWidget will automatically refresh
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا در حذف شخص: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
333
hesabixUI/hesabix_ui/lib/pages/error_404_page.dart
Normal file
333
hesabixUI/hesabix_ui/lib/pages/error_404_page.dart
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class Error404Page extends StatefulWidget {
|
||||
const Error404Page({super.key});
|
||||
|
||||
@override
|
||||
State<Error404Page> createState() => _Error404PageState();
|
||||
}
|
||||
|
||||
class _Error404PageState extends State<Error404Page>
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _fadeController;
|
||||
late AnimationController _slideController;
|
||||
late AnimationController _bounceController;
|
||||
late AnimationController _pulseController;
|
||||
late AnimationController _rotateController;
|
||||
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<Offset> _slideAnimation;
|
||||
late Animation<double> _bounceAnimation;
|
||||
late Animation<double> _pulseAnimation;
|
||||
late Animation<double> _rotateAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// کنترلرهای انیمیشن
|
||||
_fadeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1800),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_bounceController = AnimationController(
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_pulseController = AnimationController(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_rotateController = AnimationController(
|
||||
duration: const Duration(milliseconds: 3000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// انیمیشنها
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _fadeController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_slideAnimation = Tween<Offset>(
|
||||
begin: const Offset(0, 0.5),
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _slideController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_bounceAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _bounceController,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_pulseAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 1.05,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _pulseController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_rotateAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _rotateController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
// شروع انیمیشنها
|
||||
_startAnimations();
|
||||
}
|
||||
|
||||
void _startAnimations() async {
|
||||
await _fadeController.forward();
|
||||
await _slideController.forward();
|
||||
await _bounceController.forward();
|
||||
|
||||
// انیمیشنهای مداوم
|
||||
_pulseController.repeat(reverse: true);
|
||||
_rotateController.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fadeController.dispose();
|
||||
_slideController.dispose();
|
||||
_bounceController.dispose();
|
||||
_pulseController.dispose();
|
||||
_rotateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: isDark ? const Color(0xFF0F0F0F) : const Color(0xFFFAFAFA),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [
|
||||
const Color(0xFF0F0F0F),
|
||||
const Color(0xFF1A1A2E),
|
||||
const Color(0xFF16213E),
|
||||
]
|
||||
: [
|
||||
const Color(0xFFFAFAFA),
|
||||
const Color(0xFFF1F5F9),
|
||||
const Color(0xFFE2E8F0),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// انیمیشن 404 با افکتهای پیشرفته
|
||||
AnimatedBuilder(
|
||||
animation: Listenable.merge([_bounceAnimation, _pulseAnimation, _rotateAnimation]),
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _bounceAnimation.value * _pulseAnimation.value,
|
||||
child: Transform.rotate(
|
||||
angle: _rotateAnimation.value * 0.1,
|
||||
child: Container(
|
||||
width: 220,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: isDark
|
||||
? [
|
||||
const Color(0xFF6366F1).withValues(alpha: 0.4),
|
||||
const Color(0xFF8B5CF6).withValues(alpha: 0.2),
|
||||
const Color(0xFFEC4899).withValues(alpha: 0.1),
|
||||
]
|
||||
: [
|
||||
const Color(0xFF6366F1).withValues(alpha: 0.3),
|
||||
const Color(0xFF8B5CF6).withValues(alpha: 0.15),
|
||||
const Color(0xFFEC4899).withValues(alpha: 0.05),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? const Color(0xFF6366F1).withValues(alpha: 0.3)
|
||||
: const Color(0xFF4F46E5).withValues(alpha: 0.2),
|
||||
blurRadius: 30,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// حلقههای متحرک
|
||||
...List.generate(3, (index) {
|
||||
return AnimatedBuilder(
|
||||
animation: _rotateAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.rotate(
|
||||
angle: _rotateAnimation.value * (2 * 3.14159) * (index + 1) * 0.3,
|
||||
child: Container(
|
||||
width: 180 - (index * 20),
|
||||
height: 180 - (index * 20),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? const Color(0xFF6366F1).withValues(alpha: 0.3 - (index * 0.1))
|
||||
: const Color(0xFF4F46E5).withValues(alpha: 0.2 - (index * 0.05)),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
// متن 404
|
||||
Text(
|
||||
'404',
|
||||
style: TextStyle(
|
||||
fontSize: 80,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark
|
||||
? const Color(0xFF6366F1)
|
||||
: const Color(0xFF4F46E5),
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: isDark
|
||||
? const Color(0xFF6366F1).withValues(alpha: 0.6)
|
||||
: const Color(0xFF4F46E5).withValues(alpha: 0.4),
|
||||
blurRadius: 25,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 50),
|
||||
|
||||
// متن اصلی با انیمیشن
|
||||
FadeTransition(
|
||||
opacity: _fadeAnimation,
|
||||
child: SlideTransition(
|
||||
position: _slideAnimation,
|
||||
child: Column(
|
||||
children: [
|
||||
// عنوان اصلی
|
||||
Text(
|
||||
'صفحه مورد نظر یافت نشد',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : const Color(0xFF1E293B),
|
||||
height: 1.2,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// توضیحات
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'متأسفانه صفحهای که به دنبال آن هستید وجود ندارد یا حذف شده است. لطفاً آدرس را بررسی کنید یا از دکمههای زیر استفاده کنید.',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: isDark
|
||||
? Colors.grey[300]
|
||||
: const Color(0xFF64748B),
|
||||
height: 1.6,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// دکمه بازگشت
|
||||
AnimatedBuilder(
|
||||
animation: _fadeAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 30 * (1 - _fadeAnimation.value)),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// همیشه سعی کن به صفحه قبلی برگردی
|
||||
if (Navigator.canPop(context)) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
// اگر نمیتونی pop کنی، به root برگرد
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_ios, size: 20),
|
||||
label: const Text('بازگشت به صفحه قبلی'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF6366F1),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32,
|
||||
vertical: 20,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
elevation: 6,
|
||||
shadowColor: const Color(0xFF6366F1).withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -85,8 +85,8 @@ class BusinessDashboardService {
|
|||
/// دریافت لیست کسب و کارهای کاربر (مالک + عضو)
|
||||
Future<List<BusinessWithPermission>> getUserBusinesses() async {
|
||||
try {
|
||||
// دریافت کسب و کارهای مالک با POST request
|
||||
final ownedResponse = await _apiClient.post<Map<String, dynamic>>(
|
||||
// دریافت کسب و کارهای کاربر (مالک + عضو) با POST request
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/businesses/list',
|
||||
data: {
|
||||
'take': 100,
|
||||
|
|
@ -99,32 +99,13 @@ class BusinessDashboardService {
|
|||
|
||||
List<BusinessWithPermission> businesses = [];
|
||||
|
||||
if (ownedResponse.data?['success'] == true) {
|
||||
final ownedItems = ownedResponse.data!['data']['items'] as List<dynamic>;
|
||||
if (response.data?['success'] == true) {
|
||||
final items = response.data!['data']['items'] as List<dynamic>;
|
||||
businesses.addAll(
|
||||
ownedItems.map((item) {
|
||||
final business = BusinessWithPermission.fromJson(item);
|
||||
return BusinessWithPermission(
|
||||
id: business.id,
|
||||
name: business.name,
|
||||
businessType: business.businessType,
|
||||
businessField: business.businessField,
|
||||
ownerId: business.ownerId,
|
||||
address: business.address,
|
||||
phone: business.phone,
|
||||
mobile: business.mobile,
|
||||
createdAt: business.createdAt,
|
||||
isOwner: true,
|
||||
role: 'مالک',
|
||||
permissions: {},
|
||||
);
|
||||
}),
|
||||
items.map((item) => BusinessWithPermission.fromJson(item)),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: در آینده میتوان کسب و کارهای عضو را نیز اضافه کرد
|
||||
// از API endpoint جدید برای کسب و کارهای عضو
|
||||
|
||||
return businesses;
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 401) {
|
||||
|
|
@ -136,4 +117,55 @@ class BusinessDashboardService {
|
|||
throw Exception('خطا در بارگذاری کسب و کارها: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر
|
||||
Future<BusinessWithPermission> getBusinessWithPermissions(int businessId) async {
|
||||
try {
|
||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/business/$businessId/info-with-permissions',
|
||||
);
|
||||
|
||||
if (response.data?['success'] == true) {
|
||||
final data = response.data!['data'] as Map<String, dynamic>;
|
||||
|
||||
// تبدیل اطلاعات کسب و کار
|
||||
final businessInfo = data['business_info'] as Map<String, dynamic>;
|
||||
final userPermissions = data['user_permissions'] as Map<String, dynamic>? ?? {};
|
||||
final isOwner = data['is_owner'] as bool? ?? false;
|
||||
final role = data['role'] as String? ?? 'عضو';
|
||||
final hasAccess = data['has_access'] as bool? ?? false;
|
||||
|
||||
if (!hasAccess) {
|
||||
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
||||
}
|
||||
|
||||
return BusinessWithPermission(
|
||||
id: businessInfo['id'] as int,
|
||||
name: businessInfo['name'] as String,
|
||||
businessType: businessInfo['business_type'] as String,
|
||||
businessField: businessInfo['business_field'] as String,
|
||||
ownerId: businessInfo['owner_id'] as int,
|
||||
address: businessInfo['address'] as String?,
|
||||
phone: businessInfo['phone'] as String?,
|
||||
mobile: businessInfo['mobile'] as String?,
|
||||
createdAt: businessInfo['created_at'] as String,
|
||||
isOwner: isOwner,
|
||||
role: role,
|
||||
permissions: userPermissions,
|
||||
);
|
||||
} else {
|
||||
throw Exception('Failed to load business info: ${response.data?['message']}');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 403) {
|
||||
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
||||
} else if (e.response?.statusCode == 404) {
|
||||
throw Exception('کسب و کار یافت نشد');
|
||||
} else {
|
||||
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: ${e.message}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import '../core/api_client.dart';
|
||||
import '../models/person_model.dart';
|
||||
|
||||
class PersonService {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
PersonService({ApiClient? apiClient}) : _apiClient = apiClient ?? ApiClient();
|
||||
|
||||
/// دریافت لیست اشخاص یک کسب و کار
|
||||
Future<Map<String, dynamic>> getPersons({
|
||||
required int businessId,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
String? search,
|
||||
List<String>? searchFields,
|
||||
String? sortBy,
|
||||
bool sortDesc = true,
|
||||
Map<String, dynamic>? filters,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{
|
||||
'take': limit,
|
||||
'skip': (page - 1) * limit,
|
||||
'sort_desc': sortDesc,
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
|
||||
if (searchFields != null && searchFields.isNotEmpty) {
|
||||
queryParams['search_fields'] = searchFields;
|
||||
}
|
||||
|
||||
if (sortBy != null && sortBy.isNotEmpty) {
|
||||
queryParams['sort_by'] = sortBy;
|
||||
}
|
||||
|
||||
if (filters != null && filters.isNotEmpty) {
|
||||
queryParams['filters'] = filters;
|
||||
}
|
||||
|
||||
final response = await _apiClient.post(
|
||||
'/api/v1/persons/businesses/$businessId/persons',
|
||||
data: queryParams,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data['data'];
|
||||
} else {
|
||||
throw Exception('خطا در دریافت لیست اشخاص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در دریافت لیست اشخاص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// دریافت جزئیات یک شخص
|
||||
Future<Person> getPerson(int personId) async {
|
||||
try {
|
||||
final response = await _apiClient.get('/api/v1/persons/persons/$personId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Person.fromJson(response.data['data']);
|
||||
} else {
|
||||
throw Exception('خطا در دریافت جزئیات شخص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در دریافت جزئیات شخص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// ایجاد شخص جدید
|
||||
Future<Person> createPerson({
|
||||
required int businessId,
|
||||
required PersonCreateRequest personData,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.post(
|
||||
'/api/v1/persons/businesses/$businessId/persons/create',
|
||||
data: personData.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Person.fromJson(response.data['data']);
|
||||
} else {
|
||||
throw Exception('خطا در ایجاد شخص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در ایجاد شخص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// ویرایش شخص
|
||||
Future<Person> updatePerson({
|
||||
required int personId,
|
||||
required PersonUpdateRequest personData,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.put(
|
||||
'/api/v1/persons/persons/$personId',
|
||||
data: personData.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return Person.fromJson(response.data['data']);
|
||||
} else {
|
||||
throw Exception('خطا در ویرایش شخص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در ویرایش شخص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// حذف شخص
|
||||
Future<bool> deletePerson(int personId) async {
|
||||
try {
|
||||
final response = await _apiClient.delete('/api/v1/persons/persons/$personId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw Exception('خطا در حذف شخص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در حذف شخص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// دریافت خلاصه اشخاص
|
||||
Future<Map<String, dynamic>> getPersonsSummary(int businessId) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/api/v1/persons/businesses/$businessId/persons/summary',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data['data'];
|
||||
} else {
|
||||
throw Exception('خطا در دریافت خلاصه اشخاص');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در دریافت خلاصه اشخاص: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// تبدیل لیست اشخاص از JSON
|
||||
List<Person> parsePersonsList(Map<String, dynamic> data) {
|
||||
final List<dynamic> items = data['items'] ?? [];
|
||||
return items.map((item) => Person.fromJson(item)).toList();
|
||||
}
|
||||
|
||||
/// دریافت اطلاعات صفحهبندی
|
||||
Map<String, dynamic> getPaginationInfo(Map<String, dynamic> data) {
|
||||
return data['pagination'] ?? {};
|
||||
}
|
||||
|
||||
/// دریافت اطلاعات جستجو
|
||||
Map<String, dynamic> getQueryInfo(Map<String, dynamic> data) {
|
||||
return data['query_info'] ?? {};
|
||||
}
|
||||
}
|
||||
183
hesabixUI/hesabix_ui/lib/widgets/permission/README.md
Normal file
183
hesabixUI/hesabix_ui/lib/widgets/permission/README.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# سیستم مدیریت دسترسیها
|
||||
|
||||
این سیستم برای مدیریت دسترسیهای کاربران در سطح کسب و کار طراحی شده است.
|
||||
|
||||
## ویژگیها
|
||||
|
||||
- **دسترسیهای جزئی**: پشتیبانی از دسترسیهای بسیار جزئی برای هر بخش
|
||||
- **مدیریت خودکار**: فیلتر کردن منو و دکمهها بر اساس دسترسیها
|
||||
- **کامپوننتهای آماده**: ویجتهای آماده برای بررسی دسترسیها
|
||||
- **امنیت کامل**: بررسی دسترسیها در هر سطح
|
||||
|
||||
## دسترسیهای موجود
|
||||
|
||||
### اشخاص (People)
|
||||
- `people`: add, view, edit, delete
|
||||
- `people_receipts`: add, view, edit, delete, draft
|
||||
- `people_payments`: add, view, edit, delete, draft
|
||||
|
||||
### کالا و خدمات (Products & Services)
|
||||
- `products`: add, view, edit, delete
|
||||
- `price_lists`: add, view, edit, delete
|
||||
- `categories`: add, view, edit, delete
|
||||
- `product_attributes`: add, view, edit, delete
|
||||
|
||||
### بانکداری (Banking)
|
||||
- `bank_accounts`: add, view, edit, delete
|
||||
- `cash`: add, view, edit, delete
|
||||
- `petty_cash`: add, view, edit, delete
|
||||
- `checks`: add, view, edit, delete, collect, transfer, return
|
||||
- `wallet`: view, charge
|
||||
- `transfers`: add, view, edit, delete, draft
|
||||
|
||||
### فاکتورها و هزینهها (Invoices & Expenses)
|
||||
- `invoices`: add, view, edit, delete, draft
|
||||
- `expenses_income`: add, view, edit, delete, draft
|
||||
|
||||
### حسابداری (Accounting)
|
||||
- `accounting_documents`: add, view, edit, delete, draft
|
||||
- `chart_of_accounts`: add, view, edit, delete
|
||||
- `opening_balance`: view, edit
|
||||
|
||||
### انبارداری (Warehouse)
|
||||
- `warehouses`: add, view, edit, delete
|
||||
- `warehouse_transfers`: add, view, edit, delete, draft
|
||||
|
||||
### تنظیمات (Settings)
|
||||
- `settings`: business, print, history, users
|
||||
- `storage`: view, delete
|
||||
- `sms`: history, templates
|
||||
- `marketplace`: view, buy, invoices
|
||||
|
||||
## نحوه استفاده
|
||||
|
||||
### 1. بررسی دسترسی در AuthStore
|
||||
|
||||
```dart
|
||||
final authStore = Provider.of<AuthStore>(context);
|
||||
|
||||
// بررسی دسترسی کلی
|
||||
if (authStore.canReadSection('people')) {
|
||||
// نمایش لیست اشخاص
|
||||
}
|
||||
|
||||
// بررسی دسترسی خاص
|
||||
if (authStore.hasBusinessPermission('people', 'add')) {
|
||||
// نمایش دکمه اضافه کردن
|
||||
}
|
||||
|
||||
// بررسی دسترسیهای خاص
|
||||
if (authStore.canCollectChecks()) {
|
||||
// نمایش دکمه وصول چک
|
||||
}
|
||||
```
|
||||
|
||||
### 2. استفاده از کامپوننتهای آماده
|
||||
|
||||
#### PermissionButton
|
||||
```dart
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'add',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _addPerson(),
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'اضافه کردن شخص',
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### PermissionWidget
|
||||
```dart
|
||||
PermissionWidget(
|
||||
section: 'settings',
|
||||
action: 'view',
|
||||
authStore: authStore,
|
||||
child: Card(
|
||||
child: ListTile(
|
||||
title: Text('تنظیمات'),
|
||||
onTap: () => _openSettings(),
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### AccessDeniedPage
|
||||
```dart
|
||||
if (!authStore.canReadSection('people')) {
|
||||
return AccessDeniedPage(
|
||||
message: 'شما دسترسی لازم برای مشاهده لیست اشخاص را ندارید',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. فیلتر کردن منو
|
||||
|
||||
منوی کسب و کار به صورت خودکار بر اساس دسترسیهای کاربر فیلتر میشود:
|
||||
|
||||
```dart
|
||||
// در BusinessShell
|
||||
final menuItems = _getFilteredMenuItems(allMenuItems);
|
||||
```
|
||||
|
||||
### 4. API Endpoint
|
||||
|
||||
```dart
|
||||
// دریافت اطلاعات کسب و کار و دسترسیها
|
||||
final businessData = await businessService.getBusinessWithPermissions(businessId);
|
||||
await authStore.setCurrentBusiness(businessData);
|
||||
```
|
||||
|
||||
## مثال کامل
|
||||
|
||||
```dart
|
||||
class PersonsPage extends StatelessWidget {
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PersonsPage({
|
||||
super.key,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// بررسی دسترسی خواندن
|
||||
if (!authStore.canReadSection('people')) {
|
||||
return AccessDeniedPage(
|
||||
message: 'شما دسترسی لازم برای مشاهده لیست اشخاص را ندارید',
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('لیست اشخاص'),
|
||||
actions: [
|
||||
// دکمه اضافه کردن فقط در صورت داشتن دسترسی
|
||||
PermissionButton(
|
||||
section: 'people',
|
||||
action: 'add',
|
||||
authStore: authStore,
|
||||
child: IconButton(
|
||||
onPressed: () => _addPerson(),
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'اضافه کردن شخص',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: PersonsList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## نکات مهم
|
||||
|
||||
1. **امنیت**: همیشه دسترسیها را در سمت سرور نیز بررسی کنید
|
||||
2. **عملکرد**: دسترسیها در AuthStore کش میشوند
|
||||
3. **بهروزرسانی**: دسترسیها هنگام تغییر کسب و کار بهروزرسانی میشوند
|
||||
4. **مالک کسب و کار**: مالک کسب و کار تمام دسترسیها را دارد
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
|
||||
/// صفحه نمایش عدم دسترسی
|
||||
class AccessDeniedPage extends StatelessWidget {
|
||||
final String? message;
|
||||
final String? actionText;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const AccessDeniedPage({
|
||||
super.key,
|
||||
this.message,
|
||||
this.actionText,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// آیکون عدم دسترسی
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(60),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.lock_outline,
|
||||
size: 60,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// عنوان
|
||||
Text(
|
||||
t.accessDenied,
|
||||
style: theme.textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// پیام
|
||||
Text(
|
||||
message ?? 'شما دسترسی لازم برای مشاهده این بخش را ندارید',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// دکمههای عمل
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// دکمه بازگشت
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => context.pop(),
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
label: const Text('بازگشت'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// دکمه عمل سفارشی
|
||||
if (actionText != null && onAction != null)
|
||||
ElevatedButton.icon(
|
||||
onPressed: onAction,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(actionText!),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ویجت کوچک برای نمایش عدم دسترسی
|
||||
class AccessDeniedWidget extends StatelessWidget {
|
||||
final String? message;
|
||||
final IconData? icon;
|
||||
|
||||
const AccessDeniedWidget({
|
||||
super.key,
|
||||
this.message,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.errorContainer,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.lock_outline,
|
||||
size: 48,
|
||||
color: colorScheme.error,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
message ?? t.accessDenied,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
|
||||
/// کامپوننت برای نمایش دکمهها بر اساس دسترسیها
|
||||
class PermissionButton extends StatelessWidget {
|
||||
final String section;
|
||||
final String action;
|
||||
final Widget child;
|
||||
final VoidCallback? onPressed;
|
||||
final bool showIfNoPermission;
|
||||
final Widget? fallbackWidget;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionButton({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.action,
|
||||
required this.child,
|
||||
required this.authStore,
|
||||
this.onPressed,
|
||||
this.showIfNoPermission = false,
|
||||
this.fallbackWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!authStore.hasBusinessPermission(section, action)) {
|
||||
if (showIfNoPermission) {
|
||||
return child;
|
||||
}
|
||||
return fallbackWidget ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
/// کامپوننت برای نمایش ویجتها بر اساس دسترسیها
|
||||
class PermissionWidget extends StatelessWidget {
|
||||
final String section;
|
||||
final String action;
|
||||
final Widget child;
|
||||
final Widget? fallbackWidget;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionWidget({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.action,
|
||||
required this.child,
|
||||
required this.authStore,
|
||||
this.fallbackWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!authStore.hasBusinessPermission(section, action)) {
|
||||
return fallbackWidget ?? const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
/// کامپوننت برای نمایش لیست بر اساس دسترسیها
|
||||
class PermissionListTile extends StatelessWidget {
|
||||
final String section;
|
||||
final String action;
|
||||
final Widget child;
|
||||
final VoidCallback? onTap;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionListTile({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.action,
|
||||
required this.child,
|
||||
required this.authStore,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!authStore.hasBusinessPermission(section, action)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
/// کامپوننت برای نمایش منو بر اساس دسترسیها
|
||||
class PermissionMenuItem extends StatelessWidget {
|
||||
final String section;
|
||||
final String action;
|
||||
final Widget child;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionMenuItem({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.action,
|
||||
required this.child,
|
||||
required this.authStore,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!authStore.hasBusinessPermission(section, action)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
|
||||
/// ویجت برای نمایش اطلاعات دسترسیهای کاربر
|
||||
class PermissionInfoWidget extends StatelessWidget {
|
||||
final String section;
|
||||
final bool showActions;
|
||||
final AuthStore authStore;
|
||||
|
||||
const PermissionInfoWidget({
|
||||
super.key,
|
||||
required this.section,
|
||||
required this.authStore,
|
||||
this.showActions = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
if (authStore.currentBusiness == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final availableActions = authStore.getAvailableActions(section);
|
||||
final isOwner = authStore.currentBusiness!.isOwner;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// عنوان بخش
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getSectionIcon(section),
|
||||
size: 20,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_getSectionTitle(section),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (isOwner)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.orange.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.star,
|
||||
size: 12,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'مالک',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade700,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// نمایش دسترسیها
|
||||
if (showActions) ...[
|
||||
Text(
|
||||
'دسترسیهای موجود:',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: availableActions.map((action) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_getActionTitle(action),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getSectionIcon(String section) {
|
||||
switch (section) {
|
||||
case 'people':
|
||||
return Icons.people;
|
||||
case 'products':
|
||||
return Icons.inventory;
|
||||
case 'price_lists':
|
||||
return Icons.list_alt;
|
||||
case 'categories':
|
||||
return Icons.category;
|
||||
case 'product_attributes':
|
||||
return Icons.tune;
|
||||
case 'bank_accounts':
|
||||
return Icons.account_balance_wallet;
|
||||
case 'cash':
|
||||
return Icons.money;
|
||||
case 'petty_cash':
|
||||
return Icons.money;
|
||||
case 'wallet':
|
||||
return Icons.wallet;
|
||||
case 'checks':
|
||||
return Icons.receipt_long;
|
||||
case 'transfers':
|
||||
return Icons.swap_horiz;
|
||||
case 'invoices':
|
||||
return Icons.receipt;
|
||||
case 'expenses_income':
|
||||
return Icons.account_balance_wallet;
|
||||
case 'accounting_documents':
|
||||
return Icons.description;
|
||||
case 'chart_of_accounts':
|
||||
return Icons.table_chart;
|
||||
case 'opening_balance':
|
||||
return Icons.play_arrow;
|
||||
case 'reports':
|
||||
return Icons.assessment;
|
||||
case 'warehouses':
|
||||
return Icons.warehouse;
|
||||
case 'warehouse_transfers':
|
||||
return Icons.local_shipping;
|
||||
case 'storage':
|
||||
return Icons.storage;
|
||||
case 'settings':
|
||||
return Icons.settings;
|
||||
case 'marketplace':
|
||||
return Icons.store;
|
||||
default:
|
||||
return Icons.lock;
|
||||
}
|
||||
}
|
||||
|
||||
String _getSectionTitle(String section) {
|
||||
switch (section) {
|
||||
case 'people':
|
||||
return 'اشخاص';
|
||||
case 'products':
|
||||
return 'کالا و خدمات';
|
||||
case 'price_lists':
|
||||
return 'لیستهای قیمت';
|
||||
case 'categories':
|
||||
return 'دستهبندیها';
|
||||
case 'product_attributes':
|
||||
return 'ویژگیهای کالا';
|
||||
case 'bank_accounts':
|
||||
return 'حسابهای بانکی';
|
||||
case 'cash':
|
||||
return 'صندوق';
|
||||
case 'petty_cash':
|
||||
return 'تنخواه گردان';
|
||||
case 'wallet':
|
||||
return 'کیف پول';
|
||||
case 'checks':
|
||||
return 'چکها';
|
||||
case 'transfers':
|
||||
return 'انتقالها';
|
||||
case 'invoices':
|
||||
return 'فاکتورها';
|
||||
case 'expenses_income':
|
||||
return 'هزینه و درآمد';
|
||||
case 'accounting_documents':
|
||||
return 'اسناد حسابداری';
|
||||
case 'chart_of_accounts':
|
||||
return 'جدول حسابها';
|
||||
case 'opening_balance':
|
||||
return 'تراز افتتاحیه';
|
||||
case 'reports':
|
||||
return 'گزارشها';
|
||||
case 'warehouses':
|
||||
return 'انبارها';
|
||||
case 'warehouse_transfers':
|
||||
return 'حوالهها';
|
||||
case 'storage':
|
||||
return 'فضای ذخیرهسازی';
|
||||
case 'settings':
|
||||
return 'تنظیمات';
|
||||
case 'marketplace':
|
||||
return 'بازار افزونهها';
|
||||
default:
|
||||
return 'نامشخص';
|
||||
}
|
||||
}
|
||||
|
||||
String _getActionTitle(String action) {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
return 'افزودن';
|
||||
case 'view':
|
||||
return 'مشاهده';
|
||||
case 'edit':
|
||||
return 'ویرایش';
|
||||
case 'delete':
|
||||
return 'حذف';
|
||||
case 'draft':
|
||||
return 'پیشنویس';
|
||||
case 'collect':
|
||||
return 'وصول';
|
||||
case 'transfer':
|
||||
return 'انتقال';
|
||||
case 'return':
|
||||
return 'برگشت';
|
||||
case 'charge':
|
||||
return 'شارژ';
|
||||
default:
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// Export all permission-related widgets
|
||||
export 'permission_button.dart';
|
||||
export 'access_denied_page.dart';
|
||||
export 'permission_info_widget.dart';
|
||||
742
hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart
Normal file
742
hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart
Normal file
|
|
@ -0,0 +1,742 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
import '../../models/person_model.dart';
|
||||
import '../../services/person_service.dart';
|
||||
|
||||
class PersonFormDialog extends StatefulWidget {
|
||||
final int businessId;
|
||||
final Person? person; // null برای افزودن، مقدار برای ویرایش
|
||||
final VoidCallback? onSuccess;
|
||||
|
||||
const PersonFormDialog({
|
||||
super.key,
|
||||
required this.businessId,
|
||||
this.person,
|
||||
this.onSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PersonFormDialog> createState() => _PersonFormDialogState();
|
||||
}
|
||||
|
||||
class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _personService = PersonService();
|
||||
bool _isLoading = false;
|
||||
|
||||
// Controllers for basic info
|
||||
final _aliasNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _lastNameController = TextEditingController();
|
||||
final _companyNameController = TextEditingController();
|
||||
final _paymentIdController = TextEditingController();
|
||||
|
||||
// Controllers for economic info
|
||||
final _nationalIdController = TextEditingController();
|
||||
final _registrationNumberController = TextEditingController();
|
||||
final _economicIdController = TextEditingController();
|
||||
|
||||
// Controllers for contact info
|
||||
final _countryController = TextEditingController();
|
||||
final _provinceController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _mobileController = TextEditingController();
|
||||
final _faxController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _websiteController = TextEditingController();
|
||||
|
||||
PersonType _selectedPersonType = PersonType.customer;
|
||||
bool _isActive = true;
|
||||
|
||||
// Bank accounts
|
||||
List<PersonBankAccount> _bankAccounts = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeForm();
|
||||
}
|
||||
|
||||
void _initializeForm() {
|
||||
if (widget.person != null) {
|
||||
final person = widget.person!;
|
||||
_aliasNameController.text = person.aliasName;
|
||||
_firstNameController.text = person.firstName ?? '';
|
||||
_lastNameController.text = person.lastName ?? '';
|
||||
_companyNameController.text = person.companyName ?? '';
|
||||
_paymentIdController.text = person.paymentId ?? '';
|
||||
_nationalIdController.text = person.nationalId ?? '';
|
||||
_registrationNumberController.text = person.registrationNumber ?? '';
|
||||
_economicIdController.text = person.economicId ?? '';
|
||||
_countryController.text = person.country ?? '';
|
||||
_provinceController.text = person.province ?? '';
|
||||
_cityController.text = person.city ?? '';
|
||||
_addressController.text = person.address ?? '';
|
||||
_postalCodeController.text = person.postalCode ?? '';
|
||||
_phoneController.text = person.phone ?? '';
|
||||
_mobileController.text = person.mobile ?? '';
|
||||
_faxController.text = person.fax ?? '';
|
||||
_emailController.text = person.email ?? '';
|
||||
_websiteController.text = person.website ?? '';
|
||||
_selectedPersonType = person.personType;
|
||||
_isActive = person.isActive;
|
||||
_bankAccounts = List.from(person.bankAccounts);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aliasNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_companyNameController.dispose();
|
||||
_paymentIdController.dispose();
|
||||
_nationalIdController.dispose();
|
||||
_registrationNumberController.dispose();
|
||||
_economicIdController.dispose();
|
||||
_countryController.dispose();
|
||||
_provinceController.dispose();
|
||||
_cityController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_phoneController.dispose();
|
||||
_mobileController.dispose();
|
||||
_faxController.dispose();
|
||||
_emailController.dispose();
|
||||
_websiteController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _savePerson() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
if (widget.person == null) {
|
||||
// Create new person
|
||||
final personData = PersonCreateRequest(
|
||||
aliasName: _aliasNameController.text.trim(),
|
||||
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
||||
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||
personType: _selectedPersonType,
|
||||
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
||||
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
||||
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
||||
registrationNumber: _registrationNumberController.text.trim().isEmpty ? null : _registrationNumberController.text.trim(),
|
||||
economicId: _economicIdController.text.trim().isEmpty ? null : _economicIdController.text.trim(),
|
||||
country: _countryController.text.trim().isEmpty ? null : _countryController.text.trim(),
|
||||
province: _provinceController.text.trim().isEmpty ? null : _provinceController.text.trim(),
|
||||
city: _cityController.text.trim().isEmpty ? null : _cityController.text.trim(),
|
||||
address: _addressController.text.trim().isEmpty ? null : _addressController.text.trim(),
|
||||
postalCode: _postalCodeController.text.trim().isEmpty ? null : _postalCodeController.text.trim(),
|
||||
phone: _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim(),
|
||||
mobile: _mobileController.text.trim().isEmpty ? null : _mobileController.text.trim(),
|
||||
fax: _faxController.text.trim().isEmpty ? null : _faxController.text.trim(),
|
||||
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
website: _websiteController.text.trim().isEmpty ? null : _websiteController.text.trim(),
|
||||
bankAccounts: _bankAccounts,
|
||||
);
|
||||
|
||||
await _personService.createPerson(
|
||||
businessId: widget.businessId,
|
||||
personData: personData,
|
||||
);
|
||||
} else {
|
||||
// Update existing person
|
||||
final personData = PersonUpdateRequest(
|
||||
aliasName: _aliasNameController.text.trim(),
|
||||
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
||||
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||
personType: _selectedPersonType,
|
||||
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
||||
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
||||
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
||||
registrationNumber: _registrationNumberController.text.trim().isEmpty ? null : _registrationNumberController.text.trim(),
|
||||
economicId: _economicIdController.text.trim().isEmpty ? null : _economicIdController.text.trim(),
|
||||
country: _countryController.text.trim().isEmpty ? null : _countryController.text.trim(),
|
||||
province: _provinceController.text.trim().isEmpty ? null : _provinceController.text.trim(),
|
||||
city: _cityController.text.trim().isEmpty ? null : _cityController.text.trim(),
|
||||
address: _addressController.text.trim().isEmpty ? null : _addressController.text.trim(),
|
||||
postalCode: _postalCodeController.text.trim().isEmpty ? null : _postalCodeController.text.trim(),
|
||||
phone: _phoneController.text.trim().isEmpty ? null : _phoneController.text.trim(),
|
||||
mobile: _mobileController.text.trim().isEmpty ? null : _mobileController.text.trim(),
|
||||
fax: _faxController.text.trim().isEmpty ? null : _faxController.text.trim(),
|
||||
email: _emailController.text.trim().isEmpty ? null : _emailController.text.trim(),
|
||||
website: _websiteController.text.trim().isEmpty ? null : _websiteController.text.trim(),
|
||||
isActive: _isActive,
|
||||
);
|
||||
|
||||
await _personService.updatePerson(
|
||||
personId: widget.person!.id!,
|
||||
personData: personData,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onSuccess?.call();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(widget.person == null
|
||||
? AppLocalizations.of(context).personCreatedSuccessfully
|
||||
: AppLocalizations.of(context).personUpdatedSuccessfully),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _addBankAccount() {
|
||||
setState(() {
|
||||
_bankAccounts.add(PersonBankAccount(
|
||||
personId: 0, // Will be set when person is created
|
||||
bankName: '',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void _removeBankAccount(int index) {
|
||||
setState(() {
|
||||
_bankAccounts.removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateBankAccount(int index, PersonBankAccount bankAccount) {
|
||||
setState(() {
|
||||
_bankAccounts[index] = bankAccount;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
final isEditing = widget.person != null;
|
||||
|
||||
return Dialog(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isEditing ? Icons.edit : Icons.add,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isEditing ? t.editPerson : t.addPerson,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Form
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Basic Information
|
||||
_buildSectionHeader(t.personBasicInfo),
|
||||
const SizedBox(height: 16),
|
||||
_buildBasicInfoFields(t),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Economic Information
|
||||
_buildSectionHeader(t.personEconomicInfo),
|
||||
const SizedBox(height: 16),
|
||||
_buildEconomicInfoFields(t),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Contact Information
|
||||
_buildSectionHeader(t.personContactInfo),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactInfoFields(t),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Bank Accounts
|
||||
_buildSectionHeader(t.personBankInfo),
|
||||
const SizedBox(height: 16),
|
||||
_buildBankAccountsSection(t),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Status (only for editing)
|
||||
if (isEditing) ...[
|
||||
_buildSectionHeader('وضعیت'),
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text('فعال'),
|
||||
value: _isActive,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isActive = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(t.cancel),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _savePerson,
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(isEditing ? t.update : t.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionHeader(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBasicInfoFields(AppLocalizations t) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _aliasNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personAliasName,
|
||||
hintText: t.personAliasName,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return t.personAliasNameRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<PersonType>(
|
||||
value: _selectedPersonType,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personType,
|
||||
),
|
||||
items: PersonType.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.persianName),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedPersonType = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null) {
|
||||
return t.personTypeRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personFirstName,
|
||||
hintText: t.personFirstName,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personLastName,
|
||||
hintText: t.personLastName,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _companyNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personCompanyName,
|
||||
hintText: t.personCompanyName,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _paymentIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personPaymentId,
|
||||
hintText: t.personPaymentId,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEconomicInfoFields(AppLocalizations t) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _nationalIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personNationalId,
|
||||
hintText: t.personNationalId,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _registrationNumberController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personRegistrationNumber,
|
||||
hintText: t.personRegistrationNumber,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _economicIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personEconomicId,
|
||||
hintText: t.personEconomicId,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactInfoFields(AppLocalizations t) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _countryController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personCountry,
|
||||
hintText: t.personCountry,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _provinceController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personProvince,
|
||||
hintText: t.personProvince,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _cityController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personCity,
|
||||
hintText: t.personCity,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _postalCodeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personPostalCode,
|
||||
hintText: t.personPostalCode,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _addressController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personAddress,
|
||||
hintText: t.personAddress,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personPhone,
|
||||
hintText: t.personPhone,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _mobileController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personMobile,
|
||||
hintText: t.personMobile,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _faxController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personFax,
|
||||
hintText: t.personFax,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personEmail,
|
||||
hintText: t.personEmail,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _websiteController,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.personWebsite,
|
||||
hintText: t.personWebsite,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountsSection(AppLocalizations t) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
t.personBankAccounts,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _addBankAccount,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.addBankAccount),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_bankAccounts.isEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'هیچ حساب بانکی اضافه نشده است',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _bankAccounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildBankAccountCard(t, index);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBankAccountCard(AppLocalizations t, int index) {
|
||||
final bankAccount = _bankAccounts[index];
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: bankAccount.bankName,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.bankName,
|
||||
hintText: t.bankName,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateBankAccount(index, bankAccount.copyWith(bankName: value));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
onPressed: () => _removeBankAccount(index),
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: bankAccount.accountNumber ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: t.accountNumber,
|
||||
hintText: t.accountNumber,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateBankAccount(index, bankAccount.copyWith(accountNumber: value.isEmpty ? null : value));
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: bankAccount.cardNumber ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: t.cardNumber,
|
||||
hintText: t.cardNumber,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateBankAccount(index, bankAccount.copyWith(cardNumber: value.isEmpty ? null : value));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: bankAccount.shebaNumber ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: t.shebaNumber,
|
||||
hintText: t.shebaNumber,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_updateBankAccount(index, bankAccount.copyWith(shebaNumber: value.isEmpty ? null : value));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue