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)
|
stats_data = get_business_statistics(db, business_id, ctx)
|
||||||
formatted_data = format_datetime_fields(stats_data, request)
|
formatted_data = format_datetime_fields(stats_data, request)
|
||||||
return success_response(formatted_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(
|
permission_obj = permission_repo.create_or_update(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
business_id=business_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}")
|
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.auth_dependency import get_current_user, AuthContext
|
||||||
from app.core.permissions import require_business_management
|
from app.core.permissions import require_business_management
|
||||||
from app.services.business_service import (
|
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
|
update_business, delete_business, get_business_summary
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -116,8 +116,8 @@ def list_user_businesses(
|
||||||
sort_desc: bool = True,
|
sort_desc: bool = True,
|
||||||
search: str = None
|
search: str = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""لیست کسب و کارهای کاربر"""
|
"""لیست کسب و کارهای کاربر (مالک + عضو)"""
|
||||||
owner_id = ctx.get_user_id()
|
user_id = ctx.get_user_id()
|
||||||
query_dict = {
|
query_dict = {
|
||||||
"take": take,
|
"take": take,
|
||||||
"skip": skip,
|
"skip": skip,
|
||||||
|
|
@ -125,34 +125,10 @@ def list_user_businesses(
|
||||||
"sort_desc": sort_desc,
|
"sort_desc": sort_desc,
|
||||||
"search": search
|
"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 = format_datetime_fields(businesses, request)
|
||||||
|
|
||||||
# اگر formatted_data یک dict با کلید items است، آن را استخراج کنیم
|
return success_response(formatted_data, request)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{business_id}/details",
|
@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 .password_reset import PasswordReset # noqa: F401
|
||||||
from .business import Business # noqa: F401
|
from .business import Business # noqa: F401
|
||||||
from .business_permission import BusinessPermission # noqa: F401
|
from .business_permission import BusinessPermission # noqa: F401
|
||||||
|
from .person import Person, PersonBankAccount # noqa: F401
|
||||||
# Business user models removed - using business_permissions instead
|
# Business user models removed - using business_permissions instead
|
||||||
|
|
||||||
# Import support models
|
# Import support models
|
||||||
|
|
|
||||||
|
|
@ -54,5 +54,5 @@ class Business(Base):
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
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)
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
# Relationships - using business_permissions instead
|
# Relationships
|
||||||
# users = relationship("BusinessUser", back_populates="business", cascade="all, delete-orphan")
|
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 typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import select, and_
|
from sqlalchemy import select, and_, text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from adapters.db.models.business_permission import BusinessPermission
|
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)
|
stmt = select(BusinessPermission).where(BusinessPermission.business_id == business_id)
|
||||||
return self.db.execute(stmt).scalars().all()
|
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}")
|
logger.info(f"Business access check: {business_id} == {self.business_id} = {has_access}")
|
||||||
return 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:
|
def to_dict(self) -> dict:
|
||||||
"""تبدیل به dictionary برای استفاده در API"""
|
"""تبدیل به dictionary برای استفاده در API"""
|
||||||
return {
|
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.businesses import router as businesses_router
|
||||||
from adapters.api.v1.business_dashboard import router as business_dashboard_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.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.tickets import router as support_tickets_router
|
||||||
from adapters.api.v1.support.operator import router as support_operator_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
|
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(businesses_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(business_dashboard_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(business_users_router, prefix=settings.api_v1_prefix)
|
||||||
|
application.include_router(persons_router, prefix=settings.api_v1_prefix)
|
||||||
|
|
||||||
# Support endpoints
|
# Support endpoints
|
||||||
application.include_router(support_tickets_router, prefix=f"{settings.api_v1_prefix}/support")
|
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 sqlalchemy import select, and_, func
|
||||||
|
|
||||||
from adapters.db.repositories.business_repo import BusinessRepository
|
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.db.models.business import Business, BusinessType, BusinessField
|
||||||
from adapters.api.v1.schemas import (
|
from adapters.api.v1.schemas import (
|
||||||
BusinessCreateRequest, BusinessUpdateRequest, BusinessResponse,
|
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]]:
|
def update_business(db: Session, business_id: int, business_data: BusinessUpdateRequest, owner_id: int) -> Optional[Dict[str, Any]]:
|
||||||
"""ویرایش کسب و کار"""
|
"""ویرایش کسب و کار"""
|
||||||
business_repo = BusinessRepository(db)
|
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/business_users.py
|
||||||
adapters/api/v1/businesses.py
|
adapters/api/v1/businesses.py
|
||||||
adapters/api/v1/health.py
|
adapters/api/v1/health.py
|
||||||
|
adapters/api/v1/persons.py
|
||||||
adapters/api/v1/schemas.py
|
adapters/api/v1/schemas.py
|
||||||
adapters/api/v1/users.py
|
adapters/api/v1/users.py
|
||||||
adapters/api/v1/admin/email_config.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/__init__.py
|
||||||
adapters/api/v1/schema_models/email.py
|
adapters/api/v1/schema_models/email.py
|
||||||
adapters/api/v1/schema_models/file_storage.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/__init__.py
|
||||||
adapters/api/v1/support/categories.py
|
adapters/api/v1/support/categories.py
|
||||||
adapters/api/v1/support/operator.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/email_config.py
|
||||||
adapters/db/models/file_storage.py
|
adapters/db/models/file_storage.py
|
||||||
adapters/db/models/password_reset.py
|
adapters/db/models/password_reset.py
|
||||||
|
adapters/db/models/person.py
|
||||||
adapters/db/models/user.py
|
adapters/db/models/user.py
|
||||||
adapters/db/models/support/__init__.py
|
adapters/db/models/support/__init__.py
|
||||||
adapters/db/models/support/category.py
|
adapters/db/models/support/category.py
|
||||||
|
|
@ -75,6 +78,7 @@ app/services/business_service.py
|
||||||
app/services/captcha_service.py
|
app/services/captcha_service.py
|
||||||
app/services/email_service.py
|
app/services/email_service.py
|
||||||
app/services/file_storage_service.py
|
app/services/file_storage_service.py
|
||||||
|
app/services/person_service.py
|
||||||
app/services/query_service.py
|
app/services/query_service.py
|
||||||
app/services/pdf/__init__.py
|
app/services/pdf/__init__.py
|
||||||
app/services/pdf/base_pdf_service.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_000007_create_business_permissions_table.py
|
||||||
migrations/versions/20250117_000008_add_email_config_table.py
|
migrations/versions/20250117_000008_add_email_config_table.py
|
||||||
migrations/versions/20250117_000009_add_is_default_to_email_config.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/20250915_000001_init_auth_tables.py
|
||||||
migrations/versions/20250916_000002_add_referral_fields.py
|
migrations/versions/20250916_000002_add_referral_fields.py
|
||||||
migrations/versions/5553f8745c6e_add_support_tables.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__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import text
|
||||||
from adapters.db.session import get_db
|
from adapters.db.session import get_db
|
||||||
from adapters.db.models.user import User
|
from adapters.db.models.user import User
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@ def list_operators():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
operators = db.query(User).filter(
|
operators = db.query(User).filter(
|
||||||
User.app_permissions['support_operator'].astext == 'true'
|
text("app_permissions->>'support_operator' = 'true'")
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if not operators:
|
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:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import '../models/business_dashboard_models.dart';
|
||||||
|
|
||||||
class AuthStore with ChangeNotifier {
|
class AuthStore with ChangeNotifier {
|
||||||
static const _kApiKey = 'auth_api_key';
|
static const _kApiKey = 'auth_api_key';
|
||||||
|
|
@ -11,12 +12,15 @@ class AuthStore with ChangeNotifier {
|
||||||
static const _kAppPermissions = 'app_permissions';
|
static const _kAppPermissions = 'app_permissions';
|
||||||
static const _kIsSuperAdmin = 'is_superadmin';
|
static const _kIsSuperAdmin = 'is_superadmin';
|
||||||
static const _kLastUrl = 'last_url';
|
static const _kLastUrl = 'last_url';
|
||||||
|
static const _kCurrentBusiness = 'current_business';
|
||||||
|
|
||||||
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
final FlutterSecureStorage _secure = const FlutterSecureStorage();
|
||||||
String? _apiKey;
|
String? _apiKey;
|
||||||
String? _deviceId;
|
String? _deviceId;
|
||||||
Map<String, dynamic>? _appPermissions;
|
Map<String, dynamic>? _appPermissions;
|
||||||
bool _isSuperAdmin = false;
|
bool _isSuperAdmin = false;
|
||||||
|
BusinessWithPermission? _currentBusiness;
|
||||||
|
Map<String, dynamic>? _businessPermissions;
|
||||||
|
|
||||||
String? get apiKey => _apiKey;
|
String? get apiKey => _apiKey;
|
||||||
String get deviceId => _deviceId ?? '';
|
String get deviceId => _deviceId ?? '';
|
||||||
|
|
@ -24,6 +28,8 @@ class AuthStore with ChangeNotifier {
|
||||||
bool get isSuperAdmin => _isSuperAdmin;
|
bool get isSuperAdmin => _isSuperAdmin;
|
||||||
int? _currentUserId;
|
int? _currentUserId;
|
||||||
int? get currentUserId => _currentUserId;
|
int? get currentUserId => _currentUserId;
|
||||||
|
BusinessWithPermission? get currentBusiness => _currentBusiness;
|
||||||
|
Map<String, dynamic>? get businessPermissions => _businessPermissions;
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
@ -234,6 +240,147 @@ class AuthStore with ChangeNotifier {
|
||||||
await prefs.remove(_kLastUrl);
|
await prefs.remove(_kLastUrl);
|
||||||
} catch (_) {}
|
} 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",
|
"peopleList": "People List",
|
||||||
"receipts": "Receipts",
|
"receipts": "Receipts",
|
||||||
"payments": "Payments",
|
"payments": "Payments",
|
||||||
|
"receiptsAndPayments": "Receipts and Payments",
|
||||||
"productsAndServices": "Products and Services",
|
"productsAndServices": "Products and Services",
|
||||||
"products": "Products",
|
"products": "Products",
|
||||||
"services": "Services",
|
"services": "Services",
|
||||||
|
|
@ -777,6 +778,65 @@
|
||||||
"dataBackupDialogContent": "In this section you can create a backup of all business data.",
|
"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.",
|
"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.",
|
"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": "لیست اشخاص",
|
"peopleList": "لیست اشخاص",
|
||||||
"receipts": "دریافتها",
|
"receipts": "دریافتها",
|
||||||
"payments": "پرداختها",
|
"payments": "پرداختها",
|
||||||
|
"receiptsAndPayments": "دریافت و پرداخت",
|
||||||
"productsAndServices": "کالا و خدمات",
|
"productsAndServices": "کالا و خدمات",
|
||||||
"products": "کالاها",
|
"products": "کالاها",
|
||||||
"services": "خدمات",
|
"services": "خدمات",
|
||||||
|
|
@ -776,6 +777,65 @@
|
||||||
"dataBackupDialogContent": "در این بخش میتوانید از تمام اطلاعات کسب و کار نسخه پشتیبان تهیه کنید.",
|
"dataBackupDialogContent": "در این بخش میتوانید از تمام اطلاعات کسب و کار نسخه پشتیبان تهیه کنید.",
|
||||||
"dataRestoreDialogContent": "در این بخش میتوانید اطلاعات را از نسخه پشتیبان قبلی بازیابی کنید.",
|
"dataRestoreDialogContent": "در این بخش میتوانید اطلاعات را از نسخه پشتیبان قبلی بازیابی کنید.",
|
||||||
"systemLogsDialogContent": "در این بخش میتوانید گزارشهای سیستم، خطاها و فعالیتهای کاربران را مشاهده کنید.",
|
"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'**
|
/// **'Payments'**
|
||||||
String get 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.
|
/// No description provided for @productsAndServices.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -3323,7 +3329,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @addPerson.
|
/// No description provided for @addPerson.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Add New Person'**
|
/// **'Add Person'**
|
||||||
String get addPerson;
|
String get addPerson;
|
||||||
|
|
||||||
/// No description provided for @viewPeople.
|
/// No description provided for @viewPeople.
|
||||||
|
|
@ -4219,6 +4225,348 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Account Management'**
|
/// **'Account Management'**
|
||||||
String get accountManagement;
|
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
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -1379,6 +1379,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get payments => 'Payments';
|
String get payments => 'Payments';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get receiptsAndPayments => 'Receipts and Payments';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get productsAndServices => 'Products and Services';
|
String get productsAndServices => 'Products and Services';
|
||||||
|
|
||||||
|
|
@ -1665,7 +1668,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get draft => 'Manage Drafts';
|
String get draft => 'Manage Drafts';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addPerson => 'Add New Person';
|
String get addPerson => 'Add Person';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get viewPeople => 'View People List';
|
String get viewPeople => 'View People List';
|
||||||
|
|
@ -2126,4 +2129,177 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accountManagement => 'Account Management';
|
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
|
@override
|
||||||
String get payments => 'پرداختها';
|
String get payments => 'پرداختها';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get receiptsAndPayments => 'دریافت و پرداخت';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get productsAndServices => 'کالا و خدمات';
|
String get productsAndServices => 'کالا و خدمات';
|
||||||
|
|
||||||
|
|
@ -1655,7 +1658,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get draft => 'مدیریت پیشنویسها';
|
String get draft => 'مدیریت پیشنویسها';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get addPerson => 'افزودن شخص جدید';
|
String get addPerson => 'افزودن شخص';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get viewPeople => 'مشاهده لیست اشخاص';
|
String get viewPeople => 'مشاهده لیست اشخاص';
|
||||||
|
|
@ -2112,4 +2115,175 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get accountManagement => 'مدیریت حساب کاربری';
|
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/dashboard/business_dashboard_page.dart';
|
||||||
import 'pages/business/users_permissions_page.dart';
|
import 'pages/business/users_permissions_page.dart';
|
||||||
import 'pages/business/settings_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/locale_controller.dart';
|
||||||
import 'core/calendar_controller.dart';
|
import 'core/calendar_controller.dart';
|
||||||
import 'core/api_client.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.)
|
// 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(
|
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 '../../core/calendar_controller.dart';
|
||||||
import '../../theme/theme_controller.dart';
|
import '../../theme/theme_controller.dart';
|
||||||
import '../../widgets/combined_user_menu_button.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';
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
|
||||||
class BusinessShell extends StatefulWidget {
|
class BusinessShell extends StatefulWidget {
|
||||||
|
|
@ -31,11 +34,11 @@ class BusinessShell extends StatefulWidget {
|
||||||
|
|
||||||
class _BusinessShellState extends State<BusinessShell> {
|
class _BusinessShellState extends State<BusinessShell> {
|
||||||
int _hoverIndex = -1;
|
int _hoverIndex = -1;
|
||||||
bool _isPeopleExpanded = false;
|
|
||||||
bool _isProductsAndServicesExpanded = false;
|
bool _isProductsAndServicesExpanded = false;
|
||||||
bool _isBankingExpanded = false;
|
bool _isBankingExpanded = false;
|
||||||
bool _isAccountingMenuExpanded = false;
|
bool _isAccountingMenuExpanded = false;
|
||||||
bool _isWarehouseManagementExpanded = false;
|
bool _isWarehouseManagementExpanded = false;
|
||||||
|
final BusinessDashboardService _businessService = BusinessDashboardService(ApiClient());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -46,6 +49,29 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
setState(() {});
|
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
|
@override
|
||||||
|
|
@ -68,7 +94,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
final t = AppLocalizations.of(context);
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
// ساختار متمرکز منو
|
// ساختار متمرکز منو
|
||||||
final menuItems = <_MenuItem>[
|
final allMenuItems = <_MenuItem>[
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
label: t.businessDashboard,
|
label: t.businessDashboard,
|
||||||
icon: Icons.dashboard_outlined,
|
icon: Icons.dashboard_outlined,
|
||||||
|
|
@ -87,33 +113,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
label: t.people,
|
label: t.people,
|
||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
selectedIcon: Icons.people,
|
selectedIcon: Icons.people,
|
||||||
path: null, // برای منوی بازشونده
|
path: '/business/${widget.businessId}/persons',
|
||||||
type: _MenuItemType.expandable,
|
type: _MenuItemType.simple,
|
||||||
children: [
|
hasAddButton: true,
|
||||||
_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',
|
|
||||||
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(
|
_MenuItem(
|
||||||
label: t.productsAndServices,
|
label: t.productsAndServices,
|
||||||
|
|
@ -203,14 +205,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
type: _MenuItemType.simple,
|
type: _MenuItemType.simple,
|
||||||
hasAddButton: true,
|
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(
|
_MenuItem(
|
||||||
|
|
@ -228,6 +222,14 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
type: _MenuItemType.simple,
|
type: _MenuItemType.simple,
|
||||||
hasAddButton: true,
|
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(
|
_MenuItem(
|
||||||
label: t.expenseAndIncome,
|
label: t.expenseAndIncome,
|
||||||
icon: Icons.account_balance_wallet,
|
icon: Icons.account_balance_wallet,
|
||||||
|
|
@ -236,6 +238,22 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
type: _MenuItemType.simple,
|
type: _MenuItemType.simple,
|
||||||
hasAddButton: true,
|
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(
|
_MenuItem(
|
||||||
label: t.accountingMenu,
|
label: t.accountingMenu,
|
||||||
icon: Icons.calculate,
|
icon: Icons.calculate,
|
||||||
|
|
@ -243,14 +261,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
path: null, // برای منوی بازشونده
|
path: null, // برای منوی بازشونده
|
||||||
type: _MenuItemType.expandable,
|
type: _MenuItemType.expandable,
|
||||||
children: [
|
children: [
|
||||||
_MenuItem(
|
|
||||||
label: t.documents,
|
|
||||||
icon: Icons.description,
|
|
||||||
selectedIcon: Icons.description,
|
|
||||||
path: '/business/${widget.businessId}/documents',
|
|
||||||
type: _MenuItemType.simple,
|
|
||||||
hasAddButton: true,
|
|
||||||
),
|
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
label: t.chartOfAccounts,
|
label: t.chartOfAccounts,
|
||||||
icon: Icons.table_chart,
|
icon: Icons.table_chart,
|
||||||
|
|
@ -373,6 +383,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// فیلتر کردن منو بر اساس دسترسیها
|
||||||
|
final menuItems = _getFilteredMenuItems(allMenuItems);
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
for (int i = 0; i < menuItems.length; i++) {
|
for (int i = 0; i < menuItems.length; i++) {
|
||||||
final item = menuItems[i];
|
final item = menuItems[i];
|
||||||
|
|
@ -387,11 +400,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
if (child.path != null && location.startsWith(child.path!)) {
|
if (child.path != null && location.startsWith(child.path!)) {
|
||||||
selectedIndex = i;
|
selectedIndex = i;
|
||||||
// تنظیم وضعیت باز بودن منو
|
// تنظیم وضعیت باز بودن منو
|
||||||
if (i == 2) _isPeopleExpanded = true; // اشخاص در ایندکس 2
|
if (i == 2) _isProductsAndServicesExpanded = true; // کالا و خدمات در ایندکس 2
|
||||||
if (i == 3) _isProductsAndServicesExpanded = true; // کالا و خدمات در ایندکس 3
|
if (i == 3) _isBankingExpanded = true; // بانکداری در ایندکس 3
|
||||||
if (i == 4) _isBankingExpanded = true; // بانکداری در ایندکس 4
|
if (i == 5) _isAccountingMenuExpanded = true; // حسابداری در ایندکس 5
|
||||||
if (i == 6) _isAccountingMenuExpanded = true; // حسابداری در ایندکس 6
|
if (i == 7) _isWarehouseManagementExpanded = true; // انبارداری در ایندکس 7
|
||||||
if (i == 8) _isWarehouseManagementExpanded = true; // انبارداری در ایندکس 8
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,7 +425,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
}
|
}
|
||||||
} else if (item.type == _MenuItemType.expandable) {
|
} else if (item.type == _MenuItemType.expandable) {
|
||||||
// تغییر وضعیت باز/بسته بودن منو
|
// تغییر وضعیت باز/بسته بودن منو
|
||||||
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
|
||||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
||||||
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
||||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
||||||
|
|
@ -449,8 +460,20 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
context.go('/login');
|
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) {
|
bool isExpanded(_MenuItem item) {
|
||||||
if (item.label == t.people) return _isPeopleExpanded;
|
|
||||||
if (item.label == t.productsAndServices) return _isProductsAndServicesExpanded;
|
if (item.label == t.productsAndServices) return _isProductsAndServicesExpanded;
|
||||||
if (item.label == t.banking) return _isBankingExpanded;
|
if (item.label == t.banking) return _isBankingExpanded;
|
||||||
if (item.label == t.accountingMenu) return _isAccountingMenuExpanded;
|
if (item.label == t.accountingMenu) return _isAccountingMenuExpanded;
|
||||||
|
|
@ -622,10 +645,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to add new item
|
// Navigate to add new item
|
||||||
if (child.label == t.receipts) {
|
if (child.label == t.personsList) {
|
||||||
// Navigate to add receipt
|
// Navigate to add person
|
||||||
} else if (child.label == t.payments) {
|
_showAddPersonDialog();
|
||||||
// Navigate to add payment
|
|
||||||
} else if (child.label == t.products) {
|
} else if (child.label == t.products) {
|
||||||
// Navigate to add product
|
// Navigate to add product
|
||||||
} else if (child.label == t.priceLists) {
|
} else if (child.label == t.priceLists) {
|
||||||
|
|
@ -644,14 +666,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
// Navigate to add wallet
|
// Navigate to add wallet
|
||||||
} else if (child.label == t.checks) {
|
} else if (child.label == t.checks) {
|
||||||
// Navigate to add check
|
// Navigate to add check
|
||||||
} else if (child.label == t.transfers) {
|
|
||||||
// Navigate to add transfer
|
|
||||||
} else if (child.label == t.invoice) {
|
} else if (child.label == t.invoice) {
|
||||||
// Navigate to add invoice
|
// Navigate to add invoice
|
||||||
} else if (child.label == t.expenseAndIncome) {
|
} else if (child.label == t.expenseAndIncome) {
|
||||||
// Navigate to add expense/income
|
// Navigate to add expense/income
|
||||||
} else if (child.label == t.documents) {
|
|
||||||
// Navigate to add document
|
|
||||||
} else if (child.label == t.warehouses) {
|
} else if (child.label == t.warehouses) {
|
||||||
// Navigate to add warehouse
|
// Navigate to add warehouse
|
||||||
} else if (child.label == t.shipments) {
|
} else if (child.label == t.shipments) {
|
||||||
|
|
@ -733,7 +751,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (item.type == _MenuItemType.expandable) {
|
if (item.type == _MenuItemType.expandable) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (item.label == t.people) _isPeopleExpanded = !_isPeopleExpanded;
|
|
||||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = !_isProductsAndServicesExpanded;
|
||||||
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
if (item.label == t.banking) _isBankingExpanded = !_isBankingExpanded;
|
||||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = !_isAccountingMenuExpanded;
|
||||||
|
|
@ -781,8 +798,17 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Navigate to add new item
|
// 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
|
// 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) {
|
} else if (item.label == t.expenseAndIncome) {
|
||||||
// Navigate to add expense/income
|
// Navigate to add expense/income
|
||||||
} else if (item.label == t.reports) {
|
} else if (item.label == t.reports) {
|
||||||
|
|
@ -892,7 +918,6 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
initiallyExpanded: isExpanded(item),
|
initiallyExpanded: isExpanded(item),
|
||||||
onExpansionChanged: (expanded) {
|
onExpansionChanged: (expanded) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (item.label == t.people) _isPeopleExpanded = expanded;
|
|
||||||
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = expanded;
|
if (item.label == t.productsAndServices) _isProductsAndServicesExpanded = expanded;
|
||||||
if (item.label == t.banking) _isBankingExpanded = expanded;
|
if (item.label == t.banking) _isBankingExpanded = expanded;
|
||||||
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = expanded;
|
if (item.label == t.accountingMenu) _isAccountingMenuExpanded = expanded;
|
||||||
|
|
@ -906,11 +931,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.pop();
|
context.pop();
|
||||||
// Navigate to add new item
|
// Navigate to add new item
|
||||||
if (child.label == t.receipts) {
|
if (child.label == t.products) {
|
||||||
// Navigate to add receipt
|
|
||||||
} else if (child.label == t.payments) {
|
|
||||||
// Navigate to add payment
|
|
||||||
} else if (child.label == t.products) {
|
|
||||||
// Navigate to add product
|
// Navigate to add product
|
||||||
} else if (child.label == t.priceLists) {
|
} else if (child.label == t.priceLists) {
|
||||||
// Navigate to add price list
|
// Navigate to add price list
|
||||||
|
|
@ -928,14 +949,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
// Navigate to add wallet
|
// Navigate to add wallet
|
||||||
} else if (child.label == t.checks) {
|
} else if (child.label == t.checks) {
|
||||||
// Navigate to add check
|
// Navigate to add check
|
||||||
} else if (child.label == t.transfers) {
|
|
||||||
// Navigate to add transfer
|
|
||||||
} else if (child.label == t.invoice) {
|
} else if (child.label == t.invoice) {
|
||||||
// Navigate to add invoice
|
// Navigate to add invoice
|
||||||
} else if (child.label == t.expenseAndIncome) {
|
} else if (child.label == t.expenseAndIncome) {
|
||||||
// Navigate to add expense/income
|
// Navigate to add expense/income
|
||||||
} else if (child.label == t.documents) {
|
|
||||||
// Navigate to add document
|
|
||||||
} else if (child.label == t.warehouses) {
|
} else if (child.label == t.warehouses) {
|
||||||
// Navigate to add warehouse
|
// Navigate to add warehouse
|
||||||
} else if (child.label == t.shipments) {
|
} else if (child.label == t.shipments) {
|
||||||
|
|
@ -980,6 +997,67 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
body: content,
|
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 }
|
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 {
|
Future<List<BusinessWithPermission>> getUserBusinesses() async {
|
||||||
try {
|
try {
|
||||||
// دریافت کسب و کارهای مالک با POST request
|
// دریافت کسب و کارهای کاربر (مالک + عضو) با POST request
|
||||||
final ownedResponse = await _apiClient.post<Map<String, dynamic>>(
|
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||||
'/api/v1/businesses/list',
|
'/api/v1/businesses/list',
|
||||||
data: {
|
data: {
|
||||||
'take': 100,
|
'take': 100,
|
||||||
|
|
@ -99,32 +99,13 @@ class BusinessDashboardService {
|
||||||
|
|
||||||
List<BusinessWithPermission> businesses = [];
|
List<BusinessWithPermission> businesses = [];
|
||||||
|
|
||||||
if (ownedResponse.data?['success'] == true) {
|
if (response.data?['success'] == true) {
|
||||||
final ownedItems = ownedResponse.data!['data']['items'] as List<dynamic>;
|
final items = response.data!['data']['items'] as List<dynamic>;
|
||||||
businesses.addAll(
|
businesses.addAll(
|
||||||
ownedItems.map((item) {
|
items.map((item) => BusinessWithPermission.fromJson(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: {},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: در آینده میتوان کسب و کارهای عضو را نیز اضافه کرد
|
|
||||||
// از API endpoint جدید برای کسب و کارهای عضو
|
|
||||||
|
|
||||||
return businesses;
|
return businesses;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 401) {
|
if (e.response?.statusCode == 401) {
|
||||||
|
|
@ -136,4 +117,55 @@ class BusinessDashboardService {
|
||||||
throw Exception('خطا در بارگذاری کسب و کارها: $e');
|
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