progress in persons list
This commit is contained in:
parent
e372cac9ae
commit
bd34093dac
|
|
@ -10,7 +10,7 @@ from adapters.api.v1.schema_models.person import (
|
||||||
from adapters.api.v1.schemas import QueryInfo, SuccessResponse
|
from adapters.api.v1.schemas import QueryInfo, SuccessResponse
|
||||||
from app.core.responses import success_response, format_datetime_fields
|
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_dep
|
||||||
from app.services.person_service import (
|
from app.services.person_service import (
|
||||||
create_person, get_person_by_id, get_persons_by_business,
|
create_person, get_person_by_id, get_persons_by_business,
|
||||||
update_person, delete_person, get_person_summary
|
update_person, delete_person, get_person_summary
|
||||||
|
|
@ -59,13 +59,15 @@ async def create_person_endpoint(
|
||||||
person_data: PersonCreateRequest,
|
person_data: PersonCreateRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user),
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
_: None = Depends(require_business_management())
|
_: None = Depends(require_business_management_dep),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""ایجاد شخص جدید برای کسب و کار"""
|
"""ایجاد شخص جدید برای کسب و کار"""
|
||||||
result = create_person(db, business_id, person_data)
|
result = create_person(db, business_id, person_data)
|
||||||
return success_response(
|
return success_response(
|
||||||
|
data=format_datetime_fields(result['data'], request),
|
||||||
|
request=request,
|
||||||
message=result['message'],
|
message=result['message'],
|
||||||
data=format_datetime_fields(result['data'])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -103,7 +105,8 @@ async def get_persons_endpoint(
|
||||||
business_id: int,
|
business_id: int,
|
||||||
query_info: QueryInfo,
|
query_info: QueryInfo,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user)
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""دریافت لیست اشخاص کسب و کار"""
|
"""دریافت لیست اشخاص کسب و کار"""
|
||||||
query_dict = {
|
query_dict = {
|
||||||
|
|
@ -111,17 +114,21 @@ async def get_persons_endpoint(
|
||||||
"skip": query_info.skip,
|
"skip": query_info.skip,
|
||||||
"sort_by": query_info.sort_by,
|
"sort_by": query_info.sort_by,
|
||||||
"sort_desc": query_info.sort_desc,
|
"sort_desc": query_info.sort_desc,
|
||||||
"search": query_info.search
|
"search": query_info.search,
|
||||||
|
"search_fields": query_info.search_fields,
|
||||||
|
"filters": query_info.filters,
|
||||||
}
|
}
|
||||||
result = get_persons_by_business(db, business_id, query_dict)
|
result = get_persons_by_business(db, business_id, query_dict)
|
||||||
|
|
||||||
# فرمت کردن تاریخها
|
# فرمت کردن تاریخها
|
||||||
for item in result['items']:
|
result['items'] = [
|
||||||
item = format_datetime_fields(item)
|
format_datetime_fields(item, request) for item in result['items']
|
||||||
|
]
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
|
data=result,
|
||||||
|
request=request,
|
||||||
message="لیست اشخاص با موفقیت دریافت شد",
|
message="لیست اشخاص با موفقیت دریافت شد",
|
||||||
data=result
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -142,7 +149,8 @@ async def get_person_endpoint(
|
||||||
person_id: int,
|
person_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user),
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
_: None = Depends(require_business_management())
|
_: None = Depends(require_business_management_dep),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""دریافت جزئیات شخص"""
|
"""دریافت جزئیات شخص"""
|
||||||
# ابتدا باید business_id را از person دریافت کنیم
|
# ابتدا باید business_id را از person دریافت کنیم
|
||||||
|
|
@ -155,8 +163,9 @@ async def get_person_endpoint(
|
||||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
|
data=format_datetime_fields(result, request),
|
||||||
|
request=request,
|
||||||
message="جزئیات شخص با موفقیت دریافت شد",
|
message="جزئیات شخص با موفقیت دریافت شد",
|
||||||
data=format_datetime_fields(result)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -178,7 +187,8 @@ async def update_person_endpoint(
|
||||||
person_data: PersonUpdateRequest,
|
person_data: PersonUpdateRequest,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user),
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
_: None = Depends(require_business_management())
|
_: None = Depends(require_business_management_dep),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""ویرایش شخص"""
|
"""ویرایش شخص"""
|
||||||
# ابتدا باید business_id را از person دریافت کنیم
|
# ابتدا باید business_id را از person دریافت کنیم
|
||||||
|
|
@ -191,8 +201,9 @@ async def update_person_endpoint(
|
||||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
|
data=format_datetime_fields(result['data'], request),
|
||||||
|
request=request,
|
||||||
message=result['message'],
|
message=result['message'],
|
||||||
data=format_datetime_fields(result['data'])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -213,7 +224,8 @@ async def delete_person_endpoint(
|
||||||
person_id: int,
|
person_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user),
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
_: None = Depends(require_business_management())
|
_: None = Depends(require_business_management_dep),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""حذف شخص"""
|
"""حذف شخص"""
|
||||||
# ابتدا باید business_id را از person دریافت کنیم
|
# ابتدا باید business_id را از person دریافت کنیم
|
||||||
|
|
@ -225,7 +237,7 @@ async def delete_person_endpoint(
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
raise HTTPException(status_code=404, detail="شخص یافت نشد")
|
||||||
|
|
||||||
return success_response(message="شخص با موفقیت حذف شد")
|
return success_response(message="شخص با موفقیت حذف شد", request=request)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/businesses/{business_id}/persons/summary",
|
@router.get("/businesses/{business_id}/persons/summary",
|
||||||
|
|
@ -242,12 +254,14 @@ async def get_persons_summary_endpoint(
|
||||||
business_id: int,
|
business_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
auth_context: AuthContext = Depends(get_current_user),
|
auth_context: AuthContext = Depends(get_current_user),
|
||||||
_: None = Depends(require_business_management())
|
_: None = Depends(require_business_management_dep),
|
||||||
|
request: Request = None,
|
||||||
):
|
):
|
||||||
"""دریافت خلاصه اشخاص کسب و کار"""
|
"""دریافت خلاصه اشخاص کسب و کار"""
|
||||||
result = get_person_summary(db, business_id)
|
result = get_person_summary(db, business_id)
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
|
data=result,
|
||||||
|
request=request,
|
||||||
message="خلاصه اشخاص با موفقیت دریافت شد",
|
message="خلاصه اشخاص با موفقیت دریافت شد",
|
||||||
data=result
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,12 @@ class PersonBankAccountResponse(BaseModel):
|
||||||
class PersonCreateRequest(BaseModel):
|
class PersonCreateRequest(BaseModel):
|
||||||
"""درخواست ایجاد شخص جدید"""
|
"""درخواست ایجاد شخص جدید"""
|
||||||
# اطلاعات پایه
|
# اطلاعات پایه
|
||||||
|
code: Optional[int] = Field(default=None, ge=1, description="کد یکتا در هر کسب و کار (در صورت عدم ارسال، خودکار تولید میشود)")
|
||||||
alias_name: str = Field(..., min_length=1, max_length=255, description="نام مستعار (الزامی)")
|
alias_name: str = Field(..., min_length=1, max_length=255, description="نام مستعار (الزامی)")
|
||||||
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
|
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
|
||||||
last_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="نوع شخص")
|
person_type: Optional[PersonType] = Field(default=None, description="نوع شخص (سازگاری قدیمی)")
|
||||||
|
person_types: Optional[List[PersonType]] = Field(default=None, description="انواع شخص (چندانتخابی)")
|
||||||
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
||||||
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
||||||
|
|
||||||
|
|
@ -81,10 +83,12 @@ class PersonCreateRequest(BaseModel):
|
||||||
class PersonUpdateRequest(BaseModel):
|
class PersonUpdateRequest(BaseModel):
|
||||||
"""درخواست ویرایش شخص"""
|
"""درخواست ویرایش شخص"""
|
||||||
# اطلاعات پایه
|
# اطلاعات پایه
|
||||||
|
code: Optional[int] = Field(default=None, ge=1, description="کد یکتا در هر کسب و کار")
|
||||||
alias_name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام مستعار")
|
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="نام")
|
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
|
||||||
last_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="نوع شخص")
|
person_type: Optional[PersonType] = Field(default=None, description="نوع شخص (سازگاری قدیمی)")
|
||||||
|
person_types: Optional[List[PersonType]] = Field(default=None, description="انواع شخص (چندانتخابی)")
|
||||||
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
|
||||||
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
|
||||||
|
|
||||||
|
|
@ -115,10 +119,12 @@ class PersonResponse(BaseModel):
|
||||||
business_id: int = Field(..., description="شناسه کسب و کار")
|
business_id: int = Field(..., description="شناسه کسب و کار")
|
||||||
|
|
||||||
# اطلاعات پایه
|
# اطلاعات پایه
|
||||||
|
code: Optional[int] = Field(default=None, description="کد یکتا")
|
||||||
alias_name: str = Field(..., description="نام مستعار")
|
alias_name: str = Field(..., description="نام مستعار")
|
||||||
first_name: Optional[str] = Field(default=None, description="نام")
|
first_name: Optional[str] = Field(default=None, description="نام")
|
||||||
last_name: Optional[str] = Field(default=None, description="نام خانوادگی")
|
last_name: Optional[str] = Field(default=None, description="نام خانوادگی")
|
||||||
person_type: str = Field(..., description="نوع شخص")
|
person_type: str = Field(..., description="نوع شخص")
|
||||||
|
person_types: List[str] = Field(default_factory=list, description="انواع شخص")
|
||||||
company_name: Optional[str] = Field(default=None, description="نام شرکت")
|
company_name: Optional[str] = Field(default=None, description="نام شرکت")
|
||||||
payment_id: Optional[str] = Field(default=None, description="شناسه پرداخت")
|
payment_id: Optional[str] = Field(default=None, description="شناسه پرداخت")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text, Boolean
|
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text, UniqueConstraint
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from adapters.db.session import Base
|
from adapters.db.session import Base
|
||||||
|
|
@ -21,15 +21,20 @@ class PersonType(str, Enum):
|
||||||
|
|
||||||
class Person(Base):
|
class Person(Base):
|
||||||
__tablename__ = "persons"
|
__tablename__ = "persons"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('business_id', 'code', name='uq_persons_business_code'),
|
||||||
|
)
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
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)
|
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
|
||||||
# اطلاعات پایه
|
# اطلاعات پایه
|
||||||
|
code: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="کد یکتا در هر کسب و کار")
|
||||||
alias_name: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="نام مستعار (الزامی)")
|
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="نام")
|
first_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="نام")
|
||||||
last_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="نوع شخص")
|
person_type: Mapped[PersonType] = mapped_column(SQLEnum(PersonType), nullable=False, comment="نوع شخص")
|
||||||
|
person_types: Mapped[str | None] = mapped_column(Text, nullable=True, comment="لیست انواع شخص به صورت JSON")
|
||||||
company_name: Mapped[str | None] = mapped_column(String(255), nullable=True, 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="شناسه پرداخت")
|
payment_id: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="شناسه پرداخت")
|
||||||
|
|
||||||
|
|
@ -50,9 +55,6 @@ class Person(Base):
|
||||||
email: Mapped[str | None] = mapped_column(String(255), 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="وبسایت")
|
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)
|
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)
|
||||||
|
|
@ -74,9 +76,6 @@ class PersonBankAccount(Base):
|
||||||
card_number: Mapped[str | None] = mapped_column(String(20), 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="شماره شبا")
|
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)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,24 @@ def require_system_settings():
|
||||||
def require_permission(permission: str):
|
def require_permission(permission: str):
|
||||||
"""Decorator عمومی برای بررسی دسترسی - wrapper برای require_app_permission"""
|
"""Decorator عمومی برای بررسی دسترسی - wrapper برای require_app_permission"""
|
||||||
return require_app_permission(permission)
|
return require_app_permission(permission)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# FastAPI Dependencies (for Depends)
|
||||||
|
# =========================
|
||||||
|
def require_app_permission_dep(permission: str):
|
||||||
|
"""FastAPI dependency جهت بررسی دسترسی در سطح اپلیکیشن.
|
||||||
|
|
||||||
|
استفاده:
|
||||||
|
_: None = Depends(require_app_permission_dep("business_management"))
|
||||||
|
"""
|
||||||
|
def _dependency(auth_context: AuthContext = Depends(get_current_user)) -> None:
|
||||||
|
if not auth_context.has_app_permission(permission):
|
||||||
|
raise ApiError("FORBIDDEN", f"Missing app permission: {permission}", http_status=403)
|
||||||
|
return _dependency
|
||||||
|
|
||||||
|
|
||||||
|
def require_business_management_dep(auth_context: AuthContext = Depends(get_current_user)) -> None:
|
||||||
|
"""FastAPI dependency برای بررسی مجوز مدیریت کسب و کارها."""
|
||||||
|
if not auth_context.has_app_permission("business_management"):
|
||||||
|
raise ApiError("FORBIDDEN", "Missing app permission: business_management", http_status=403)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
|
import json
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
from app.core.responses import ApiError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import and_, or_, func
|
from sqlalchemy import and_, or_, func
|
||||||
from adapters.db.models.person import Person, PersonBankAccount, PersonType
|
from adapters.db.models.person import Person, PersonBankAccount, PersonType
|
||||||
|
|
@ -10,13 +13,36 @@ from app.core.responses import success_response
|
||||||
|
|
||||||
def create_person(db: Session, business_id: int, person_data: PersonCreateRequest) -> Dict[str, Any]:
|
def create_person(db: Session, business_id: int, person_data: PersonCreateRequest) -> Dict[str, Any]:
|
||||||
"""ایجاد شخص جدید"""
|
"""ایجاد شخص جدید"""
|
||||||
|
# محاسبه/اعتبارسنجی کد یکتا
|
||||||
|
code: Optional[int] = getattr(person_data, 'code', None)
|
||||||
|
if code is not None:
|
||||||
|
exists = db.query(Person).filter(
|
||||||
|
and_(Person.business_id == business_id, Person.code == code)
|
||||||
|
).first()
|
||||||
|
if exists:
|
||||||
|
raise ApiError("DUPLICATE_PERSON_CODE", "کد شخص تکراری است", http_status=400)
|
||||||
|
else:
|
||||||
|
# تولید خودکار کد: بیشینه فعلی + 1 (نسبت به همان کسب و کار)
|
||||||
|
max_code = db.query(func.max(Person.code)).filter(Person.business_id == business_id).scalar()
|
||||||
|
code = (max_code or 0) + 1
|
||||||
|
|
||||||
|
# آمادهسازی person_types (چندانتخابی) و سازگاری person_type تکی
|
||||||
|
types_list: List[str] = []
|
||||||
|
if getattr(person_data, 'person_types', None):
|
||||||
|
types_list = [t.value if hasattr(t, 'value') else str(t) for t in person_data.person_types] # type: ignore[attr-defined]
|
||||||
|
elif getattr(person_data, 'person_type', None):
|
||||||
|
t = person_data.person_type
|
||||||
|
types_list = [t.value if hasattr(t, 'value') else str(t)]
|
||||||
|
|
||||||
# ایجاد شخص
|
# ایجاد شخص
|
||||||
person = Person(
|
person = Person(
|
||||||
business_id=business_id,
|
business_id=business_id,
|
||||||
|
code=code,
|
||||||
alias_name=person_data.alias_name,
|
alias_name=person_data.alias_name,
|
||||||
first_name=person_data.first_name,
|
first_name=person_data.first_name,
|
||||||
last_name=person_data.last_name,
|
last_name=person_data.last_name,
|
||||||
person_type=person_data.person_type,
|
person_type=person_data.person_type or (PersonType(types_list[0]) if types_list else PersonType.CUSTOMER),
|
||||||
|
person_types=json.dumps(types_list, ensure_ascii=False) if types_list else None,
|
||||||
company_name=person_data.company_name,
|
company_name=person_data.company_name,
|
||||||
payment_id=person_data.payment_id,
|
payment_id=person_data.payment_id,
|
||||||
national_id=person_data.national_id,
|
national_id=person_data.national_id,
|
||||||
|
|
@ -49,7 +75,11 @@ def create_person(db: Session, business_id: int, person_data: PersonCreateReques
|
||||||
)
|
)
|
||||||
db.add(bank_account)
|
db.add(bank_account)
|
||||||
|
|
||||||
db.commit()
|
try:
|
||||||
|
db.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
db.rollback()
|
||||||
|
raise ApiError("DUPLICATE_PERSON_CODE", "کد شخص تکراری است", http_status=400)
|
||||||
db.refresh(person)
|
db.refresh(person)
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
|
|
@ -84,6 +114,13 @@ def get_persons_by_business(
|
||||||
search_conditions = []
|
search_conditions = []
|
||||||
|
|
||||||
for field in query_info['search_fields']:
|
for field in query_info['search_fields']:
|
||||||
|
if field == 'code':
|
||||||
|
# تبدیل به رشته برای جستجو مانند LIKE
|
||||||
|
try:
|
||||||
|
code_int = int(query_info['search']) # type: ignore[arg-type]
|
||||||
|
search_conditions.append(Person.code == code_int)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if field == 'alias_name':
|
if field == 'alias_name':
|
||||||
search_conditions.append(Person.alias_name.ilike(search_term))
|
search_conditions.append(Person.alias_name.ilike(search_term))
|
||||||
elif field == 'first_name':
|
elif field == 'first_name':
|
||||||
|
|
@ -105,28 +142,108 @@ def get_persons_by_business(
|
||||||
# اعمال فیلترها
|
# اعمال فیلترها
|
||||||
if query_info.get('filters'):
|
if query_info.get('filters'):
|
||||||
for filter_item in query_info['filters']:
|
for filter_item in query_info['filters']:
|
||||||
field = filter_item.get('property')
|
# پشتیبانی از هر دو حالت: دیکشنری یا شیء Pydantic
|
||||||
operator = filter_item.get('operator')
|
if isinstance(filter_item, dict):
|
||||||
value = filter_item.get('value')
|
field = filter_item.get('property')
|
||||||
|
operator = filter_item.get('operator')
|
||||||
|
value = filter_item.get('value')
|
||||||
|
else:
|
||||||
|
field = getattr(filter_item, 'property', None)
|
||||||
|
operator = getattr(filter_item, 'operator', None)
|
||||||
|
value = getattr(filter_item, 'value', None)
|
||||||
|
|
||||||
|
if not field or not operator:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# کد
|
||||||
|
if field == 'code':
|
||||||
|
if operator == '=':
|
||||||
|
query = query.filter(Person.code == value)
|
||||||
|
elif operator == 'in' and isinstance(value, list):
|
||||||
|
query = query.filter(Person.code.in_(value))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# نوع شخص تکانتخابی
|
||||||
if field == 'person_type':
|
if field == 'person_type':
|
||||||
if operator == '=':
|
if operator == '=':
|
||||||
query = query.filter(Person.person_type == value)
|
query = query.filter(Person.person_type == value)
|
||||||
elif operator == 'in':
|
elif operator == 'in' and isinstance(value, list):
|
||||||
query = query.filter(Person.person_type.in_(value))
|
query = query.filter(Person.person_type.in_(value))
|
||||||
elif field == 'is_active':
|
continue
|
||||||
|
|
||||||
|
# انواع شخص چندانتخابی (رشته JSON)
|
||||||
|
if field == 'person_types':
|
||||||
|
if operator == '=' and isinstance(value, str):
|
||||||
|
query = query.filter(Person.person_types.ilike(f'%"{value}"%'))
|
||||||
|
elif operator == 'in' and isinstance(value, list):
|
||||||
|
sub_filters = [Person.person_types.ilike(f'%"{v}"%') for v in value]
|
||||||
|
if sub_filters:
|
||||||
|
query = query.filter(or_(*sub_filters))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# فیلترهای متنی عمومی (حمایت از عملگرهای contains/startsWith/endsWith)
|
||||||
|
def apply_text_filter(column):
|
||||||
|
nonlocal query
|
||||||
if operator == '=':
|
if operator == '=':
|
||||||
query = query.filter(Person.is_active == value)
|
query = query.filter(column == value)
|
||||||
elif field == 'country':
|
elif operator == 'like' or operator == '*':
|
||||||
if operator == '=':
|
query = query.filter(column.ilike(f"%{value}%"))
|
||||||
query = query.filter(Person.country == value)
|
elif operator == '*?': # starts with
|
||||||
elif operator == 'like':
|
query = query.filter(column.ilike(f"{value}%"))
|
||||||
query = query.filter(Person.country.ilike(f"%{value}%"))
|
elif operator == '?*': # ends with
|
||||||
elif field == 'province':
|
query = query.filter(column.ilike(f"%{value}"))
|
||||||
if operator == '=':
|
|
||||||
query = query.filter(Person.province == value)
|
if field == 'country':
|
||||||
elif operator == 'like':
|
apply_text_filter(Person.country)
|
||||||
query = query.filter(Person.province.ilike(f"%{value}%"))
|
continue
|
||||||
|
|
||||||
|
if field == 'province':
|
||||||
|
apply_text_filter(Person.province)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'alias_name':
|
||||||
|
apply_text_filter(Person.alias_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'first_name':
|
||||||
|
apply_text_filter(Person.first_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'last_name':
|
||||||
|
apply_text_filter(Person.last_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'company_name':
|
||||||
|
apply_text_filter(Person.company_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'mobile':
|
||||||
|
apply_text_filter(Person.mobile)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'email':
|
||||||
|
apply_text_filter(Person.email)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'national_id':
|
||||||
|
apply_text_filter(Person.national_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'registration_number':
|
||||||
|
apply_text_filter(Person.registration_number)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'economic_id':
|
||||||
|
apply_text_filter(Person.economic_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'city':
|
||||||
|
apply_text_filter(Person.city)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field == 'address':
|
||||||
|
apply_text_filter(Person.address)
|
||||||
|
continue
|
||||||
|
|
||||||
# شمارش کل رکوردها
|
# شمارش کل رکوردها
|
||||||
total = query.count()
|
total = query.count()
|
||||||
|
|
@ -135,7 +252,9 @@ def get_persons_by_business(
|
||||||
sort_by = query_info.get('sort_by', 'created_at')
|
sort_by = query_info.get('sort_by', 'created_at')
|
||||||
sort_desc = query_info.get('sort_desc', True)
|
sort_desc = query_info.get('sort_desc', True)
|
||||||
|
|
||||||
if sort_by == 'alias_name':
|
if sort_by == 'code':
|
||||||
|
query = query.order_by(Person.code.desc() if sort_desc else Person.code.asc())
|
||||||
|
elif sort_by == 'alias_name':
|
||||||
query = query.order_by(Person.alias_name.desc() if sort_desc else Person.alias_name.asc())
|
query = query.order_by(Person.alias_name.desc() if sort_desc else Person.alias_name.asc())
|
||||||
elif sort_by == 'first_name':
|
elif sort_by == 'first_name':
|
||||||
query = query.order_by(Person.first_name.desc() if sort_desc else Person.first_name.asc())
|
query = query.order_by(Person.first_name.desc() if sort_desc else Person.first_name.asc())
|
||||||
|
|
@ -195,8 +314,35 @@ def update_person(
|
||||||
|
|
||||||
# بهروزرسانی فیلدها
|
# بهروزرسانی فیلدها
|
||||||
update_data = person_data.dict(exclude_unset=True)
|
update_data = person_data.dict(exclude_unset=True)
|
||||||
for field, value in update_data.items():
|
|
||||||
setattr(person, field, value)
|
# مدیریت کد یکتا
|
||||||
|
if 'code' in update_data and update_data['code'] is not None:
|
||||||
|
desired_code = update_data['code']
|
||||||
|
exists = db.query(Person).filter(
|
||||||
|
and_(Person.business_id == business_id, Person.code == desired_code, Person.id != person_id)
|
||||||
|
).first()
|
||||||
|
if exists:
|
||||||
|
raise ValueError("کد شخص تکراری است")
|
||||||
|
person.code = desired_code
|
||||||
|
|
||||||
|
# مدیریت انواع شخص چندگانه
|
||||||
|
types_list: Optional[List[str]] = None
|
||||||
|
if 'person_types' in update_data and update_data['person_types'] is not None:
|
||||||
|
incoming = update_data['person_types'] or []
|
||||||
|
types_list = [t.value if hasattr(t, 'value') else str(t) for t in incoming]
|
||||||
|
person.person_types = json.dumps(types_list, ensure_ascii=False) if types_list else None
|
||||||
|
# همگام کردن person_type تکی برای سازگاری
|
||||||
|
if types_list:
|
||||||
|
try:
|
||||||
|
person.person_type = PersonType(types_list[0])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# سایر فیلدها
|
||||||
|
for field in list(update_data.keys()):
|
||||||
|
if field in {'code', 'person_types'}:
|
||||||
|
continue
|
||||||
|
setattr(person, field, update_data[field])
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(person)
|
db.refresh(person)
|
||||||
|
|
@ -227,12 +373,9 @@ def get_person_summary(db: Session, business_id: int) -> Dict[str, Any]:
|
||||||
# تعداد کل اشخاص
|
# تعداد کل اشخاص
|
||||||
total_persons = db.query(Person).filter(Person.business_id == business_id).count()
|
total_persons = db.query(Person).filter(Person.business_id == business_id).count()
|
||||||
|
|
||||||
# تعداد اشخاص فعال و غیرفعال
|
# حذف مفهوم فعال/غیرفعال
|
||||||
active_persons = db.query(Person).filter(
|
active_persons = 0
|
||||||
and_(Person.business_id == business_id, Person.is_active == True)
|
inactive_persons = total_persons
|
||||||
).count()
|
|
||||||
|
|
||||||
inactive_persons = total_persons - active_persons
|
|
||||||
|
|
||||||
# تعداد بر اساس نوع
|
# تعداد بر اساس نوع
|
||||||
by_type = {}
|
by_type = {}
|
||||||
|
|
@ -252,13 +395,25 @@ def get_person_summary(db: Session, business_id: int) -> Dict[str, Any]:
|
||||||
|
|
||||||
def _person_to_dict(person: Person) -> Dict[str, Any]:
|
def _person_to_dict(person: Person) -> Dict[str, Any]:
|
||||||
"""تبدیل مدل Person به دیکشنری"""
|
"""تبدیل مدل Person به دیکشنری"""
|
||||||
|
# Parse person_types JSON to list
|
||||||
|
types_list: List[str] = []
|
||||||
|
if person.person_types:
|
||||||
|
try:
|
||||||
|
types = json.loads(person.person_types)
|
||||||
|
if isinstance(types, list):
|
||||||
|
types_list = [str(x) for x in types]
|
||||||
|
except Exception:
|
||||||
|
types_list = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': person.id,
|
'id': person.id,
|
||||||
'business_id': person.business_id,
|
'business_id': person.business_id,
|
||||||
|
'code': person.code,
|
||||||
'alias_name': person.alias_name,
|
'alias_name': person.alias_name,
|
||||||
'first_name': person.first_name,
|
'first_name': person.first_name,
|
||||||
'last_name': person.last_name,
|
'last_name': person.last_name,
|
||||||
'person_type': person.person_type.value,
|
'person_type': person.person_type.value,
|
||||||
|
'person_types': types_list,
|
||||||
'company_name': person.company_name,
|
'company_name': person.company_name,
|
||||||
'payment_id': person.payment_id,
|
'payment_id': person.payment_id,
|
||||||
'national_id': person.national_id,
|
'national_id': person.national_id,
|
||||||
|
|
@ -274,7 +429,6 @@ def _person_to_dict(person: Person) -> Dict[str, Any]:
|
||||||
'fax': person.fax,
|
'fax': person.fax,
|
||||||
'email': person.email,
|
'email': person.email,
|
||||||
'website': person.website,
|
'website': person.website,
|
||||||
'is_active': person.is_active,
|
|
||||||
'created_at': person.created_at.isoformat(),
|
'created_at': person.created_at.isoformat(),
|
||||||
'updated_at': person.updated_at.isoformat(),
|
'updated_at': person.updated_at.isoformat(),
|
||||||
'bank_accounts': [
|
'bank_accounts': [
|
||||||
|
|
@ -285,7 +439,6 @@ def _person_to_dict(person: Person) -> Dict[str, Any]:
|
||||||
'account_number': ba.account_number,
|
'account_number': ba.account_number,
|
||||||
'card_number': ba.card_number,
|
'card_number': ba.card_number,
|
||||||
'sheba_number': ba.sheba_number,
|
'sheba_number': ba.sheba_number,
|
||||||
'is_active': ba.is_active,
|
|
||||||
'created_at': ba.created_at.isoformat(),
|
'created_at': ba.created_at.isoformat(),
|
||||||
'updated_at': ba.updated_at.isoformat(),
|
'updated_at': ba.updated_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,8 @@ migrations/versions/20250120_000001_add_persons_tables.py
|
||||||
migrations/versions/20250120_000002_add_join_permission.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/20250926_000010_add_person_code_and_types.py
|
||||||
|
migrations/versions/20250926_000011_drop_person_is_active.py
|
||||||
migrations/versions/5553f8745c6e_add_support_tables.py
|
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||||
tests/__init__.py
|
tests/__init__.py
|
||||||
tests/test_health.py
|
tests/test_health.py
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20250926_000010_add_person_code_and_types'
|
||||||
|
down_revision = '20250916_000002'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('code', sa.Integer(), nullable=True))
|
||||||
|
batch_op.add_column(sa.Column('person_types', sa.Text(), nullable=True))
|
||||||
|
batch_op.create_unique_constraint('uq_persons_business_code', ['business_id', 'code'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
batch_op.drop_constraint('uq_persons_business_code', type_='unique')
|
||||||
|
batch_op.drop_column('person_types')
|
||||||
|
batch_op.drop_column('code')
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20250926_000011_drop_person_is_active'
|
||||||
|
down_revision = '20250926_000010_add_person_code_and_types'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
try:
|
||||||
|
batch_op.drop_column('is_active')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
with op.batch_alter_table('person_bank_accounts') as batch_op:
|
||||||
|
try:
|
||||||
|
batch_op.drop_column('is_active')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table('persons') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')))
|
||||||
|
with op.batch_alter_table('person_bank_accounts') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')))
|
||||||
|
|
@ -97,10 +97,12 @@ enum PersonType {
|
||||||
class Person {
|
class Person {
|
||||||
final int? id;
|
final int? id;
|
||||||
final int businessId;
|
final int businessId;
|
||||||
|
final int? code;
|
||||||
final String aliasName;
|
final String aliasName;
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
final String? lastName;
|
final String? lastName;
|
||||||
final PersonType personType;
|
final PersonType personType;
|
||||||
|
final List<PersonType> personTypes;
|
||||||
final String? companyName;
|
final String? companyName;
|
||||||
final String? paymentId;
|
final String? paymentId;
|
||||||
final String? nationalId;
|
final String? nationalId;
|
||||||
|
|
@ -124,10 +126,12 @@ class Person {
|
||||||
Person({
|
Person({
|
||||||
this.id,
|
this.id,
|
||||||
required this.businessId,
|
required this.businessId,
|
||||||
|
this.code,
|
||||||
required this.aliasName,
|
required this.aliasName,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
this.lastName,
|
this.lastName,
|
||||||
required this.personType,
|
required this.personType,
|
||||||
|
this.personTypes = const [],
|
||||||
this.companyName,
|
this.companyName,
|
||||||
this.paymentId,
|
this.paymentId,
|
||||||
this.nationalId,
|
this.nationalId,
|
||||||
|
|
@ -150,13 +154,22 @@ class Person {
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Person.fromJson(Map<String, dynamic> json) {
|
factory Person.fromJson(Map<String, dynamic> json) {
|
||||||
|
final List<PersonType> types = (json['person_types'] as List?)
|
||||||
|
?.map((e) => PersonType.fromString(e.toString()))
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
final PersonType primaryType = types.isNotEmpty
|
||||||
|
? types.first
|
||||||
|
: PersonType.fromString(json['person_type']);
|
||||||
return Person(
|
return Person(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
businessId: json['business_id'],
|
businessId: json['business_id'],
|
||||||
|
code: json['code'],
|
||||||
aliasName: json['alias_name'],
|
aliasName: json['alias_name'],
|
||||||
firstName: json['first_name'],
|
firstName: json['first_name'],
|
||||||
lastName: json['last_name'],
|
lastName: json['last_name'],
|
||||||
personType: PersonType.fromString(json['person_type']),
|
personType: primaryType,
|
||||||
|
personTypes: types,
|
||||||
companyName: json['company_name'],
|
companyName: json['company_name'],
|
||||||
paymentId: json['payment_id'],
|
paymentId: json['payment_id'],
|
||||||
nationalId: json['national_id'],
|
nationalId: json['national_id'],
|
||||||
|
|
@ -185,10 +198,12 @@ class Person {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
'business_id': businessId,
|
'business_id': businessId,
|
||||||
|
'code': code,
|
||||||
'alias_name': aliasName,
|
'alias_name': aliasName,
|
||||||
'first_name': firstName,
|
'first_name': firstName,
|
||||||
'last_name': lastName,
|
'last_name': lastName,
|
||||||
'person_type': personType.persianName,
|
'person_type': personType.persianName,
|
||||||
|
'person_types': personTypes.map((t) => t.persianName).toList(),
|
||||||
'company_name': companyName,
|
'company_name': companyName,
|
||||||
'payment_id': paymentId,
|
'payment_id': paymentId,
|
||||||
'national_id': nationalId,
|
'national_id': nationalId,
|
||||||
|
|
@ -285,9 +300,10 @@ class Person {
|
||||||
|
|
||||||
class PersonCreateRequest {
|
class PersonCreateRequest {
|
||||||
final String aliasName;
|
final String aliasName;
|
||||||
|
final int? code;
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
final String? lastName;
|
final String? lastName;
|
||||||
final PersonType personType;
|
final List<PersonType> personTypes;
|
||||||
final String? companyName;
|
final String? companyName;
|
||||||
final String? paymentId;
|
final String? paymentId;
|
||||||
final String? nationalId;
|
final String? nationalId;
|
||||||
|
|
@ -307,9 +323,10 @@ class PersonCreateRequest {
|
||||||
|
|
||||||
PersonCreateRequest({
|
PersonCreateRequest({
|
||||||
required this.aliasName,
|
required this.aliasName,
|
||||||
|
this.code,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
this.lastName,
|
this.lastName,
|
||||||
required this.personType,
|
this.personTypes = const [],
|
||||||
this.companyName,
|
this.companyName,
|
||||||
this.paymentId,
|
this.paymentId,
|
||||||
this.nationalId,
|
this.nationalId,
|
||||||
|
|
@ -331,9 +348,10 @@ class PersonCreateRequest {
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'alias_name': aliasName,
|
'alias_name': aliasName,
|
||||||
|
if (code != null) 'code': code,
|
||||||
'first_name': firstName,
|
'first_name': firstName,
|
||||||
'last_name': lastName,
|
'last_name': lastName,
|
||||||
'person_type': personType.persianName,
|
if (personTypes.isNotEmpty) 'person_types': personTypes.map((t) => t.persianName).toList(),
|
||||||
'company_name': companyName,
|
'company_name': companyName,
|
||||||
'payment_id': paymentId,
|
'payment_id': paymentId,
|
||||||
'national_id': nationalId,
|
'national_id': nationalId,
|
||||||
|
|
@ -349,16 +367,27 @@ class PersonCreateRequest {
|
||||||
'fax': fax,
|
'fax': fax,
|
||||||
'email': email,
|
'email': email,
|
||||||
'website': website,
|
'website': website,
|
||||||
'bank_accounts': bankAccounts.map((ba) => ba.toJson()).toList(),
|
// Only send fields expected by backend for create
|
||||||
|
'bank_accounts': bankAccounts
|
||||||
|
.where((ba) => (ba.bankName).trim().isNotEmpty)
|
||||||
|
.map((ba) => {
|
||||||
|
'bank_name': ba.bankName,
|
||||||
|
'account_number': ba.accountNumber,
|
||||||
|
'card_number': ba.cardNumber,
|
||||||
|
'sheba_number': ba.shebaNumber,
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PersonUpdateRequest {
|
class PersonUpdateRequest {
|
||||||
|
final int? code;
|
||||||
final String? aliasName;
|
final String? aliasName;
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
final String? lastName;
|
final String? lastName;
|
||||||
final PersonType? personType;
|
final PersonType? personType;
|
||||||
|
final List<PersonType>? personTypes;
|
||||||
final String? companyName;
|
final String? companyName;
|
||||||
final String? paymentId;
|
final String? paymentId;
|
||||||
final String? nationalId;
|
final String? nationalId;
|
||||||
|
|
@ -377,10 +406,12 @@ class PersonUpdateRequest {
|
||||||
final bool? isActive;
|
final bool? isActive;
|
||||||
|
|
||||||
PersonUpdateRequest({
|
PersonUpdateRequest({
|
||||||
|
this.code,
|
||||||
this.aliasName,
|
this.aliasName,
|
||||||
this.firstName,
|
this.firstName,
|
||||||
this.lastName,
|
this.lastName,
|
||||||
this.personType,
|
this.personType,
|
||||||
|
this.personTypes,
|
||||||
this.companyName,
|
this.companyName,
|
||||||
this.paymentId,
|
this.paymentId,
|
||||||
this.nationalId,
|
this.nationalId,
|
||||||
|
|
@ -402,10 +433,12 @@ class PersonUpdateRequest {
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final Map<String, dynamic> json = {};
|
final Map<String, dynamic> json = {};
|
||||||
|
|
||||||
|
if (code != null) json['code'] = code;
|
||||||
if (aliasName != null) json['alias_name'] = aliasName;
|
if (aliasName != null) json['alias_name'] = aliasName;
|
||||||
if (firstName != null) json['first_name'] = firstName;
|
if (firstName != null) json['first_name'] = firstName;
|
||||||
if (lastName != null) json['last_name'] = lastName;
|
if (lastName != null) json['last_name'] = lastName;
|
||||||
if (personType != null) json['person_type'] = personType!.persianName;
|
if (personType != null) json['person_type'] = personType!.persianName;
|
||||||
|
if (personTypes != null) json['person_types'] = personTypes!.map((t) => t.persianName).toList();
|
||||||
if (companyName != null) json['company_name'] = companyName;
|
if (companyName != null) json['company_name'] = companyName;
|
||||||
if (paymentId != null) json['payment_id'] = paymentId;
|
if (paymentId != null) json['payment_id'] = paymentId;
|
||||||
if (nationalId != null) json['national_id'] = nationalId;
|
if (nationalId != null) json['national_id'] = nationalId;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class PersonsPage extends StatefulWidget {
|
||||||
|
|
||||||
class _PersonsPageState extends State<PersonsPage> {
|
class _PersonsPageState extends State<PersonsPage> {
|
||||||
final _personService = PersonService();
|
final _personService = PersonService();
|
||||||
|
final GlobalKey _personsTableKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -54,6 +55,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: DataTableWidget<Person>(
|
body: DataTableWidget<Person>(
|
||||||
|
key: _personsTableKey,
|
||||||
config: _buildDataTableConfig(t),
|
config: _buildDataTableConfig(t),
|
||||||
fromJson: Person.fromJson,
|
fromJson: Person.fromJson,
|
||||||
),
|
),
|
||||||
|
|
@ -64,7 +66,16 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
return DataTableConfig<Person>(
|
return DataTableConfig<Person>(
|
||||||
endpoint: '/api/v1/persons/businesses/${widget.businessId}/persons',
|
endpoint: '/api/v1/persons/businesses/${widget.businessId}/persons',
|
||||||
title: t.personsList,
|
title: t.personsList,
|
||||||
|
showRowNumbers: true,
|
||||||
|
enableRowSelection: true,
|
||||||
columns: [
|
columns: [
|
||||||
|
NumberColumn(
|
||||||
|
'code',
|
||||||
|
'کد شخص',
|
||||||
|
width: ColumnWidth.small,
|
||||||
|
formatter: (person) => (person.code?.toString() ?? '-'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
TextColumn(
|
TextColumn(
|
||||||
'alias_name',
|
'alias_name',
|
||||||
t.personAliasName,
|
t.personAliasName,
|
||||||
|
|
@ -87,7 +98,9 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
'person_type',
|
'person_type',
|
||||||
t.personType,
|
t.personType,
|
||||||
width: ColumnWidth.medium,
|
width: ColumnWidth.medium,
|
||||||
formatter: (person) => person.personType.persianName,
|
formatter: (person) => (person.personTypes.isNotEmpty
|
||||||
|
? person.personTypes.map((e) => e.persianName).join('، ')
|
||||||
|
: person.personType.persianName),
|
||||||
),
|
),
|
||||||
TextColumn(
|
TextColumn(
|
||||||
'company_name',
|
'company_name',
|
||||||
|
|
@ -108,10 +121,10 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
formatter: (person) => person.email ?? '-',
|
formatter: (person) => person.email ?? '-',
|
||||||
),
|
),
|
||||||
TextColumn(
|
TextColumn(
|
||||||
'is_active',
|
'national_id',
|
||||||
'وضعیت',
|
t.personNationalId,
|
||||||
width: ColumnWidth.small,
|
width: ColumnWidth.medium,
|
||||||
formatter: (person) => person.isActive ? 'فعال' : 'غیرفعال',
|
formatter: (person) => person.nationalId ?? '-',
|
||||||
),
|
),
|
||||||
DateColumn(
|
DateColumn(
|
||||||
'created_at',
|
'created_at',
|
||||||
|
|
@ -137,6 +150,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
searchFields: [
|
searchFields: [
|
||||||
|
'code',
|
||||||
'alias_name',
|
'alias_name',
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
|
|
@ -147,6 +161,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
],
|
],
|
||||||
filterFields: [
|
filterFields: [
|
||||||
'person_type',
|
'person_type',
|
||||||
|
'person_types',
|
||||||
'is_active',
|
'is_active',
|
||||||
'country',
|
'country',
|
||||||
'province',
|
'province',
|
||||||
|
|
@ -162,7 +177,12 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
builder: (context) => PersonFormDialog(
|
builder: (context) => PersonFormDialog(
|
||||||
businessId: widget.businessId,
|
businessId: widget.businessId,
|
||||||
onSuccess: () {
|
onSuccess: () {
|
||||||
// DataTableWidget will automatically refresh
|
final state = _personsTableKey.currentState;
|
||||||
|
try {
|
||||||
|
// Call public refresh() via dynamic to avoid private state typing
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
|
(state as dynamic)?.refresh();
|
||||||
|
} catch (_) {}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -175,7 +195,11 @@ class _PersonsPageState extends State<PersonsPage> {
|
||||||
businessId: widget.businessId,
|
businessId: widget.businessId,
|
||||||
person: person,
|
person: person,
|
||||||
onSuccess: () {
|
onSuccess: () {
|
||||||
// DataTableWidget will automatically refresh
|
final state = _personsTableKey.currentState;
|
||||||
|
try {
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
|
(state as dynamic)?.refresh();
|
||||||
|
} catch (_) {}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@ class _DataTableSearchDialogState extends State<DataTableSearchDialog> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller = TextEditingController(text: widget.searchValue);
|
_controller = TextEditingController(text: widget.searchValue);
|
||||||
_selectedType = widget.searchType;
|
_selectedType = widget.searchType;
|
||||||
|
// Enable/disable Apply button reactively on text changes
|
||||||
|
_controller.addListener(() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1232,12 +1232,38 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add data columns (use visible columns if column settings are enabled)
|
// Resolve action column (if defined in config)
|
||||||
|
ActionColumn? actionColumn;
|
||||||
|
for (final c in widget.config.columns) {
|
||||||
|
if (c is ActionColumn) {
|
||||||
|
actionColumn = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed action column immediately after selection and row number columns
|
||||||
|
if (actionColumn != null) {
|
||||||
|
columns.add(DataColumn2(
|
||||||
|
label: Text(
|
||||||
|
actionColumn.label,
|
||||||
|
style: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
size: ColumnSize.S,
|
||||||
|
fixedWidth: 80.0,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add data columns (use visible columns if column settings are enabled), excluding ActionColumn
|
||||||
final columnsToShow = widget.config.enableColumnSettings && _visibleColumns.isNotEmpty
|
final columnsToShow = widget.config.enableColumnSettings && _visibleColumns.isNotEmpty
|
||||||
? _visibleColumns
|
? _visibleColumns
|
||||||
: widget.config.columns;
|
: widget.config.columns;
|
||||||
|
final dataColumnsToShow = columnsToShow.where((c) => c is! ActionColumn).toList();
|
||||||
|
|
||||||
columns.addAll(columnsToShow.map((column) {
|
columns.addAll(dataColumnsToShow.map((column) {
|
||||||
return DataColumn2(
|
return DataColumn2(
|
||||||
label: _ColumnHeaderWithSearch(
|
label: _ColumnHeaderWithSearch(
|
||||||
text: column.label,
|
text: column.label,
|
||||||
|
|
@ -1295,7 +1321,22 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add data cells
|
// 3) Fixed action cell (immediately after selection and row number)
|
||||||
|
// Resolve action column once (same logic as header)
|
||||||
|
ActionColumn? actionColumn;
|
||||||
|
for (final c in widget.config.columns) {
|
||||||
|
if (c is ActionColumn) {
|
||||||
|
actionColumn = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actionColumn != null) {
|
||||||
|
cells.add(DataCell(
|
||||||
|
_buildActionButtons(item, actionColumn),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Add data cells
|
||||||
if (widget.config.customRowBuilder != null) {
|
if (widget.config.customRowBuilder != null) {
|
||||||
cells.add(DataCell(
|
cells.add(DataCell(
|
||||||
widget.config.customRowBuilder!(item) ?? const SizedBox.shrink(),
|
widget.config.customRowBuilder!(item) ?? const SizedBox.shrink(),
|
||||||
|
|
@ -1304,8 +1345,9 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
final columnsToShow = widget.config.enableColumnSettings && _visibleColumns.isNotEmpty
|
final columnsToShow = widget.config.enableColumnSettings && _visibleColumns.isNotEmpty
|
||||||
? _visibleColumns
|
? _visibleColumns
|
||||||
: widget.config.columns;
|
: widget.config.columns;
|
||||||
|
final dataColumnsToShow = columnsToShow.where((c) => c is! ActionColumn).toList();
|
||||||
|
|
||||||
cells.addAll(columnsToShow.map((column) {
|
cells.addAll(dataColumnsToShow.map((column) {
|
||||||
return DataCell(
|
return DataCell(
|
||||||
_buildCellContent(item, column, index),
|
_buildCellContent(item, column, index),
|
||||||
);
|
);
|
||||||
|
|
@ -1328,18 +1370,49 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCellContent(dynamic item, DataTableColumn column, int index) {
|
Widget _buildCellContent(dynamic item, DataTableColumn column, int index) {
|
||||||
final value = DataTableUtils.getCellValue(item, column.key);
|
// 1) Custom widget builder takes precedence
|
||||||
|
|
||||||
if (column is CustomColumn && column.builder != null) {
|
if (column is CustomColumn && column.builder != null) {
|
||||||
return column.builder!(item, index);
|
return column.builder!(item, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2) Action column
|
||||||
if (column is ActionColumn) {
|
if (column is ActionColumn) {
|
||||||
return _buildActionButtons(item, column);
|
return _buildActionButtons(item, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) If a formatter is provided on the column, call it with the full item
|
||||||
|
// This allows working with strongly-typed objects (not just Map)
|
||||||
|
if (column is TextColumn && column.formatter != null) {
|
||||||
|
final text = column.formatter!(item) ?? '';
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
textAlign: _getTextAlign(column),
|
||||||
|
maxLines: _getMaxLines(column),
|
||||||
|
overflow: _getOverflow(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (column is NumberColumn && column.formatter != null) {
|
||||||
|
final text = column.formatter!(item) ?? '';
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
textAlign: _getTextAlign(column),
|
||||||
|
maxLines: _getMaxLines(column),
|
||||||
|
overflow: _getOverflow(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (column is DateColumn && column.formatter != null) {
|
||||||
|
final text = column.formatter!(item) ?? '';
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
textAlign: _getTextAlign(column),
|
||||||
|
maxLines: _getMaxLines(column),
|
||||||
|
overflow: _getOverflow(column),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Fallback: get property value from Map items by key
|
||||||
|
final value = DataTableUtils.getCellValue(item, column.key);
|
||||||
final formattedValue = DataTableUtils.formatCellValue(value, column);
|
final formattedValue = DataTableUtils.formatCellValue(value, column);
|
||||||
|
|
||||||
return Text(
|
return Text(
|
||||||
formattedValue,
|
formattedValue,
|
||||||
textAlign: _getTextAlign(column),
|
textAlign: _getTextAlign(column),
|
||||||
|
|
@ -1351,24 +1424,35 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
||||||
Widget _buildActionButtons(dynamic item, ActionColumn column) {
|
Widget _buildActionButtons(dynamic item, ActionColumn column) {
|
||||||
if (column.actions.isEmpty) return const SizedBox.shrink();
|
if (column.actions.isEmpty) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Row(
|
return PopupMenuButton<int>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
tooltip: column.label,
|
||||||
children: column.actions.map((action) {
|
icon: const Icon(Icons.more_vert, size: 20),
|
||||||
return IconButton(
|
onSelected: (index) {
|
||||||
onPressed: action.enabled ? () => action.onTap(item) : null,
|
final action = column.actions[index];
|
||||||
icon: Icon(
|
if (action.enabled) action.onTap(item);
|
||||||
action.icon,
|
},
|
||||||
color: action.color,
|
itemBuilder: (context) {
|
||||||
size: 20,
|
return List.generate(column.actions.length, (index) {
|
||||||
),
|
final action = column.actions[index];
|
||||||
tooltip: action.label,
|
return PopupMenuItem<int>(
|
||||||
style: IconButton.styleFrom(
|
value: index,
|
||||||
foregroundColor: action.isDestructive
|
enabled: action.enabled,
|
||||||
? Theme.of(context).colorScheme.error
|
child: Row(
|
||||||
: action.color,
|
children: [
|
||||||
),
|
Icon(
|
||||||
);
|
action.icon,
|
||||||
}).toList(),
|
color: action.isDestructive
|
||||||
|
? Theme.of(context).colorScheme.error
|
||||||
|
: (action.color ?? Theme.of(context).iconTheme.color),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(action.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
final _personService = PersonService();
|
final _personService = PersonService();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
// Code (unique) controls
|
||||||
|
final _codeController = TextEditingController();
|
||||||
|
bool _autoGenerateCode = true;
|
||||||
|
|
||||||
// Controllers for basic info
|
// Controllers for basic info
|
||||||
final _aliasNameController = TextEditingController();
|
final _aliasNameController = TextEditingController();
|
||||||
final _firstNameController = TextEditingController();
|
final _firstNameController = TextEditingController();
|
||||||
|
|
@ -48,7 +52,8 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _websiteController = TextEditingController();
|
final _websiteController = TextEditingController();
|
||||||
|
|
||||||
PersonType _selectedPersonType = PersonType.customer;
|
PersonType _selectedPersonType = PersonType.customer; // legacy single select (for compatibility)
|
||||||
|
final Set<PersonType> _selectedPersonTypes = <PersonType>{};
|
||||||
bool _isActive = true;
|
bool _isActive = true;
|
||||||
|
|
||||||
// Bank accounts
|
// Bank accounts
|
||||||
|
|
@ -63,6 +68,10 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
void _initializeForm() {
|
void _initializeForm() {
|
||||||
if (widget.person != null) {
|
if (widget.person != null) {
|
||||||
final person = widget.person!;
|
final person = widget.person!;
|
||||||
|
if (person.code != null) {
|
||||||
|
_codeController.text = person.code!.toString();
|
||||||
|
_autoGenerateCode = false;
|
||||||
|
}
|
||||||
_aliasNameController.text = person.aliasName;
|
_aliasNameController.text = person.aliasName;
|
||||||
_firstNameController.text = person.firstName ?? '';
|
_firstNameController.text = person.firstName ?? '';
|
||||||
_lastNameController.text = person.lastName ?? '';
|
_lastNameController.text = person.lastName ?? '';
|
||||||
|
|
@ -82,6 +91,9 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
_emailController.text = person.email ?? '';
|
_emailController.text = person.email ?? '';
|
||||||
_websiteController.text = person.website ?? '';
|
_websiteController.text = person.website ?? '';
|
||||||
_selectedPersonType = person.personType;
|
_selectedPersonType = person.personType;
|
||||||
|
_selectedPersonTypes
|
||||||
|
..clear()
|
||||||
|
..addAll(person.personTypes.isNotEmpty ? person.personTypes : [person.personType]);
|
||||||
_isActive = person.isActive;
|
_isActive = person.isActive;
|
||||||
_bankAccounts = List.from(person.bankAccounts);
|
_bankAccounts = List.from(person.bankAccounts);
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +101,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_codeController.dispose();
|
||||||
_aliasNameController.dispose();
|
_aliasNameController.dispose();
|
||||||
_firstNameController.dispose();
|
_firstNameController.dispose();
|
||||||
_lastNameController.dispose();
|
_lastNameController.dispose();
|
||||||
|
|
@ -121,10 +134,13 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
if (widget.person == null) {
|
if (widget.person == null) {
|
||||||
// Create new person
|
// Create new person
|
||||||
final personData = PersonCreateRequest(
|
final personData = PersonCreateRequest(
|
||||||
|
code: _autoGenerateCode
|
||||||
|
? null
|
||||||
|
: (int.tryParse(_codeController.text.trim()) ?? null),
|
||||||
aliasName: _aliasNameController.text.trim(),
|
aliasName: _aliasNameController.text.trim(),
|
||||||
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
||||||
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||||
personType: _selectedPersonType,
|
personTypes: _selectedPersonTypes.toList(),
|
||||||
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
||||||
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
||||||
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
||||||
|
|
@ -150,10 +166,12 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
} else {
|
} else {
|
||||||
// Update existing person
|
// Update existing person
|
||||||
final personData = PersonUpdateRequest(
|
final personData = PersonUpdateRequest(
|
||||||
|
code: (int.tryParse(_codeController.text.trim()) ?? null),
|
||||||
aliasName: _aliasNameController.text.trim(),
|
aliasName: _aliasNameController.text.trim(),
|
||||||
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
||||||
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||||
personType: _selectedPersonType,
|
personType: null,
|
||||||
|
personTypes: _selectedPersonTypes.isNotEmpty ? _selectedPersonTypes.toList() : null,
|
||||||
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
||||||
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
||||||
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
nationalId: _nationalIdController.text.trim().isEmpty ? null : _nationalIdController.text.trim(),
|
||||||
|
|
@ -266,53 +284,54 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
const Divider(),
|
const Divider(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Form
|
// Form with tabs
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: SingleChildScrollView(
|
child: DefaultTabController(
|
||||||
|
length: 4,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
// Basic Information
|
TabBar(
|
||||||
_buildSectionHeader(t.personBasicInfo),
|
isScrollable: true,
|
||||||
const SizedBox(height: 16),
|
tabs: [
|
||||||
_buildBasicInfoFields(t),
|
Tab(text: t.personBasicInfo),
|
||||||
const SizedBox(height: 24),
|
Tab(text: t.personEconomicInfo),
|
||||||
|
Tab(text: t.personContactInfo),
|
||||||
// Economic Information
|
Tab(text: t.personBankInfo),
|
||||||
_buildSectionHeader(t.personEconomicInfo),
|
],
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
_buildEconomicInfoFields(t),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 24),
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
// Contact Information
|
children: [
|
||||||
_buildSectionHeader(t.personContactInfo),
|
SingleChildScrollView(
|
||||||
const SizedBox(height: 16),
|
child: Padding(
|
||||||
_buildContactInfoFields(t),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
const SizedBox(height: 24),
|
child: _buildBasicInfoFields(t),
|
||||||
|
),
|
||||||
// Bank Accounts
|
),
|
||||||
_buildSectionHeader(t.personBankInfo),
|
SingleChildScrollView(
|
||||||
const SizedBox(height: 16),
|
child: Padding(
|
||||||
_buildBankAccountsSection(t),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
const SizedBox(height: 24),
|
child: _buildEconomicInfoFields(t),
|
||||||
|
),
|
||||||
// Status (only for editing)
|
),
|
||||||
if (isEditing) ...[
|
SingleChildScrollView(
|
||||||
_buildSectionHeader('وضعیت'),
|
child: Padding(
|
||||||
const SizedBox(height: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
SwitchListTile(
|
child: _buildContactInfoFields(t),
|
||||||
title: Text('فعال'),
|
),
|
||||||
value: _isActive,
|
),
|
||||||
onChanged: (value) {
|
SingleChildScrollView(
|
||||||
setState(() {
|
child: Padding(
|
||||||
_isActive = value;
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
});
|
child: _buildBankAccountsSection(t),
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -361,6 +380,61 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
Widget _buildBasicInfoFields(AppLocalizations t) {
|
Widget _buildBasicInfoFields(AppLocalizations t) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _codeController,
|
||||||
|
readOnly: _autoGenerateCode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'کد شخص (اختیاری)',
|
||||||
|
hintText: 'کد یکتا (عددی)',
|
||||||
|
suffixIcon: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
child: ToggleButtons(
|
||||||
|
isSelected: [_autoGenerateCode, !_autoGenerateCode],
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
constraints: const BoxConstraints(minHeight: 32, minWidth: 64),
|
||||||
|
onPressed: (index) {
|
||||||
|
setState(() {
|
||||||
|
_autoGenerateCode = (index == 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
children: const [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Text('اتوماتیک'),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Text('دستی'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (!_autoGenerateCode) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'کد شخص الزامی است';
|
||||||
|
}
|
||||||
|
if (int.tryParse(value.trim()) == null) {
|
||||||
|
return 'کد باید عددی باشد';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -379,36 +453,10 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(child: _buildPersonTypesMultiSelect(t)),
|
||||||
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),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -432,7 +480,7 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -460,6 +508,39 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPersonTypesMultiSelect(AppLocalizations t) {
|
||||||
|
final types = PersonType.values;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Text(t.personType),
|
||||||
|
),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: types.map((type) {
|
||||||
|
final selected = _selectedPersonTypes.contains(type);
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(type.persianName),
|
||||||
|
selected: selected,
|
||||||
|
onSelected: (v) {
|
||||||
|
setState(() {
|
||||||
|
if (v) {
|
||||||
|
_selectedPersonTypes.add(type);
|
||||||
|
} else {
|
||||||
|
_selectedPersonTypes.remove(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEconomicInfoFields(AppLocalizations t) {
|
Widget _buildEconomicInfoFields(AppLocalizations t) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue