progress in invoice
This commit is contained in:
parent
6c1606fe24
commit
0edff7d020
228
hesabixAPI/adapters/api/v1/customers.py
Normal file
228
hesabixAPI/adapters/api/v1/customers.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
from fastapi import APIRouter, Depends, Request, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
|
||||
from adapters.db.session import get_db
|
||||
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_access_dep
|
||||
from app.services.person_service import search_persons, count_persons, get_person_by_id
|
||||
|
||||
router = APIRouter(prefix="/customers", tags=["customers"])
|
||||
|
||||
|
||||
class CustomerSearchRequest(BaseModel):
|
||||
business_id: int
|
||||
page: int = 1
|
||||
limit: int = 20
|
||||
search: Optional[str] = None
|
||||
|
||||
|
||||
class CustomerResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
code: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
is_active: bool = True
|
||||
created_at: Optional[str] = None
|
||||
|
||||
|
||||
class CustomerSearchResponse(BaseModel):
|
||||
customers: List[CustomerResponse]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
has_more: bool
|
||||
|
||||
|
||||
@router.post("/search",
|
||||
summary="جستوجوی مشتریها",
|
||||
description="جستوجو در لیست مشتریها (اشخاص) با قابلیت فیلتر و صفحهبندی",
|
||||
response_model=CustomerSearchResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "لیست مشتریها با موفقیت دریافت شد",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"customers": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "احمد احمدی",
|
||||
"code": "CUST001",
|
||||
"phone": "09123456789",
|
||||
"email": "ahmad@example.com",
|
||||
"address": "تهران، خیابان ولیعصر",
|
||||
"is_active": True,
|
||||
"created_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"has_more": False
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
401: {
|
||||
"description": "کاربر احراز هویت نشده است"
|
||||
},
|
||||
403: {
|
||||
"description": "دسترسی غیرمجاز - نیاز به دسترسی به کسب و کار"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def search_customers(
|
||||
request: Request,
|
||||
search_request: CustomerSearchRequest,
|
||||
ctx: AuthContext = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
_: None = Depends(require_business_access_dep)
|
||||
):
|
||||
"""جستوجو در لیست مشتریها"""
|
||||
|
||||
# بررسی دسترسی به بخش اشخاص (یا join permission)
|
||||
# در اینجا میتوانید منطق بررسی دسترسی join را پیادهسازی کنید
|
||||
# برای مثال: اگر کاربر دسترسی مستقیم به اشخاص ندارد، اما دسترسی join دارد
|
||||
|
||||
# جستوجو در اشخاص
|
||||
persons = search_persons(
|
||||
db=db,
|
||||
business_id=search_request.business_id,
|
||||
search_query=search_request.search,
|
||||
page=search_request.page,
|
||||
limit=search_request.limit
|
||||
)
|
||||
|
||||
# تبدیل به فرمت مشتری
|
||||
customers = []
|
||||
for person in persons:
|
||||
# ساخت نام کامل
|
||||
name_parts = []
|
||||
if person.alias_name:
|
||||
name_parts.append(person.alias_name)
|
||||
if person.first_name:
|
||||
name_parts.append(person.first_name)
|
||||
if person.last_name:
|
||||
name_parts.append(person.last_name)
|
||||
full_name = " ".join(name_parts) if name_parts else person.alias_name or "نامشخص"
|
||||
|
||||
customer = CustomerResponse(
|
||||
id=person.id,
|
||||
name=full_name,
|
||||
code=str(person.code) if person.code else None,
|
||||
phone=person.phone or person.mobile,
|
||||
email=person.email,
|
||||
address=person.address,
|
||||
is_active=True, # اشخاص همیشه فعال در نظر گرفته میشوند
|
||||
created_at=person.created_at.isoformat() if person.created_at else None
|
||||
)
|
||||
customers.append(customer)
|
||||
|
||||
# محاسبه تعداد کل
|
||||
total_count = count_persons(
|
||||
db=db,
|
||||
business_id=search_request.business_id,
|
||||
search_query=search_request.search
|
||||
)
|
||||
|
||||
has_more = len(customers) == search_request.limit
|
||||
|
||||
return CustomerSearchResponse(
|
||||
customers=customers,
|
||||
total=total_count,
|
||||
page=search_request.page,
|
||||
limit=search_request.limit,
|
||||
has_more=has_more
|
||||
)
|
||||
|
||||
|
||||
@router.get("/detail/{customer_id}",
|
||||
summary="دریافت اطلاعات مشتری",
|
||||
description="دریافت اطلاعات کامل یک مشتری بر اساس شناسه",
|
||||
response_model=CustomerResponse,
|
||||
responses={
|
||||
200: {
|
||||
"description": "اطلاعات مشتری با موفقیت دریافت شد"
|
||||
},
|
||||
401: {
|
||||
"description": "کاربر احراز هویت نشده است"
|
||||
},
|
||||
403: {
|
||||
"description": "دسترسی غیرمجاز"
|
||||
},
|
||||
404: {
|
||||
"description": "مشتری یافت نشد"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def get_customer(
|
||||
customer_id: int,
|
||||
business_id: int,
|
||||
request: Request,
|
||||
ctx: AuthContext = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
_: None = Depends(require_business_access_dep)
|
||||
):
|
||||
"""دریافت اطلاعات یک مشتری"""
|
||||
|
||||
# دریافت اطلاعات شخص
|
||||
person_data = get_person_by_id(db, customer_id, business_id)
|
||||
|
||||
if not person_data:
|
||||
raise HTTPException(status_code=404, detail="مشتری یافت نشد")
|
||||
|
||||
# ساخت نام کامل
|
||||
name_parts = []
|
||||
if person_data.get('alias_name'):
|
||||
name_parts.append(person_data['alias_name'])
|
||||
if person_data.get('first_name'):
|
||||
name_parts.append(person_data['first_name'])
|
||||
if person_data.get('last_name'):
|
||||
name_parts.append(person_data['last_name'])
|
||||
full_name = " ".join(name_parts) if name_parts else person_data.get('alias_name', 'نامشخص')
|
||||
|
||||
customer = CustomerResponse(
|
||||
id=person_data['id'],
|
||||
name=full_name,
|
||||
code=str(person_data['code']) if person_data.get('code') else None,
|
||||
phone=person_data.get('phone') or person_data.get('mobile'),
|
||||
email=person_data.get('email'),
|
||||
address=person_data.get('address'),
|
||||
is_active=True, # اشخاص همیشه فعال در نظر گرفته میشوند
|
||||
created_at=person_data.get('created_at')
|
||||
)
|
||||
|
||||
return customer
|
||||
|
||||
|
||||
@router.get("/check-access",
|
||||
summary="بررسی دسترسی به مشتریها",
|
||||
description="بررسی دسترسی کاربر به بخش مشتریها",
|
||||
responses={
|
||||
200: {
|
||||
"description": "دسترسی مجاز است"
|
||||
},
|
||||
401: {
|
||||
"description": "کاربر احراز هویت نشده است"
|
||||
},
|
||||
403: {
|
||||
"description": "دسترسی غیرمجاز"
|
||||
}
|
||||
}
|
||||
)
|
||||
async def check_customer_access(
|
||||
business_id: int,
|
||||
ctx: AuthContext = Depends(get_current_user),
|
||||
_: None = Depends(require_business_access_dep)
|
||||
):
|
||||
"""بررسی دسترسی به بخش مشتریها"""
|
||||
|
||||
# در اینجا میتوانید منطق بررسی دسترسی join را پیادهسازی کنید
|
||||
# برای مثال: بررسی اینکه آیا کاربر دسترسی به اشخاص یا join permission دارد
|
||||
|
||||
return {"access": True, "message": "دسترسی مجاز است"}
|
||||
|
|
@ -34,12 +34,7 @@ class Person(Base):
|
|||
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, values_callable=lambda obj: [e.value for e in obj], name="person_type_enum"),
|
||||
nullable=False,
|
||||
comment="نوع شخص"
|
||||
)
|
||||
person_types: Mapped[str | None] = mapped_column(Text, nullable=True, comment="لیست انواع شخص به صورت JSON")
|
||||
person_types: Mapped[str] = mapped_column(Text, nullable=False, comment="لیست انواع شخص به صورت JSON")
|
||||
company_name: Mapped[str | None] = mapped_column(String(255), nullable=True, comment="نام شرکت")
|
||||
payment_id: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="شناسه پرداخت")
|
||||
# سهام
|
||||
|
|
|
|||
|
|
@ -212,3 +212,10 @@ def require_business_management_dep(auth_context: AuthContext = Depends(get_curr
|
|||
"""FastAPI dependency برای بررسی مجوز مدیریت کسب و کارها."""
|
||||
if not auth_context.has_app_permission("business_management"):
|
||||
raise ApiError("FORBIDDEN", "Missing app permission: business_management", http_status=403)
|
||||
|
||||
|
||||
def require_business_access_dep(auth_context: AuthContext = Depends(get_current_user)) -> None:
|
||||
"""FastAPI dependency برای بررسی دسترسی به کسب و کار."""
|
||||
# در اینجا میتوانید منطق بررسی دسترسی به کسب و کار را پیادهسازی کنید
|
||||
# برای مثال: بررسی اینکه آیا کاربر دسترسی به کسب و کار دارد
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from adapters.api.v1.product_attributes import router as product_attributes_rout
|
|||
from adapters.api.v1.products import router as products_router
|
||||
from adapters.api.v1.price_lists import router as price_lists_router
|
||||
from adapters.api.v1.persons import router as persons_router
|
||||
from adapters.api.v1.customers import router as customers_router
|
||||
from adapters.api.v1.bank_accounts import router as bank_accounts_router
|
||||
from adapters.api.v1.cash_registers import router as cash_registers_router
|
||||
from adapters.api.v1.petty_cash import router as petty_cash_router
|
||||
|
|
@ -295,6 +296,7 @@ def create_app() -> FastAPI:
|
|||
application.include_router(products_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(price_lists_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(persons_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(customers_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(bank_accounts_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(cash_registers_router, prefix=settings.api_v1_prefix)
|
||||
application.include_router(petty_cash_router, prefix=settings.api_v1_prefix)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ def create_person(db: Session, business_id: int, person_data: PersonCreateReques
|
|||
first_name=person_data.first_name,
|
||||
last_name=person_data.last_name,
|
||||
# ذخیره مقدار Enum با مقدار فارسی (values_callable در مدل مقادیر فارسی را مینویسد)
|
||||
person_type=(mapped_single_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,
|
||||
payment_id=person_data.payment_id,
|
||||
|
|
@ -198,14 +197,6 @@ def get_persons_by_business(
|
|||
query = query.filter(Person.code.in_(value))
|
||||
continue
|
||||
|
||||
# نوع شخص تکانتخابی
|
||||
if field == 'person_type':
|
||||
if operator == '=':
|
||||
query = query.filter(Person.person_type == value)
|
||||
elif operator == 'in' and isinstance(value, list):
|
||||
query = query.filter(Person.person_type.in_(value))
|
||||
continue
|
||||
|
||||
# انواع شخص چندانتخابی (رشته JSON)
|
||||
if field == 'person_types':
|
||||
if operator == '=' and isinstance(value, str):
|
||||
|
|
@ -295,8 +286,7 @@ def get_persons_by_business(
|
|||
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())
|
||||
# person_type sorting removed - use person_types instead
|
||||
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':
|
||||
|
|
@ -367,23 +357,7 @@ def update_person(
|
|||
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:
|
||||
# مقدار Enum را با مقدار فارسی ست میکنیم
|
||||
try:
|
||||
person.person_type = PersonType(types_list[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# مدیریت person_type تکی از اسکیما
|
||||
if 'person_type' in update_data and update_data['person_type'] is not None:
|
||||
single_type = update_data['person_type']
|
||||
# نگاشت به Enum (مقدار فارسی)
|
||||
try:
|
||||
person.person_type = PersonType(getattr(single_type, 'value', str(single_type)))
|
||||
except Exception:
|
||||
pass
|
||||
# پس از ست کردن مستقیم، از دیکشنری حذف شود تا در حلقه عمومی دوباره اعمال نشود
|
||||
update_data.pop('person_type', None)
|
||||
# person_type handling removed - only person_types is used now
|
||||
|
||||
# اگر شخص سهامدار شد، share_count معتبر باشد
|
||||
resulting_types: List[str] = []
|
||||
|
|
@ -394,7 +368,7 @@ def update_person(
|
|||
resulting_types = [str(x) for x in tmp]
|
||||
except Exception:
|
||||
resulting_types = []
|
||||
if (person.person_type == 'سهامدار') or ('سهامدار' in resulting_types):
|
||||
if 'سهامدار' in resulting_types:
|
||||
sc_val2 = update_data.get('share_count', person.share_count)
|
||||
if sc_val2 is None or (isinstance(sc_val2, int) and sc_val2 <= 0):
|
||||
raise ApiError("INVALID_SHARE_COUNT", "برای سهامدار، تعداد سهام الزامی و باید بزرگتر از صفر باشد", http_status=400)
|
||||
|
|
@ -442,7 +416,7 @@ def get_person_summary(db: Session, business_id: int) -> Dict[str, Any]:
|
|||
by_type = {}
|
||||
for person_type in PersonType:
|
||||
count = db.query(Person).filter(
|
||||
and_(Person.business_id == business_id, Person.person_type == person_type)
|
||||
and_(Person.business_id == business_id, Person.person_types.ilike(f'%"{person_type.value}"%'))
|
||||
).count()
|
||||
by_type[person_type.value] = count
|
||||
|
||||
|
|
@ -473,7 +447,6 @@ def _person_to_dict(person: Person) -> Dict[str, Any]:
|
|||
'alias_name': person.alias_name,
|
||||
'first_name': person.first_name,
|
||||
'last_name': person.last_name,
|
||||
'person_type': person.person_type.value,
|
||||
'person_types': types_list,
|
||||
'company_name': person.company_name,
|
||||
'payment_id': person.payment_id,
|
||||
|
|
@ -514,3 +487,51 @@ def _person_to_dict(person: Person) -> Dict[str, Any]:
|
|||
for ba in person.bank_accounts
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def search_persons(db: Session, business_id: int, search_query: Optional[str] = None,
|
||||
page: int = 1, limit: int = 20) -> List[Person]:
|
||||
"""جستوجو در اشخاص"""
|
||||
query = db.query(Person).filter(Person.business_id == business_id)
|
||||
|
||||
if search_query:
|
||||
# جستوجو در نام، نام خانوادگی، نام مستعار، کد، تلفن و ایمیل
|
||||
search_filter = or_(
|
||||
Person.alias_name.ilike(f"%{search_query}%"),
|
||||
Person.first_name.ilike(f"%{search_query}%"),
|
||||
Person.last_name.ilike(f"%{search_query}%"),
|
||||
Person.company_name.ilike(f"%{search_query}%"),
|
||||
Person.phone.ilike(f"%{search_query}%"),
|
||||
Person.mobile.ilike(f"%{search_query}%"),
|
||||
Person.email.ilike(f"%{search_query}%"),
|
||||
Person.code == int(search_query) if search_query.isdigit() else False
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
# مرتبسازی بر اساس نام مستعار
|
||||
query = query.order_by(Person.alias_name)
|
||||
|
||||
# صفحهبندی
|
||||
offset = (page - 1) * limit
|
||||
return query.offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
def count_persons(db: Session, business_id: int, search_query: Optional[str] = None) -> int:
|
||||
"""شمارش تعداد اشخاص"""
|
||||
query = db.query(Person).filter(Person.business_id == business_id)
|
||||
|
||||
if search_query:
|
||||
# جستوجو در نام، نام خانوادگی، نام مستعار، کد، تلفن و ایمیل
|
||||
search_filter = or_(
|
||||
Person.alias_name.ilike(f"%{search_query}%"),
|
||||
Person.first_name.ilike(f"%{search_query}%"),
|
||||
Person.last_name.ilike(f"%{search_query}%"),
|
||||
Person.company_name.ilike(f"%{search_query}%"),
|
||||
Person.phone.ilike(f"%{search_query}%"),
|
||||
Person.mobile.ilike(f"%{search_query}%"),
|
||||
Person.email.ilike(f"%{search_query}%"),
|
||||
Person.code == int(search_query) if search_query.isdigit() else False
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
return query.count()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ adapters/api/v1/businesses.py
|
|||
adapters/api/v1/cash_registers.py
|
||||
adapters/api/v1/categories.py
|
||||
adapters/api/v1/currencies.py
|
||||
adapters/api/v1/customers.py
|
||||
adapters/api/v1/health.py
|
||||
adapters/api/v1/persons.py
|
||||
adapters/api/v1/petty_cash.py
|
||||
|
|
@ -177,6 +178,7 @@ migrations/versions/4b2ea782bcb3_merge_heads.py
|
|||
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||
migrations/versions/9f9786ae7191_create_tax_units_table.py
|
||||
migrations/versions/a1443c153b47_merge_heads.py
|
||||
migrations/versions/c302bc2f2cb8_remove_person_type_column.py
|
||||
migrations/versions/caf3f4ef4b76_add_tax_units_table.py
|
||||
migrations/versions/d3e84892c1c2_sync_person_type_enum_values_callable_.py
|
||||
migrations/versions/f876bfa36805_merge_multiple_heads.py
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
"""remove_person_type_column
|
||||
|
||||
Revision ID: c302bc2f2cb8
|
||||
Revises: 1f0abcdd7300
|
||||
Create Date: 2025-10-04 19:04:30.866110
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c302bc2f2cb8'
|
||||
down_revision = '1f0abcdd7300'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# حذف ستون person_type از جدول persons
|
||||
op.drop_column('persons', 'person_type')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# بازگردانی ستون person_type
|
||||
op.add_column('persons',
|
||||
sa.Column('person_type',
|
||||
sa.Enum('مشتری', 'بازاریاب', 'کارمند', 'تامینکننده', 'همکار', 'فروشنده', 'سهامدار', name='person_type_enum'),
|
||||
nullable=False,
|
||||
comment='نوع شخص'
|
||||
)
|
||||
)
|
||||
|
|
@ -652,6 +652,7 @@ class _MyAppState extends State<MyApp> {
|
|||
child: NewInvoicePage(
|
||||
businessId: businessId,
|
||||
authStore: _authStore!,
|
||||
calendarController: _calendarController!,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
61
hesabixUI/hesabix_ui/lib/models/customer_model.dart
Normal file
61
hesabixUI/hesabix_ui/lib/models/customer_model.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
class Customer {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? code;
|
||||
final String? phone;
|
||||
final String? email;
|
||||
final String? address;
|
||||
final bool isActive;
|
||||
final DateTime? createdAt;
|
||||
|
||||
const Customer({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.code,
|
||||
this.phone,
|
||||
this.email,
|
||||
this.address,
|
||||
this.isActive = true,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory Customer.fromJson(Map<String, dynamic> json) {
|
||||
return Customer(
|
||||
id: json['id'] as int,
|
||||
name: json['name'] as String,
|
||||
code: json['code'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
email: json['email'] as String?,
|
||||
address: json['address'] as String?,
|
||||
isActive: json['is_active'] ?? true,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.tryParse(json['created_at'].toString())
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'code': code,
|
||||
'phone': phone,
|
||||
'email': email,
|
||||
'address': address,
|
||||
'is_active': isActive,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is Customer && other.id == id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
25
hesabixUI/hesabix_ui/lib/models/invoice_type_model.dart
Normal file
25
hesabixUI/hesabix_ui/lib/models/invoice_type_model.dart
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
enum InvoiceType {
|
||||
sales('sales', 'فروش'),
|
||||
salesReturn('sales_return', 'برگشت از فروش'),
|
||||
purchase('purchase', 'خرید'),
|
||||
purchaseReturn('purchase_return', 'برگشت از خرید'),
|
||||
waste('waste', 'ضایعات'),
|
||||
directConsumption('direct_consumption', 'مصرف مستقیم'),
|
||||
production('production', 'تولید');
|
||||
|
||||
const InvoiceType(this.value, this.label);
|
||||
|
||||
final String value;
|
||||
final String label;
|
||||
|
||||
static InvoiceType? fromValue(String value) {
|
||||
for (final type in InvoiceType.values) {
|
||||
if (type.value == value) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<InvoiceType> get allTypes => InvoiceType.values;
|
||||
}
|
||||
|
|
@ -102,7 +102,6 @@ class Person {
|
|||
final String aliasName;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final PersonType personType;
|
||||
final List<PersonType> personTypes;
|
||||
final String? companyName;
|
||||
final String? paymentId;
|
||||
|
|
@ -140,8 +139,7 @@ class Person {
|
|||
required this.aliasName,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
required this.personType,
|
||||
this.personTypes = const [],
|
||||
required this.personTypes,
|
||||
this.companyName,
|
||||
this.paymentId,
|
||||
this.nationalId,
|
||||
|
|
@ -176,9 +174,6 @@ class Person {
|
|||
?.map((e) => PersonType.fromString(e.toString()))
|
||||
.toList() ??
|
||||
[];
|
||||
final PersonType primaryType = types.isNotEmpty
|
||||
? types.first
|
||||
: PersonType.fromString(json['person_type']);
|
||||
return Person(
|
||||
id: json['id'],
|
||||
businessId: json['business_id'],
|
||||
|
|
@ -186,7 +181,6 @@ class Person {
|
|||
aliasName: json['alias_name'],
|
||||
firstName: json['first_name'],
|
||||
lastName: json['last_name'],
|
||||
personType: primaryType,
|
||||
personTypes: types,
|
||||
companyName: json['company_name'],
|
||||
paymentId: json['payment_id'],
|
||||
|
|
@ -228,7 +222,6 @@ class Person {
|
|||
'alias_name': aliasName,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'person_type': personType.persianName,
|
||||
'person_types': personTypes.map((t) => t.persianName).toList(),
|
||||
'company_name': companyName,
|
||||
'payment_id': paymentId,
|
||||
|
|
@ -266,7 +259,7 @@ class Person {
|
|||
String? aliasName,
|
||||
String? firstName,
|
||||
String? lastName,
|
||||
PersonType? personType,
|
||||
List<PersonType>? personTypes,
|
||||
String? companyName,
|
||||
String? paymentId,
|
||||
String? nationalId,
|
||||
|
|
@ -293,7 +286,7 @@ class Person {
|
|||
aliasName: aliasName ?? this.aliasName,
|
||||
firstName: firstName ?? this.firstName,
|
||||
lastName: lastName ?? this.lastName,
|
||||
personType: personType ?? this.personType,
|
||||
personTypes: personTypes ?? this.personTypes,
|
||||
companyName: companyName ?? this.companyName,
|
||||
paymentId: paymentId ?? this.paymentId,
|
||||
nationalId: nationalId ?? this.nationalId,
|
||||
|
|
@ -444,7 +437,6 @@ class PersonUpdateRequest {
|
|||
final String? aliasName;
|
||||
final String? firstName;
|
||||
final String? lastName;
|
||||
final PersonType? personType;
|
||||
final List<PersonType>? personTypes;
|
||||
final String? companyName;
|
||||
final String? paymentId;
|
||||
|
|
@ -476,7 +468,6 @@ class PersonUpdateRequest {
|
|||
this.aliasName,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.personType,
|
||||
this.personTypes,
|
||||
this.companyName,
|
||||
this.paymentId,
|
||||
|
|
@ -511,7 +502,6 @@ class PersonUpdateRequest {
|
|||
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 (personTypes != null) json['person_types'] = personTypes!.map((t) => t.persianName).toList();
|
||||
if (companyName != null) json['company_name'] = companyName;
|
||||
if (paymentId != null) json['payment_id'] = paymentId;
|
||||
|
|
|
|||
|
|
@ -948,6 +948,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
showAddBankAccountDialog();
|
||||
} else if (item.label == t.cashBox) {
|
||||
showAddCashBoxDialog();
|
||||
} else if (item.label == t.invoice) {
|
||||
// Navigate to add invoice
|
||||
context.go('/business/${widget.businessId}/invoice/new');
|
||||
}
|
||||
// سایر مسیرهای افزودن در آینده متصل میشوند
|
||||
},
|
||||
|
|
@ -1044,6 +1047,9 @@ class _BusinessShellState extends State<BusinessShell> {
|
|||
// در حال حاضر فقط اشخاص پشتیبانی میشود
|
||||
if (item.label == t.people) {
|
||||
showAddPersonDialog();
|
||||
} else if (item.label == t.invoice) {
|
||||
// Navigate to add invoice
|
||||
context.go('/business/${widget.businessId}/invoice/new');
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
|
|
|
|||
|
|
@ -34,20 +34,20 @@ class _InvoicePageState extends State<InvoicePage> {
|
|||
Icon(
|
||||
Icons.receipt,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.invoice,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'صفحه فاکتور در حال توسعه است',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,23 +1,68 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
import '../../core/calendar_controller.dart';
|
||||
import '../../widgets/permission/access_denied_page.dart';
|
||||
import '../../widgets/invoice/invoice_type_combobox.dart';
|
||||
import '../../widgets/invoice/code_field_widget.dart';
|
||||
import '../../widgets/invoice/customer_combobox_widget.dart';
|
||||
import '../../widgets/invoice/seller_picker_widget.dart';
|
||||
import '../../widgets/invoice/commission_percentage_field.dart';
|
||||
import '../../widgets/invoice/commission_type_selector.dart';
|
||||
import '../../widgets/invoice/commission_amount_field.dart';
|
||||
import '../../widgets/date_input_field.dart';
|
||||
import '../../widgets/banking/currency_picker_widget.dart';
|
||||
import '../../core/date_utils.dart';
|
||||
import '../../models/invoice_type_model.dart';
|
||||
import '../../models/customer_model.dart';
|
||||
import '../../models/person_model.dart';
|
||||
|
||||
class NewInvoicePage extends StatefulWidget {
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
final CalendarController calendarController;
|
||||
|
||||
const NewInvoicePage({
|
||||
super.key,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
required this.calendarController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NewInvoicePage> createState() => _NewInvoicePageState();
|
||||
}
|
||||
|
||||
class _NewInvoicePageState extends State<NewInvoicePage> {
|
||||
class _NewInvoicePageState extends State<NewInvoicePage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
InvoiceType? _selectedInvoiceType;
|
||||
bool _isDraft = false;
|
||||
String? _invoiceNumber;
|
||||
bool _autoGenerateInvoiceNumber = true;
|
||||
Customer? _selectedCustomer;
|
||||
Person? _selectedSeller;
|
||||
double? _commissionPercentage;
|
||||
double? _commissionAmount;
|
||||
CommissionType? _commissionType;
|
||||
DateTime? _invoiceDate;
|
||||
DateTime? _dueDate;
|
||||
int? _selectedCurrencyId;
|
||||
String? _invoiceTitle;
|
||||
String? _invoiceReference;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
|
|
@ -29,53 +74,830 @@ class _NewInvoicePageState extends State<NewInvoicePage> {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.addInvoice),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.add_circle_outline,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
|
||||
toolbarHeight: 56,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.info_outline),
|
||||
text: 'اطلاعات فاکتور',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.addInvoice,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
text: 'کالاها و خدمات',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'فرم ایجاد فاکتور جدید در حال توسعه است',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.receipt_long_outlined),
|
||||
text: 'تراکنشها',
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: پیادهسازی منطق ایجاد فاکتور
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('فرم ایجاد فاکتور به زودی اضافه خواهد شد'),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(t.addInvoice),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
text: 'تنظیمات',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// تب اطلاعات فاکتور
|
||||
_buildInvoiceInfoTab(),
|
||||
// تب کالاها و خدمات
|
||||
_buildProductsTab(),
|
||||
// تب تراکنشها
|
||||
_buildTransactionsTab(),
|
||||
// تب تنظیمات
|
||||
_buildSettingsTab(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInvoiceInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// فیلدهای اصلی - responsive layout
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// اگر عرض صفحه کمتر از 768 پیکسل باشد، تک ستونه
|
||||
if (constraints.maxWidth < 768) {
|
||||
return Column(
|
||||
children: [
|
||||
// نوع فاکتور
|
||||
InvoiceTypeCombobox(
|
||||
selectedType: _selectedInvoiceType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_selectedInvoiceType = type;
|
||||
});
|
||||
},
|
||||
isDraft: _isDraft,
|
||||
onDraftChanged: (isDraft) {
|
||||
setState(() {
|
||||
_isDraft = isDraft;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'نوع فاکتور',
|
||||
hintText: 'انتخاب نوع فاکتور',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// شماره فاکتور
|
||||
CodeFieldWidget(
|
||||
initialValue: _invoiceNumber,
|
||||
onChanged: (number) {
|
||||
setState(() {
|
||||
_invoiceNumber = number;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'شماره فاکتور',
|
||||
hintText: 'مثال: INV-2024-001',
|
||||
autoGenerateCode: _autoGenerateInvoiceNumber,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// تاریخ فاکتور
|
||||
DateInputField(
|
||||
value: _invoiceDate,
|
||||
labelText: 'تاریخ فاکتور *',
|
||||
hintText: 'انتخاب تاریخ فاکتور',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_invoiceDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// تاریخ سررسید
|
||||
DateInputField(
|
||||
value: _dueDate,
|
||||
labelText: 'تاریخ سررسید',
|
||||
hintText: 'انتخاب تاریخ سررسید',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_dueDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// مشتری
|
||||
CustomerComboboxWidget(
|
||||
selectedCustomer: _selectedCustomer,
|
||||
onCustomerChanged: (customer) {
|
||||
setState(() {
|
||||
_selectedCustomer = customer;
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'مشتری',
|
||||
hintText: 'انتخاب مشتری',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ارز فاکتور
|
||||
CurrencyPickerWidget(
|
||||
businessId: widget.businessId,
|
||||
selectedCurrencyId: _selectedCurrencyId,
|
||||
onChanged: (currencyId) {
|
||||
setState(() {
|
||||
_selectedCurrencyId = currencyId;
|
||||
});
|
||||
},
|
||||
label: 'ارز فاکتور',
|
||||
hintText: 'انتخاب ارز فاکتور',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// فروشنده و کارمزد (فقط برای فروش و برگشت فروش)
|
||||
if (_selectedInvoiceType == InvoiceType.sales || _selectedInvoiceType == InvoiceType.salesReturn) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SellerPickerWidget(
|
||||
selectedSeller: _selectedSeller,
|
||||
onSellerChanged: (seller) {
|
||||
setState(() {
|
||||
_selectedSeller = seller;
|
||||
// تنظیم خودکار نوع کارمزد و مقادیر بر اساس فروشنده
|
||||
if (seller != null) {
|
||||
if (seller.commissionSalePercent != null) {
|
||||
_commissionType = CommissionType.percentage;
|
||||
_commissionPercentage = seller.commissionSalePercent;
|
||||
_commissionAmount = null;
|
||||
} else if (seller.commissionSalesAmount != null) {
|
||||
_commissionType = CommissionType.amount;
|
||||
_commissionAmount = seller.commissionSalesAmount;
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
} else {
|
||||
_commissionType = null;
|
||||
_commissionPercentage = null;
|
||||
_commissionAmount = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'فروشنده/بازاریاب',
|
||||
hintText: 'جستوجو و انتخاب فروشنده یا بازاریاب',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلدهای کارمزد (فقط اگر فروشنده انتخاب شده باشد)
|
||||
if (_selectedSeller != null) ...[
|
||||
Expanded(
|
||||
child: CommissionTypeSelector(
|
||||
selectedType: _commissionType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_commissionType = type;
|
||||
// پاک کردن مقادیر قبلی هنگام تغییر نوع
|
||||
if (type == CommissionType.percentage) {
|
||||
_commissionAmount = null;
|
||||
} else if (type == CommissionType.amount) {
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'نوع کارمزد',
|
||||
hintText: 'انتخاب نوع کارمزد',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلد درصد کارمزد (فقط اگر نوع درصدی انتخاب شده)
|
||||
if (_commissionType == CommissionType.percentage)
|
||||
Expanded(
|
||||
child: CommissionPercentageField(
|
||||
initialValue: _commissionPercentage,
|
||||
onChanged: (percentage) {
|
||||
setState(() {
|
||||
_commissionPercentage = percentage;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'درصد کارمزد',
|
||||
hintText: 'مثال: 5.5',
|
||||
),
|
||||
)
|
||||
// فیلد مبلغ کارمزد (فقط اگر نوع مبلغی انتخاب شده)
|
||||
else if (_commissionType == CommissionType.amount)
|
||||
Expanded(
|
||||
child: CommissionAmountField(
|
||||
initialValue: _commissionAmount,
|
||||
onChanged: (amount) {
|
||||
setState(() {
|
||||
_commissionAmount = amount;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'مبلغ کارمزد',
|
||||
hintText: 'مثال: 100000',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// عنوان فاکتور
|
||||
TextFormField(
|
||||
initialValue: _invoiceTitle,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceTitle = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'عنوان فاکتور',
|
||||
hintText: 'مثال: فروش محصولات',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ارجاع
|
||||
TextFormField(
|
||||
initialValue: _invoiceReference,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceReference = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'ارجاع',
|
||||
hintText: 'مثال: PO-2024-001',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// برای دسکتاپ - چند ستونه
|
||||
return Column(
|
||||
children: [
|
||||
// ردیف اول: 5 فیلد اصلی
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InvoiceTypeCombobox(
|
||||
selectedType: _selectedInvoiceType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_selectedInvoiceType = type;
|
||||
});
|
||||
},
|
||||
isDraft: _isDraft,
|
||||
onDraftChanged: (isDraft) {
|
||||
setState(() {
|
||||
_isDraft = isDraft;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'نوع فاکتور',
|
||||
hintText: 'انتخاب نوع فاکتور',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: CodeFieldWidget(
|
||||
initialValue: _invoiceNumber,
|
||||
onChanged: (number) {
|
||||
setState(() {
|
||||
_invoiceNumber = number;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'شماره فاکتور',
|
||||
hintText: 'مثال: INV-2024-001',
|
||||
autoGenerateCode: _autoGenerateInvoiceNumber,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DateInputField(
|
||||
value: _invoiceDate,
|
||||
labelText: 'تاریخ فاکتور *',
|
||||
hintText: 'انتخاب تاریخ فاکتور',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_invoiceDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DateInputField(
|
||||
value: _dueDate,
|
||||
labelText: 'تاریخ سررسید',
|
||||
hintText: 'انتخاب تاریخ سررسید',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_dueDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: CustomerComboboxWidget(
|
||||
selectedCustomer: _selectedCustomer,
|
||||
onCustomerChanged: (customer) {
|
||||
setState(() {
|
||||
_selectedCustomer = customer;
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'مشتری',
|
||||
hintText: 'انتخاب مشتری',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ردیف دوم: ارز، عنوان فاکتور، ارجاع
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CurrencyPickerWidget(
|
||||
businessId: widget.businessId,
|
||||
selectedCurrencyId: _selectedCurrencyId,
|
||||
onChanged: (currencyId) {
|
||||
setState(() {
|
||||
_selectedCurrencyId = currencyId;
|
||||
});
|
||||
},
|
||||
label: 'ارز فاکتور',
|
||||
hintText: 'انتخاب ارز فاکتور',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _invoiceTitle,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceTitle = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'عنوان فاکتور',
|
||||
hintText: 'مثال: فروش محصولات',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _invoiceReference,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceReference = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'ارجاع',
|
||||
hintText: 'مثال: PO-2024-001',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ردیف سوم: فروشنده و کارمزد (فقط برای فروش و برگشت فروش)
|
||||
if (_selectedInvoiceType == InvoiceType.sales || _selectedInvoiceType == InvoiceType.salesReturn) ...[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SellerPickerWidget(
|
||||
selectedSeller: _selectedSeller,
|
||||
onSellerChanged: (seller) {
|
||||
setState(() {
|
||||
_selectedSeller = seller;
|
||||
// تنظیم خودکار نوع کارمزد و مقادیر بر اساس فروشنده
|
||||
if (seller != null) {
|
||||
if (seller.commissionSalePercent != null) {
|
||||
_commissionType = CommissionType.percentage;
|
||||
_commissionPercentage = seller.commissionSalePercent;
|
||||
_commissionAmount = null;
|
||||
} else if (seller.commissionSalesAmount != null) {
|
||||
_commissionType = CommissionType.amount;
|
||||
_commissionAmount = seller.commissionSalesAmount;
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
} else {
|
||||
_commissionType = null;
|
||||
_commissionPercentage = null;
|
||||
_commissionAmount = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'فروشنده/بازاریاب',
|
||||
hintText: 'جستوجو و انتخاب فروشنده یا بازاریاب',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلدهای کارمزد (فقط اگر فروشنده انتخاب شده باشد)
|
||||
if (_selectedSeller != null) ...[
|
||||
Expanded(
|
||||
child: CommissionTypeSelector(
|
||||
selectedType: _commissionType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_commissionType = type;
|
||||
// پاک کردن مقادیر قبلی هنگام تغییر نوع
|
||||
if (type == CommissionType.percentage) {
|
||||
_commissionAmount = null;
|
||||
} else if (type == CommissionType.amount) {
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'نوع کارمزد',
|
||||
hintText: 'انتخاب نوع کارمزد',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلد درصد کارمزد (فقط اگر نوع درصدی انتخاب شده)
|
||||
if (_commissionType == CommissionType.percentage)
|
||||
Expanded(
|
||||
child: CommissionPercentageField(
|
||||
initialValue: _commissionPercentage,
|
||||
onChanged: (percentage) {
|
||||
setState(() {
|
||||
_commissionPercentage = percentage;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'درصد کارمزد',
|
||||
hintText: 'مثال: 5.5',
|
||||
),
|
||||
)
|
||||
// فیلد مبلغ کارمزد (فقط اگر نوع مبلغی انتخاب شده)
|
||||
else if (_commissionType == CommissionType.amount)
|
||||
Expanded(
|
||||
child: CommissionAmountField(
|
||||
initialValue: _commissionAmount,
|
||||
onChanged: (amount) {
|
||||
setState(() {
|
||||
_commissionAmount = amount;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'مبلغ کارمزد',
|
||||
hintText: 'مثال: 100000',
|
||||
),
|
||||
)
|
||||
else
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
] else ...[
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// دکمه ادامه
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (_selectedInvoiceType != null && _invoiceDate != null) ? _continueToInvoiceForm : null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: Text('ادامه ایجاد فاکتور'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
minimumSize: const Size(200, 48),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// نمایش اطلاعات انتخاب شده
|
||||
if (_selectedInvoiceType != null || _invoiceDate != null || _dueDate != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'اطلاعات انتخاب شده:',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// نمایش اطلاعات در دو ستون
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_selectedInvoiceType != null)
|
||||
_buildInfoItem('نوع فاکتور', _selectedInvoiceType!.label),
|
||||
if (_invoiceDate != null)
|
||||
_buildInfoItem('تاریخ فاکتور', HesabixDateUtils.formatForDisplay(_invoiceDate, widget.calendarController.isJalali == true)),
|
||||
if (_dueDate != null)
|
||||
_buildInfoItem('تاریخ سررسید', HesabixDateUtils.formatForDisplay(_dueDate, widget.calendarController.isJalali == true)),
|
||||
if (_selectedCurrencyId != null)
|
||||
_buildInfoItem('ارز فاکتور', 'انتخاب شده'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_selectedSeller != null)
|
||||
_buildInfoItem('فروشنده/بازاریاب', '${_selectedSeller!.displayName} (${_selectedSeller!.personTypes.isNotEmpty ? _selectedSeller!.personTypes.first.persianName : 'نامشخص'})'),
|
||||
if (_commissionType != null)
|
||||
_buildInfoItem('نوع کارمزد', _commissionType!.label),
|
||||
if (_commissionPercentage != null)
|
||||
_buildInfoItem('درصد کارمزد', '${_commissionPercentage!.toStringAsFixed(1)}%'),
|
||||
if (_commissionAmount != null)
|
||||
_buildInfoItem('مبلغ کارمزد', '${_commissionAmount!.toStringAsFixed(0)} ریال'),
|
||||
if (_invoiceTitle != null)
|
||||
_buildInfoItem('عنوان فاکتور', _invoiceTitle!),
|
||||
if (_invoiceReference != null)
|
||||
_buildInfoItem('ارجاع', _invoiceReference!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _continueToInvoiceForm() {
|
||||
if (_selectedInvoiceType == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('لطفا نوع فاکتور را انتخاب کنید'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final invoiceNumberText = _autoGenerateInvoiceNumber
|
||||
? 'شماره فاکتور: اتوماتیک\n'
|
||||
: (_invoiceNumber != null
|
||||
? 'شماره فاکتور: $_invoiceNumber\n'
|
||||
: 'شماره فاکتور: انتخاب نشده\n');
|
||||
|
||||
final customerText = _selectedCustomer != null
|
||||
? 'مشتری: ${_selectedCustomer!.name}\n'
|
||||
: 'مشتری: خویشتنفروش\n';
|
||||
|
||||
final sellerText = _selectedSeller != null
|
||||
? 'فروشنده/بازاریاب: ${_selectedSeller!.displayName} (${_selectedSeller!.personTypes.isNotEmpty ? _selectedSeller!.personTypes.first.persianName : 'نامشخص'})\n'
|
||||
: '';
|
||||
|
||||
final commissionText = _commissionPercentage != null
|
||||
? 'درصد کارمزد: ${_commissionPercentage!.toStringAsFixed(1)}%\n'
|
||||
: '';
|
||||
|
||||
final invoiceDateText = _invoiceDate != null
|
||||
? 'تاریخ فاکتور: ${HesabixDateUtils.formatForDisplay(_invoiceDate, widget.calendarController.isJalali == true)}\n'
|
||||
: 'تاریخ فاکتور: انتخاب نشده\n';
|
||||
|
||||
final dueDateText = _dueDate != null
|
||||
? 'تاریخ سررسید: ${HesabixDateUtils.formatForDisplay(_dueDate, widget.calendarController.isJalali == true)}\n'
|
||||
: 'تاریخ سررسید: انتخاب نشده\n';
|
||||
|
||||
final currencyText = _selectedCurrencyId != null
|
||||
? 'ارز فاکتور: انتخاب شده\n'
|
||||
: 'ارز فاکتور: انتخاب نشده\n';
|
||||
|
||||
final titleText = _invoiceTitle != null
|
||||
? 'عنوان فاکتور: $_invoiceTitle\n'
|
||||
: 'عنوان فاکتور: انتخاب نشده\n';
|
||||
|
||||
final referenceText = _invoiceReference != null
|
||||
? 'ارجاع: $_invoiceReference\n'
|
||||
: 'ارجاع: انتخاب نشده\n';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('نوع فاکتور: ${_selectedInvoiceType!.label}\n$invoiceNumberText$customerText$sellerText$commissionText$invoiceDateText$dueDateText$currencyText$titleText$referenceText\nفرم کامل فاکتور به زودی اضافه خواهد شد'),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: در آینده میتوانید به صفحه فرم کامل فاکتور بروید
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => InvoiceFormPage(
|
||||
// businessId: widget.businessId,
|
||||
// authStore: widget.authStore,
|
||||
// invoiceType: _selectedInvoiceType!,
|
||||
// invoiceNumber: _invoiceNumber,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
|
||||
Widget _buildProductsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'کالاها و خدمات',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'تراکنشها',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'تنظیمات',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,903 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
import '../../core/calendar_controller.dart';
|
||||
import '../../widgets/permission/access_denied_page.dart';
|
||||
import '../../widgets/invoice/invoice_type_combobox.dart';
|
||||
import '../../widgets/invoice/code_field_widget.dart';
|
||||
import '../../widgets/invoice/customer_combobox_widget.dart';
|
||||
import '../../widgets/invoice/seller_picker_widget.dart';
|
||||
import '../../widgets/invoice/commission_percentage_field.dart';
|
||||
import '../../widgets/invoice/commission_type_selector.dart';
|
||||
import '../../widgets/invoice/commission_amount_field.dart';
|
||||
import '../../widgets/date_input_field.dart';
|
||||
import '../../widgets/banking/currency_picker_widget.dart';
|
||||
import '../../core/date_utils.dart';
|
||||
import '../../models/invoice_type_model.dart';
|
||||
import '../../models/customer_model.dart';
|
||||
import '../../models/person_model.dart';
|
||||
|
||||
class NewInvoicePage extends StatefulWidget {
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
final CalendarController calendarController;
|
||||
|
||||
const NewInvoicePage({
|
||||
super.key,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
required this.calendarController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NewInvoicePage> createState() => _NewInvoicePageState();
|
||||
}
|
||||
|
||||
class _NewInvoicePageState extends State<NewInvoicePage> with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
InvoiceType? _selectedInvoiceType;
|
||||
bool _isDraft = false;
|
||||
String? _invoiceNumber;
|
||||
bool _autoGenerateInvoiceNumber = true;
|
||||
Customer? _selectedCustomer;
|
||||
Person? _selectedSeller;
|
||||
double? _commissionPercentage;
|
||||
double? _commissionAmount;
|
||||
CommissionType? _commissionType;
|
||||
DateTime? _invoiceDate;
|
||||
DateTime? _dueDate;
|
||||
int? _selectedCurrencyId;
|
||||
String? _invoiceTitle;
|
||||
String? _invoiceReference;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
|
||||
if (!widget.authStore.canWriteSection('invoices')) {
|
||||
return AccessDeniedPage(message: t.accessDenied);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(t.addInvoice),
|
||||
toolbarHeight: 56,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
icon: Icon(Icons.info_outline),
|
||||
text: 'اطلاعات',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.inventory_2_outlined),
|
||||
text: 'کالا و خدمات',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.receipt_long_outlined),
|
||||
text: 'تراکنشها',
|
||||
),
|
||||
Tab(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
text: 'تنظیمات',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// تب اطلاعات
|
||||
_buildInfoTab(),
|
||||
// تب کالا و خدمات
|
||||
_buildProductsTab(),
|
||||
// تب تراکنشها
|
||||
_buildTransactionsTab(),
|
||||
// تب تنظیمات
|
||||
_buildSettingsTab(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTab() {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1600),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// فیلدهای اصلی - responsive layout
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// اگر عرض صفحه کمتر از 768 پیکسل باشد، تک ستونه
|
||||
if (constraints.maxWidth < 768) {
|
||||
return Column(
|
||||
children: [
|
||||
// نوع فاکتور
|
||||
InvoiceTypeCombobox(
|
||||
selectedType: _selectedInvoiceType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_selectedInvoiceType = type;
|
||||
});
|
||||
},
|
||||
isDraft: _isDraft,
|
||||
onDraftChanged: (isDraft) {
|
||||
setState(() {
|
||||
_isDraft = isDraft;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'نوع فاکتور',
|
||||
hintText: 'انتخاب نوع فاکتور',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// شماره فاکتور
|
||||
CodeFieldWidget(
|
||||
initialValue: _invoiceNumber,
|
||||
onChanged: (number) {
|
||||
setState(() {
|
||||
_invoiceNumber = number;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'شماره فاکتور',
|
||||
hintText: 'مثال: INV-2024-001',
|
||||
autoGenerateCode: _autoGenerateInvoiceNumber,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// تاریخ فاکتور
|
||||
DateInputField(
|
||||
value: _invoiceDate,
|
||||
labelText: 'تاریخ فاکتور *',
|
||||
hintText: 'انتخاب تاریخ فاکتور',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_invoiceDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// تاریخ سررسید
|
||||
DateInputField(
|
||||
value: _dueDate,
|
||||
labelText: 'تاریخ سررسید',
|
||||
hintText: 'انتخاب تاریخ سررسید',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_dueDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// مشتری
|
||||
CustomerComboboxWidget(
|
||||
selectedCustomer: _selectedCustomer,
|
||||
onCustomerChanged: (customer) {
|
||||
setState(() {
|
||||
_selectedCustomer = customer;
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'مشتری',
|
||||
hintText: 'انتخاب مشتری',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ارز فاکتور
|
||||
CurrencyPickerWidget(
|
||||
businessId: widget.businessId,
|
||||
selectedCurrencyId: _selectedCurrencyId,
|
||||
onChanged: (currencyId) {
|
||||
setState(() {
|
||||
_selectedCurrencyId = currencyId;
|
||||
});
|
||||
},
|
||||
label: 'ارز فاکتور',
|
||||
hintText: 'انتخاب ارز فاکتور',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// فروشنده و کارمزد (فقط برای فروش و برگشت فروش)
|
||||
if (_selectedInvoiceType == InvoiceType.sales || _selectedInvoiceType == InvoiceType.salesReturn) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SellerPickerWidget(
|
||||
selectedSeller: _selectedSeller,
|
||||
onSellerChanged: (seller) {
|
||||
setState(() {
|
||||
_selectedSeller = seller;
|
||||
// تنظیم خودکار نوع کارمزد و مقادیر بر اساس فروشنده
|
||||
if (seller != null) {
|
||||
if (seller.commissionSalePercent != null) {
|
||||
_commissionType = CommissionType.percentage;
|
||||
_commissionPercentage = seller.commissionSalePercent;
|
||||
_commissionAmount = null;
|
||||
} else if (seller.commissionSalesAmount != null) {
|
||||
_commissionType = CommissionType.amount;
|
||||
_commissionAmount = seller.commissionSalesAmount;
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
} else {
|
||||
_commissionType = null;
|
||||
_commissionPercentage = null;
|
||||
_commissionAmount = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'فروشنده/بازاریاب',
|
||||
hintText: 'جستوجو و انتخاب فروشنده یا بازاریاب',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلدهای کارمزد (فقط اگر فروشنده انتخاب شده باشد)
|
||||
if (_selectedSeller != null) ...[
|
||||
Expanded(
|
||||
child: CommissionTypeSelector(
|
||||
selectedType: _commissionType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_commissionType = type;
|
||||
// پاک کردن مقادیر قبلی هنگام تغییر نوع
|
||||
if (type == CommissionType.percentage) {
|
||||
_commissionAmount = null;
|
||||
} else if (type == CommissionType.amount) {
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'نوع کارمزد',
|
||||
hintText: 'انتخاب نوع کارمزد',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلد درصد کارمزد (فقط اگر نوع درصدی انتخاب شده)
|
||||
if (_commissionType == CommissionType.percentage)
|
||||
Expanded(
|
||||
child: CommissionPercentageField(
|
||||
initialValue: _commissionPercentage,
|
||||
onChanged: (percentage) {
|
||||
setState(() {
|
||||
_commissionPercentage = percentage;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'درصد کارمزد',
|
||||
hintText: 'مثال: 5.5',
|
||||
),
|
||||
)
|
||||
// فیلد مبلغ کارمزد (فقط اگر نوع مبلغی انتخاب شده)
|
||||
else if (_commissionType == CommissionType.amount)
|
||||
Expanded(
|
||||
child: CommissionAmountField(
|
||||
initialValue: _commissionAmount,
|
||||
onChanged: (amount) {
|
||||
setState(() {
|
||||
_commissionAmount = amount;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'مبلغ کارمزد',
|
||||
hintText: 'مثال: 100000',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// عنوان فاکتور
|
||||
TextFormField(
|
||||
initialValue: _invoiceTitle,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceTitle = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'عنوان فاکتور',
|
||||
hintText: 'مثال: فروش محصولات',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ارجاع
|
||||
TextFormField(
|
||||
initialValue: _invoiceReference,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceReference = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'ارجاع',
|
||||
hintText: 'مثال: PO-2024-001',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
// برای دسکتاپ - چند ستونه
|
||||
return Column(
|
||||
children: [
|
||||
// ردیف اول: 5 فیلد اصلی
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: InvoiceTypeCombobox(
|
||||
selectedType: _selectedInvoiceType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_selectedInvoiceType = type;
|
||||
});
|
||||
},
|
||||
isDraft: _isDraft,
|
||||
onDraftChanged: (isDraft) {
|
||||
setState(() {
|
||||
_isDraft = isDraft;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'نوع فاکتور',
|
||||
hintText: 'انتخاب نوع فاکتور',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: CodeFieldWidget(
|
||||
initialValue: _invoiceNumber,
|
||||
onChanged: (number) {
|
||||
setState(() {
|
||||
_invoiceNumber = number;
|
||||
});
|
||||
},
|
||||
isRequired: true,
|
||||
label: 'شماره فاکتور',
|
||||
hintText: 'مثال: INV-2024-001',
|
||||
autoGenerateCode: _autoGenerateInvoiceNumber,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DateInputField(
|
||||
value: _invoiceDate,
|
||||
labelText: 'تاریخ فاکتور *',
|
||||
hintText: 'انتخاب تاریخ فاکتور',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_invoiceDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: DateInputField(
|
||||
value: _dueDate,
|
||||
labelText: 'تاریخ سررسید',
|
||||
hintText: 'انتخاب تاریخ سررسید',
|
||||
calendarController: widget.calendarController,
|
||||
onChanged: (date) {
|
||||
setState(() {
|
||||
_dueDate = date;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: CustomerComboboxWidget(
|
||||
selectedCustomer: _selectedCustomer,
|
||||
onCustomerChanged: (customer) {
|
||||
setState(() {
|
||||
_selectedCustomer = customer;
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'مشتری',
|
||||
hintText: 'انتخاب مشتری',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ردیف دوم: ارز، عنوان فاکتور، ارجاع
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CurrencyPickerWidget(
|
||||
businessId: widget.businessId,
|
||||
selectedCurrencyId: _selectedCurrencyId,
|
||||
onChanged: (currencyId) {
|
||||
setState(() {
|
||||
_selectedCurrencyId = currencyId;
|
||||
});
|
||||
},
|
||||
label: 'ارز فاکتور',
|
||||
hintText: 'انتخاب ارز فاکتور',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _invoiceTitle,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceTitle = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'عنوان فاکتور',
|
||||
hintText: 'مثال: فروش محصولات',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: _invoiceReference,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_invoiceReference = value.trim().isEmpty ? null : value.trim();
|
||||
});
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'ارجاع',
|
||||
hintText: 'مثال: PO-2024-001',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ردیف سوم: فروشنده و کارمزد (فقط برای فروش و برگشت فروش)
|
||||
if (_selectedInvoiceType == InvoiceType.sales || _selectedInvoiceType == InvoiceType.salesReturn) ...[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SellerPickerWidget(
|
||||
selectedSeller: _selectedSeller,
|
||||
onSellerChanged: (seller) {
|
||||
setState(() {
|
||||
_selectedSeller = seller;
|
||||
// تنظیم خودکار نوع کارمزد و مقادیر بر اساس فروشنده
|
||||
if (seller != null) {
|
||||
if (seller.commissionSalePercent != null) {
|
||||
_commissionType = CommissionType.percentage;
|
||||
_commissionPercentage = seller.commissionSalePercent;
|
||||
_commissionAmount = null;
|
||||
} else if (seller.commissionSalesAmount != null) {
|
||||
_commissionType = CommissionType.amount;
|
||||
_commissionAmount = seller.commissionSalesAmount;
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
} else {
|
||||
_commissionType = null;
|
||||
_commissionPercentage = null;
|
||||
_commissionAmount = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
businessId: widget.businessId,
|
||||
authStore: widget.authStore,
|
||||
isRequired: false,
|
||||
label: 'فروشنده/بازاریاب',
|
||||
hintText: 'جستوجو و انتخاب فروشنده یا بازاریاب',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلدهای کارمزد (فقط اگر فروشنده انتخاب شده باشد)
|
||||
if (_selectedSeller != null) ...[
|
||||
Expanded(
|
||||
child: CommissionTypeSelector(
|
||||
selectedType: _commissionType,
|
||||
onTypeChanged: (type) {
|
||||
setState(() {
|
||||
_commissionType = type;
|
||||
// پاک کردن مقادیر قبلی هنگام تغییر نوع
|
||||
if (type == CommissionType.percentage) {
|
||||
_commissionAmount = null;
|
||||
} else if (type == CommissionType.amount) {
|
||||
_commissionPercentage = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'نوع کارمزد',
|
||||
hintText: 'انتخاب نوع کارمزد',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// فیلد درصد کارمزد (فقط اگر نوع درصدی انتخاب شده)
|
||||
if (_commissionType == CommissionType.percentage)
|
||||
Expanded(
|
||||
child: CommissionPercentageField(
|
||||
initialValue: _commissionPercentage,
|
||||
onChanged: (percentage) {
|
||||
setState(() {
|
||||
_commissionPercentage = percentage;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'درصد کارمزد',
|
||||
hintText: 'مثال: 5.5',
|
||||
),
|
||||
)
|
||||
// فیلد مبلغ کارمزد (فقط اگر نوع مبلغی انتخاب شده)
|
||||
else if (_commissionType == CommissionType.amount)
|
||||
Expanded(
|
||||
child: CommissionAmountField(
|
||||
initialValue: _commissionAmount,
|
||||
onChanged: (amount) {
|
||||
setState(() {
|
||||
_commissionAmount = amount;
|
||||
});
|
||||
},
|
||||
isRequired: false,
|
||||
label: 'مبلغ کارمزد',
|
||||
hintText: 'مثال: 100000',
|
||||
),
|
||||
)
|
||||
else
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
] else ...[
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: SizedBox()),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
const Expanded(child: SizedBox()), // جای خالی
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// دکمه ادامه
|
||||
Center(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: (_selectedInvoiceType != null && _invoiceDate != null) ? _continueToInvoiceForm : null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: Text('ادامه ایجاد فاکتور'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
minimumSize: const Size(200, 48),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// نمایش اطلاعات انتخاب شده
|
||||
if (_selectedInvoiceType != null || _invoiceDate != null || _dueDate != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'اطلاعات انتخاب شده:',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// نمایش اطلاعات در دو ستون
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_selectedInvoiceType != null)
|
||||
_buildInfoItem('نوع فاکتور', _selectedInvoiceType!.label),
|
||||
if (_invoiceDate != null)
|
||||
_buildInfoItem('تاریخ فاکتور', HesabixDateUtils.formatForDisplay(_invoiceDate, widget.calendarController.isJalali == true)),
|
||||
if (_dueDate != null)
|
||||
_buildInfoItem('تاریخ سررسید', HesabixDateUtils.formatForDisplay(_dueDate, widget.calendarController.isJalali == true)),
|
||||
if (_selectedCurrencyId != null)
|
||||
_buildInfoItem('ارز فاکتور', 'انتخاب شده'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_selectedSeller != null)
|
||||
_buildInfoItem('فروشنده/بازاریاب', '${_selectedSeller!.displayName} (${_selectedSeller!.personTypes.isNotEmpty ? _selectedSeller!.personTypes.first.persianName : 'نامشخص'})'),
|
||||
if (_commissionType != null)
|
||||
_buildInfoItem('نوع کارمزد', _commissionType!.label),
|
||||
if (_commissionPercentage != null)
|
||||
_buildInfoItem('درصد کارمزد', '${_commissionPercentage!.toStringAsFixed(1)}%'),
|
||||
if (_commissionAmount != null)
|
||||
_buildInfoItem('مبلغ کارمزد', '${_commissionAmount!.toStringAsFixed(0)} ریال'),
|
||||
if (_invoiceTitle != null)
|
||||
_buildInfoItem('عنوان فاکتور', _invoiceTitle!),
|
||||
if (_invoiceReference != null)
|
||||
_buildInfoItem('ارجاع', _invoiceReference!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: Text(
|
||||
'$label:',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _continueToInvoiceForm() {
|
||||
if (_selectedInvoiceType == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('لطفا نوع فاکتور را انتخاب کنید'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final invoiceNumberText = _autoGenerateInvoiceNumber
|
||||
? 'شماره فاکتور: اتوماتیک\n'
|
||||
: (_invoiceNumber != null
|
||||
? 'شماره فاکتور: $_invoiceNumber\n'
|
||||
: 'شماره فاکتور: انتخاب نشده\n');
|
||||
|
||||
final customerText = _selectedCustomer != null
|
||||
? 'مشتری: ${_selectedCustomer!.name}\n'
|
||||
: 'مشتری: خویشتنفروش\n';
|
||||
|
||||
final sellerText = _selectedSeller != null
|
||||
? 'فروشنده/بازاریاب: ${_selectedSeller!.displayName} (${_selectedSeller!.personTypes.isNotEmpty ? _selectedSeller!.personTypes.first.persianName : 'نامشخص'})\n'
|
||||
: '';
|
||||
|
||||
final commissionText = _commissionPercentage != null
|
||||
? 'درصد کارمزد: ${_commissionPercentage!.toStringAsFixed(1)}%\n'
|
||||
: '';
|
||||
|
||||
final invoiceDateText = _invoiceDate != null
|
||||
? 'تاریخ فاکتور: ${HesabixDateUtils.formatForDisplay(_invoiceDate, widget.calendarController.isJalali == true)}\n'
|
||||
: 'تاریخ فاکتور: انتخاب نشده\n';
|
||||
|
||||
final dueDateText = _dueDate != null
|
||||
? 'تاریخ سررسید: ${HesabixDateUtils.formatForDisplay(_dueDate, widget.calendarController.isJalali == true)}\n'
|
||||
: 'تاریخ سررسید: انتخاب نشده\n';
|
||||
|
||||
final currencyText = _selectedCurrencyId != null
|
||||
? 'ارز فاکتور: انتخاب شده\n'
|
||||
: 'ارز فاکتور: انتخاب نشده\n';
|
||||
|
||||
final titleText = _invoiceTitle != null
|
||||
? 'عنوان فاکتور: $_invoiceTitle\n'
|
||||
: 'عنوان فاکتور: انتخاب نشده\n';
|
||||
|
||||
final referenceText = _invoiceReference != null
|
||||
? 'ارجاع: $_invoiceReference\n'
|
||||
: 'ارجاع: انتخاب نشده\n';
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('نوع فاکتور: ${_selectedInvoiceType!.label}\n$invoiceNumberText$customerText$sellerText$commissionText$invoiceDateText$dueDateText$currencyText$titleText$referenceText\nفرم کامل فاکتور به زودی اضافه خواهد شد'),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
|
||||
// TODO: در آینده میتوانید به صفحه فرم کامل فاکتور بروید
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => InvoiceFormPage(
|
||||
// businessId: widget.businessId,
|
||||
// authStore: widget.authStore,
|
||||
// invoiceType: _selectedInvoiceType!,
|
||||
// invoiceNumber: _invoiceNumber,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
|
||||
Widget _buildProductsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'کالا و خدمات',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTransactionsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'تراکنشها',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsTab() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'تنظیمات',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'این بخش در آینده پیادهسازی خواهد شد',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +103,7 @@ class _PersonsPageState extends State<PersonsPage> {
|
|||
FilterOption(value: 'فروشنده', label: t.personTypeSeller),
|
||||
FilterOption(value: 'سهامدار', label: 'سهامدار'),
|
||||
],
|
||||
formatter: (person) => (person.personTypes.isNotEmpty
|
||||
? person.personTypes.map((e) => e.persianName).join('، ')
|
||||
: person.personType.persianName),
|
||||
formatter: (person) => person.personTypes.map((e) => e.persianName).join('، '),
|
||||
),
|
||||
TextColumn(
|
||||
'company_name',
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ class _PriceListsPageState extends State<PriceListsPage> {
|
|||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isActive
|
||||
? Theme.of(context).primaryColor.withOpacity(0.1)
|
||||
: Colors.grey.withOpacity(0.1),
|
||||
? Theme.of(context).primaryColor.withValues(alpha: 0.1)
|
||||
: Colors.grey.withValues(alpha: 0.1),
|
||||
child: Icon(
|
||||
Icons.price_change,
|
||||
color: isActive
|
||||
|
|
@ -193,7 +193,7 @@ class _PriceListsPageState extends State<PriceListsPage> {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
|
|
@ -392,9 +392,9 @@ class _PriceListsPageState extends State<PriceListsPage> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withOpacity(0.1),
|
||||
color: Colors.red.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.red.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.red.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -34,20 +34,20 @@ class _WalletPageState extends State<WalletPage> {
|
|||
Icon(
|
||||
Icons.wallet,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.6),
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
t.wallet,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'صفحه کیف پول در حال توسعه است',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
98
hesabixUI/hesabix_ui/lib/services/customer_service.dart
Normal file
98
hesabixUI/hesabix_ui/lib/services/customer_service.dart
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import '../core/api_client.dart';
|
||||
import '../models/customer_model.dart';
|
||||
|
||||
class CustomerService {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
CustomerService(this._apiClient);
|
||||
|
||||
/// جستوجوی مشتریها با پشتیبانی از pagination
|
||||
Future<Map<String, dynamic>> searchCustomers({
|
||||
required int businessId,
|
||||
String? searchQuery,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final requestData = <String, dynamic>{
|
||||
'business_id': businessId,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
};
|
||||
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
requestData['search'] = searchQuery;
|
||||
}
|
||||
|
||||
final response = await _apiClient.post(
|
||||
'/api/v1/customers/search',
|
||||
data: requestData,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
|
||||
// تبدیل لیست مشتریها
|
||||
final customersJson = data['customers'] as List<dynamic>;
|
||||
final customers = customersJson
|
||||
.map((json) => Customer.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return {
|
||||
'customers': customers,
|
||||
'total': data['total'] as int,
|
||||
'page': data['page'] as int,
|
||||
'limit': data['limit'] as int,
|
||||
'hasMore': data['has_more'] as bool,
|
||||
};
|
||||
} else {
|
||||
throw Exception('خطا در دریافت لیست مشتریها: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('خطا در جستوجوی مشتریها: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// دریافت اطلاعات یک مشتری خاص
|
||||
Future<Customer?> getCustomerById({
|
||||
required int businessId,
|
||||
required int customerId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/api/v1/customers/$customerId',
|
||||
query: {'business_id': businessId},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final customerJson = response.data as Map<String, dynamic>;
|
||||
return Customer.fromJson(customerJson);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// بررسی دسترسی کاربر به بخش مشتریها
|
||||
Future<bool> checkCustomerAccess({
|
||||
required int businessId,
|
||||
required String authToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/api/v1/customers/access-check',
|
||||
query: {'business_id': businessId},
|
||||
options: Options(
|
||||
headers: {'Authorization': 'Bearer $authToken'},
|
||||
),
|
||||
);
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,13 @@ class PersonService {
|
|||
}
|
||||
|
||||
if (filters != null && filters.isNotEmpty) {
|
||||
queryParams['filters'] = filters;
|
||||
// تبدیل Map به لیست برای API
|
||||
final filtersList = filters.entries.map((e) => {
|
||||
'property': e.key,
|
||||
'operator': 'in', // برای فیلترهای چندتایی از عملگر 'in' استفاده میکنیم
|
||||
'value': e.value,
|
||||
}).toList();
|
||||
queryParams['filters'] = filtersList;
|
||||
}
|
||||
|
||||
final response = await _apiClient.post(
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ class _CurrencyPickerWidgetState extends State<CurrencyPickerWidget> {
|
|||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
color: Theme.of(context).primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
|
|
@ -53,7 +53,7 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer.withOpacity(0.1),
|
||||
color: cs.primaryContainer.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
|
|
@ -106,10 +106,10 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primaryContainer.withOpacity(0.1),
|
||||
color: cs.primaryContainer.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: cs.primary.withOpacity(0.2),
|
||||
color: cs.primary.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
|
|
@ -175,7 +175,7 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surfaceContainerHighest.withOpacity(0.3),
|
||||
color: cs.surfaceContainerHighest.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -242,10 +242,10 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.errorContainer.withOpacity(0.1),
|
||||
color: cs.errorContainer.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: cs.error.withOpacity(0.2),
|
||||
color: cs.error.withValues(alpha: 0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
|
|
@ -342,7 +342,7 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
|
|
@ -355,7 +355,7 @@ class CombinedUserMenuButton extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.errorContainer.withOpacity(0.1),
|
||||
color: cs.errorContainer.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
|
|
|
|||
100
hesabixUI/hesabix_ui/lib/widgets/invoice/code_field_widget.dart
Normal file
100
hesabixUI/hesabix_ui/lib/widgets/invoice/code_field_widget.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||
|
||||
class CodeFieldWidget extends StatefulWidget {
|
||||
final String? initialValue;
|
||||
final ValueChanged<String?> onChanged;
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
final bool isRequired;
|
||||
final bool autoGenerateCode;
|
||||
|
||||
const CodeFieldWidget({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.onChanged,
|
||||
this.label,
|
||||
this.hintText,
|
||||
this.isRequired = false,
|
||||
this.autoGenerateCode = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CodeFieldWidget> createState() => _CodeFieldWidgetState();
|
||||
}
|
||||
|
||||
class _CodeFieldWidgetState extends State<CodeFieldWidget> {
|
||||
late TextEditingController _controller;
|
||||
late bool _autoGenerateCode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialValue ?? '');
|
||||
_autoGenerateCode = widget.autoGenerateCode;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = AppLocalizations.of(context);
|
||||
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
readOnly: _autoGenerateCode,
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label ?? t.code,
|
||||
hintText: widget.hintText ?? t.uniqueCodeNumeric,
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// سویچ اتوماتیک/دستی
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: Tooltip(
|
||||
message: _autoGenerateCode ? 'تولید خودکار کد فعال است' : 'تولید دستی کد فعال است',
|
||||
child: Switch(
|
||||
value: _autoGenerateCode,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_autoGenerateCode = value;
|
||||
if (_autoGenerateCode) {
|
||||
_controller.clear();
|
||||
widget.onChanged(null);
|
||||
}
|
||||
});
|
||||
},
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.text,
|
||||
onChanged: (value) {
|
||||
widget.onChanged(_autoGenerateCode ? null : value.trim().isEmpty ? null : value.trim());
|
||||
},
|
||||
validator: (value) {
|
||||
if (widget.isRequired && !_autoGenerateCode) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return t.personCodeRequired;
|
||||
}
|
||||
if (value.trim().length < 3) {
|
||||
return t.passwordMinLength;
|
||||
}
|
||||
if (!RegExp(r'^\d+$').hasMatch(value.trim())) {
|
||||
return t.codeMustBeNumeric;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CommissionAmountField extends StatefulWidget {
|
||||
final double? initialValue;
|
||||
final Function(double?) onChanged;
|
||||
final bool isRequired;
|
||||
final String label;
|
||||
final String hintText;
|
||||
|
||||
const CommissionAmountField({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.onChanged,
|
||||
this.isRequired = false,
|
||||
this.label = 'مبلغ کارمزد',
|
||||
this.hintText = 'مثال: 100000',
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommissionAmountField> createState() => _CommissionAmountFieldState();
|
||||
}
|
||||
|
||||
class _CommissionAmountFieldState extends State<CommissionAmountField> {
|
||||
late TextEditingController _controller;
|
||||
String? _errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(
|
||||
text: widget.initialValue?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _validateAndUpdate(String value) {
|
||||
setState(() {
|
||||
_errorText = null;
|
||||
});
|
||||
|
||||
if (value.isEmpty) {
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final doubleValue = double.tryParse(value);
|
||||
if (doubleValue == null) {
|
||||
setState(() {
|
||||
_errorText = 'لطفا مبلغ معتبر وارد کنید';
|
||||
});
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (doubleValue < 0) {
|
||||
setState(() {
|
||||
_errorText = 'مبلغ کارمزد نمیتواند منفی باشد';
|
||||
});
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChanged(doubleValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
|
||||
ThousandsSeparatorInputFormatter(),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: widget.hintText,
|
||||
prefixIcon: Icon(
|
||||
Icons.attach_money,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorText: _errorText,
|
||||
errorStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
onChanged: _validateAndUpdate,
|
||||
validator: (value) {
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return 'این فیلد الزامی است';
|
||||
}
|
||||
return _errorText;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ThousandsSeparatorInputFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (newValue.text.isEmpty) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
// Remove all non-digit characters except decimal point
|
||||
String cleanText = newValue.text.replaceAll(RegExp(r'[^\d.]'), '');
|
||||
|
||||
// Split by decimal point
|
||||
List<String> parts = cleanText.split('.');
|
||||
String integerPart = parts[0];
|
||||
String decimalPart = parts.length > 1 ? '.${parts[1]}' : '';
|
||||
|
||||
// Add thousands separators to integer part
|
||||
String formattedInteger = _addThousandsSeparator(integerPart);
|
||||
|
||||
String formattedText = formattedInteger + decimalPart;
|
||||
|
||||
return TextEditingValue(
|
||||
text: formattedText,
|
||||
selection: TextSelection.collapsed(offset: formattedText.length),
|
||||
);
|
||||
}
|
||||
|
||||
String _addThousandsSeparator(String text) {
|
||||
if (text.isEmpty) return text;
|
||||
|
||||
String reversed = text.split('').reversed.join('');
|
||||
String withCommas = reversed.replaceAllMapped(
|
||||
RegExp(r'(\d{3})(?=\d)'),
|
||||
(Match match) => '${match.group(1)},',
|
||||
);
|
||||
return withCommas.split('').reversed.join('');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CommissionPercentageField extends StatefulWidget {
|
||||
final double? initialValue;
|
||||
final Function(double?) onChanged;
|
||||
final bool isRequired;
|
||||
final String label;
|
||||
final String hintText;
|
||||
|
||||
const CommissionPercentageField({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.onChanged,
|
||||
this.isRequired = false,
|
||||
this.label = 'درصد کارمزد',
|
||||
this.hintText = 'مثال: 5.5',
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommissionPercentageField> createState() => _CommissionPercentageFieldState();
|
||||
}
|
||||
|
||||
class _CommissionPercentageFieldState extends State<CommissionPercentageField> {
|
||||
late TextEditingController _controller;
|
||||
String? _errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(
|
||||
text: widget.initialValue?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _validateAndUpdate(String value) {
|
||||
setState(() {
|
||||
_errorText = null;
|
||||
});
|
||||
|
||||
if (value.isEmpty) {
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
final doubleValue = double.tryParse(value);
|
||||
if (doubleValue == null) {
|
||||
setState(() {
|
||||
_errorText = 'لطفا عدد معتبر وارد کنید';
|
||||
});
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (doubleValue < 0 || doubleValue > 100) {
|
||||
setState(() {
|
||||
_errorText = 'درصد کارمزد باید بین 0 تا 100 باشد';
|
||||
});
|
||||
widget.onChanged(null);
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onChanged(doubleValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: widget.hintText,
|
||||
suffixText: '%',
|
||||
suffixStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.percent,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
errorText: _errorText,
|
||||
errorStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
onChanged: _validateAndUpdate,
|
||||
validator: (value) {
|
||||
if (widget.isRequired && (value == null || value.isEmpty)) {
|
||||
return 'این فیلد الزامی است';
|
||||
}
|
||||
return _errorText;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CommissionType {
|
||||
percentage('درصدی'),
|
||||
amount('مبلغی');
|
||||
|
||||
const CommissionType(this.label);
|
||||
final String label;
|
||||
}
|
||||
|
||||
class CommissionTypeSelector extends StatefulWidget {
|
||||
final CommissionType? selectedType;
|
||||
final ValueChanged<CommissionType?> onTypeChanged;
|
||||
final bool isRequired;
|
||||
final String label;
|
||||
final String hintText;
|
||||
|
||||
const CommissionTypeSelector({
|
||||
super.key,
|
||||
this.selectedType,
|
||||
required this.onTypeChanged,
|
||||
this.isRequired = false,
|
||||
this.label = 'نوع کارمزد',
|
||||
this.hintText = 'انتخاب نوع کارمزد',
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommissionTypeSelector> createState() => _CommissionTypeSelectorState();
|
||||
}
|
||||
|
||||
class _CommissionTypeSelectorState extends State<CommissionTypeSelector> {
|
||||
CommissionType? _selectedType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedType = widget.selectedType;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CommissionTypeSelector oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedType != oldWidget.selectedType) {
|
||||
setState(() {
|
||||
_selectedType = widget.selectedType;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectType(CommissionType type) {
|
||||
setState(() {
|
||||
_selectedType = type;
|
||||
});
|
||||
widget.onTypeChanged(type);
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
setState(() {
|
||||
_selectedType = null;
|
||||
});
|
||||
widget.onTypeChanged(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return DropdownButtonFormField<CommissionType>(
|
||||
value: _selectedType,
|
||||
onChanged: (CommissionType? newValue) {
|
||||
if (newValue != null) {
|
||||
_selectType(newValue);
|
||||
} else if (!widget.isRequired) {
|
||||
_clearSelection();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: widget.hintText,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: _selectedType != null
|
||||
? Icon(_getTypeIcon(_selectedType!))
|
||||
: const Icon(Icons.toggle_on_outlined),
|
||||
suffixIcon: _selectedType != null && !widget.isRequired
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _clearSelection,
|
||||
iconSize: 18,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
items: CommissionType.values.map((CommissionType type) {
|
||||
return DropdownMenuItem<CommissionType>(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getTypeIcon(type),
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type.label,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
validator: (value) {
|
||||
if (widget.isRequired && value == null) {
|
||||
return 'لطفا نوع کارمزد را انتخاب کنید';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(CommissionType type) {
|
||||
switch (type) {
|
||||
case CommissionType.percentage:
|
||||
return Icons.percent;
|
||||
case CommissionType.amount:
|
||||
return Icons.attach_money;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,313 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/customer_model.dart';
|
||||
import '../../services/customer_service.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
import '../../core/api_client.dart';
|
||||
|
||||
class CustomerComboboxWidget extends StatefulWidget {
|
||||
final Customer? selectedCustomer;
|
||||
final ValueChanged<Customer?> onCustomerChanged;
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
final bool isRequired;
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
|
||||
const CustomerComboboxWidget({
|
||||
super.key,
|
||||
this.selectedCustomer,
|
||||
required this.onCustomerChanged,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
this.isRequired = false,
|
||||
this.label = 'مشتری',
|
||||
this.hintText = 'انتخاب مشتری',
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomerComboboxWidget> createState() => _CustomerComboboxWidgetState();
|
||||
}
|
||||
|
||||
class _CustomerComboboxWidgetState extends State<CustomerComboboxWidget> {
|
||||
final CustomerService _customerService = CustomerService(ApiClient());
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<Customer> _customers = [];
|
||||
bool _isLoading = false;
|
||||
bool _hasLoadedRecent = false;
|
||||
bool _isSearchMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.text = widget.selectedCustomer?.name ?? '';
|
||||
_loadRecentCustomers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadRecentCustomers() async {
|
||||
if (_hasLoadedRecent && !_isSearchMode) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await _customerService.searchCustomers(
|
||||
businessId: widget.businessId,
|
||||
limit: 5,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_customers = result['customers'] as List<Customer>;
|
||||
_isLoading = false;
|
||||
_hasLoadedRecent = true;
|
||||
_isSearchMode = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasLoadedRecent = true;
|
||||
_isSearchMode = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchCustomers(String query) async {
|
||||
if (query.trim().isEmpty) {
|
||||
await _loadRecentCustomers();
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_isSearchMode = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await _customerService.searchCustomers(
|
||||
businessId: widget.businessId,
|
||||
searchQuery: query.trim(),
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_customers = result['customers'] as List<Customer>;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_customers.clear();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void _showCustomerPicker() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _CustomerPickerBottomSheet(
|
||||
customers: _customers,
|
||||
selectedCustomer: widget.selectedCustomer,
|
||||
onCustomerSelected: (customer) {
|
||||
widget.onCustomerChanged(customer);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
searchController: _searchController,
|
||||
onSearchChanged: _searchCustomers,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: _showCustomerPicker,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: widget.selectedCustomer != null
|
||||
? Text(
|
||||
widget.selectedCustomer!.name,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
widget.hintText!,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _CustomerPickerBottomSheet extends StatefulWidget {
|
||||
final List<Customer> customers;
|
||||
final Customer? selectedCustomer;
|
||||
final Function(Customer) onCustomerSelected;
|
||||
final TextEditingController searchController;
|
||||
final Function(String) onSearchChanged;
|
||||
final bool isLoading;
|
||||
|
||||
const _CustomerPickerBottomSheet({
|
||||
required this.customers,
|
||||
required this.selectedCustomer,
|
||||
required this.onCustomerSelected,
|
||||
required this.searchController,
|
||||
required this.onSearchChanged,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_CustomerPickerBottomSheet> createState() => _CustomerPickerBottomSheetState();
|
||||
}
|
||||
|
||||
class _CustomerPickerBottomSheetState extends State<_CustomerPickerBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// هدر
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'انتخاب مشتری',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// فیلد جستوجو
|
||||
TextField(
|
||||
controller: widget.searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'جستوجو در مشتریان...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onChanged: widget.onSearchChanged,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// لیست مشتریان
|
||||
Expanded(
|
||||
child: widget.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: widget.customers.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_off,
|
||||
size: 48,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'مشتریای یافت نشد',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: widget.customers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final customer = widget.customers[index];
|
||||
final isSelected = widget.selectedCustomer?.id == customer.id;
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
title: Text(customer.name),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (customer.code != null)
|
||||
Text('کد: ${customer.code}'),
|
||||
if (customer.phone != null)
|
||||
Text('تلفن: ${customer.phone}'),
|
||||
],
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () => widget.onCustomerSelected(customer),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/customer_model.dart';
|
||||
import '../../services/customer_service.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
import '../../core/api_client.dart';
|
||||
|
||||
class CustomerPickerWidget extends StatefulWidget {
|
||||
final Customer? selectedCustomer;
|
||||
final ValueChanged<Customer?> onCustomerChanged;
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
final bool isRequired;
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
|
||||
const CustomerPickerWidget({
|
||||
super.key,
|
||||
this.selectedCustomer,
|
||||
required this.onCustomerChanged,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
this.isRequired = false,
|
||||
this.label =
|
||||
'مشتری',
|
||||
this.hintText = 'خویشتنفروش',
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomerPickerWidget> createState() => _CustomerPickerWidgetState();
|
||||
}
|
||||
|
||||
class _CustomerPickerWidgetState extends State<CustomerPickerWidget> {
|
||||
final CustomerService _customerService = CustomerService(ApiClient());
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<Customer> _customers = [];
|
||||
bool _isLoading = false;
|
||||
bool _hasSearched = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController.text = widget.selectedCustomer?.name ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _searchCustomers(String query) async {
|
||||
if (query.trim().isEmpty) {
|
||||
setState(() {
|
||||
_customers.clear();
|
||||
_hasSearched = false;
|
||||
_errorMessage = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final result = await _customerService.searchCustomers(
|
||||
businessId: widget.businessId,
|
||||
searchQuery: query.trim(),
|
||||
limit: 50,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_customers = result['customers'] as List<Customer>;
|
||||
_hasSearched = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString();
|
||||
_customers.clear();
|
||||
_hasSearched = true;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectCustomer(Customer customer) {
|
||||
setState(() {
|
||||
_searchController.text = customer.name;
|
||||
});
|
||||
widget.onCustomerChanged(customer);
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
});
|
||||
widget.onCustomerChanged(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// هدر
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search_outlined,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.label!,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// فیلد جستوجو
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
_clearSelection();
|
||||
},
|
||||
)
|
||||
: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value.length >= 2) {
|
||||
_searchCustomers(value);
|
||||
} else if (value.isEmpty) {
|
||||
_clearSelection();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// لیست نتایج جستوجو
|
||||
if (_hasSearched && _customers.isNotEmpty) ...[
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _customers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final customer = _customers[index];
|
||||
final isSelected = widget.selectedCustomer?.id == customer.id;
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: isSelected
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
customer.name,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (customer.code != null)
|
||||
Text(
|
||||
'کد: ${customer.code}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (customer.phone != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'تلفن: ${customer.phone}',
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
selected: isSelected,
|
||||
selectedTileColor: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
onTap: () => _selectCustomer(customer),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// پیام خطا یا عدم وجود نتیجه
|
||||
if (_hasSearched && _customers.isEmpty && _errorMessage == null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'مشتریای با این مشخصات یافت نشد',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// پیام خطا
|
||||
if (_errorMessage != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: colorScheme.onErrorContainer,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'خطا در جستوجو: ${_errorMessage!.split(':').last}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// نمایش مشتری انتخاب شده
|
||||
if (widget.selectedCustomer != null) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'مشتری انتخاب شده: ${widget.selectedCustomer!.name}',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class InvoiceNumberField extends StatefulWidget {
|
||||
final String? initialValue;
|
||||
final ValueChanged<String?> onChanged;
|
||||
final bool isRequired;
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
final bool autoGenerate;
|
||||
|
||||
const InvoiceNumberField({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.onChanged,
|
||||
this.isRequired = true,
|
||||
this.label,
|
||||
this.hintText,
|
||||
this.autoGenerate = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceNumberField> createState() => _InvoiceNumberFieldState();
|
||||
}
|
||||
|
||||
class _InvoiceNumberFieldState extends State<InvoiceNumberField> {
|
||||
late TextEditingController _controller;
|
||||
bool _isAutoGenerate = true;
|
||||
bool _isManualEntry = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController(text: widget.initialValue ?? '');
|
||||
_isAutoGenerate = widget.autoGenerate;
|
||||
_isManualEntry = widget.initialValue != null && widget.initialValue!.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(InvoiceNumberField oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.initialValue != oldWidget.initialValue) {
|
||||
_controller.text = widget.initialValue ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// هدر با عنوان و دکمههای انتخاب نوع
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.confirmation_number_outlined,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.label ?? 'شماره فاکتور',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// دکمههای انتخاب نوع شمارهگذاری
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
context: context,
|
||||
icon: Icons.auto_awesome,
|
||||
label: 'اتوماتیک',
|
||||
isSelected: _isAutoGenerate,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isAutoGenerate = true;
|
||||
_isManualEntry = false;
|
||||
_controller.clear();
|
||||
});
|
||||
widget.onChanged(null);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildModeButton(
|
||||
context: context,
|
||||
icon: Icons.edit,
|
||||
label: 'دستی',
|
||||
isSelected: _isManualEntry,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isAutoGenerate = false;
|
||||
_isManualEntry = true;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// فیلد ورودی شماره فاکتور
|
||||
if (_isManualEntry) ...[
|
||||
TextFormField(
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText ?? 'شماره فاکتور را وارد کنید',
|
||||
prefixIcon: const Icon(Icons.numbers),
|
||||
suffixIcon: _controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_controller.clear();
|
||||
widget.onChanged('');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
onChanged: (value) {
|
||||
widget.onChanged(value.isEmpty ? null : value);
|
||||
},
|
||||
inputFormatters: [
|
||||
// فقط اعداد و حروف انگلیسی و خط تیره و زیرخط
|
||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9\-_]')),
|
||||
],
|
||||
validator: widget.isRequired && _isManualEntry
|
||||
? (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'شماره فاکتور الزامی است';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
: null,
|
||||
),
|
||||
] else ...[
|
||||
// نمایش حالت اتوماتیک
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_awesome,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'شماره فاکتور به صورت خودکار تولید خواهد شد',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// راهنما
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_isAutoGenerate
|
||||
? 'شماره فاکتور بر اساس الگوی تعریف شده تولید میشود'
|
||||
: 'شماره فاکتور را به صورت دستی وارد کنید (فقط حروف انگلیسی، اعداد، خط تیره و زیرخط مجاز است)',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildModeButton({
|
||||
required BuildContext context,
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? colorScheme.primary
|
||||
: colorScheme.outline.withValues(alpha: 0.3),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: isSelected
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/invoice_type_model.dart';
|
||||
|
||||
class InvoiceTypeCombobox extends StatefulWidget {
|
||||
final InvoiceType? selectedType;
|
||||
final ValueChanged<InvoiceType?> onTypeChanged;
|
||||
final bool isDraft;
|
||||
final ValueChanged<bool> onDraftChanged;
|
||||
final bool isRequired;
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
|
||||
const InvoiceTypeCombobox({
|
||||
super.key,
|
||||
this.selectedType,
|
||||
required this.onTypeChanged,
|
||||
this.isDraft = false,
|
||||
required this.onDraftChanged,
|
||||
this.isRequired = true,
|
||||
this.label = 'نوع فاکتور',
|
||||
this.hintText = 'انتخاب نوع فاکتور',
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceTypeCombobox> createState() => _InvoiceTypeComboboxState();
|
||||
}
|
||||
|
||||
class _InvoiceTypeComboboxState extends State<InvoiceTypeCombobox> {
|
||||
InvoiceType? _selectedType;
|
||||
late bool _isDraft;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedType = widget.selectedType;
|
||||
_isDraft = widget.isDraft;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(InvoiceTypeCombobox oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedType != oldWidget.selectedType) {
|
||||
setState(() {
|
||||
_selectedType = widget.selectedType;
|
||||
});
|
||||
}
|
||||
if (widget.isDraft != oldWidget.isDraft) {
|
||||
setState(() {
|
||||
_isDraft = widget.isDraft;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _selectType(InvoiceType type) {
|
||||
setState(() {
|
||||
_selectedType = type;
|
||||
});
|
||||
widget.onTypeChanged(type);
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
setState(() {
|
||||
_selectedType = null;
|
||||
});
|
||||
widget.onTypeChanged(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return DropdownButtonFormField<InvoiceType>(
|
||||
value: _selectedType,
|
||||
onChanged: (InvoiceType? newValue) {
|
||||
if (newValue != null) {
|
||||
_selectType(newValue);
|
||||
} else if (!widget.isRequired) {
|
||||
_clearSelection();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.label,
|
||||
hintText: widget.hintText,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: _selectedType != null
|
||||
? Icon(_getTypeIcon(_selectedType!))
|
||||
: const Icon(Icons.category_outlined),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// سویچ پیشنویس کوچک
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
child: Tooltip(
|
||||
message: _isDraft ? 'حالت پیشنویس فعال است' : 'فعال کردن حالت پیشنویس',
|
||||
child: Switch(
|
||||
value: _isDraft,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isDraft = value;
|
||||
});
|
||||
widget.onDraftChanged(value);
|
||||
},
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
),
|
||||
// دکمه پاک کردن (اگر نیاز باشد)
|
||||
if (_selectedType != null && !widget.isRequired)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _clearSelection,
|
||||
iconSize: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
items: InvoiceType.allTypes.map((InvoiceType type) {
|
||||
return DropdownMenuItem<InvoiceType>(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
_getTypeIcon(type),
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type.label,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
validator: (value) {
|
||||
if (widget.isRequired && value == null) {
|
||||
return 'انتخاب ${widget.label} الزامی است';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(InvoiceType type) {
|
||||
switch (type) {
|
||||
case InvoiceType.sales:
|
||||
return Icons.shopping_cart_outlined;
|
||||
case InvoiceType.salesReturn:
|
||||
return Icons.keyboard_return_outlined;
|
||||
case InvoiceType.purchase:
|
||||
return Icons.shop_outlined;
|
||||
case InvoiceType.purchaseReturn:
|
||||
return Icons.assignment_return_outlined;
|
||||
case InvoiceType.waste:
|
||||
return Icons.delete_outline;
|
||||
case InvoiceType.directConsumption:
|
||||
return Icons.flash_on_outlined;
|
||||
case InvoiceType.production:
|
||||
return Icons.precision_manufacturing_outlined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../models/invoice_type_model.dart';
|
||||
|
||||
class InvoiceTypeSelector extends StatefulWidget {
|
||||
final InvoiceType? selectedType;
|
||||
final ValueChanged<InvoiceType?> onTypeChanged;
|
||||
final bool isRequired;
|
||||
|
||||
const InvoiceTypeSelector({
|
||||
super.key,
|
||||
this.selectedType,
|
||||
required this.onTypeChanged,
|
||||
this.isRequired = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<InvoiceTypeSelector> createState() => _InvoiceTypeSelectorState();
|
||||
}
|
||||
|
||||
class _InvoiceTypeSelectorState extends State<InvoiceTypeSelector> {
|
||||
InvoiceType? _selectedType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedType = widget.selectedType;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(InvoiceTypeSelector oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedType != oldWidget.selectedType) {
|
||||
setState(() {
|
||||
_selectedType = widget.selectedType;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.category_outlined,
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'انتخاب نوع فاکتور',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (widget.isRequired)
|
||||
Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
color: colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// استفاده از SegmentedButton
|
||||
SegmentedButton<InvoiceType>(
|
||||
segments: InvoiceType.allTypes.map((type) {
|
||||
return ButtonSegment<InvoiceType>(
|
||||
value: type,
|
||||
label: Text(type.label),
|
||||
icon: Icon(_getTypeIcon(type)),
|
||||
);
|
||||
}).toList(),
|
||||
selected: _selectedType != null ? {_selectedType!} : <InvoiceType>{},
|
||||
onSelectionChanged: (Set<InvoiceType> selection) {
|
||||
final selectedType = selection.isNotEmpty ? selection.first : null;
|
||||
setState(() {
|
||||
_selectedType = selectedType;
|
||||
});
|
||||
widget.onTypeChanged(selectedType);
|
||||
},
|
||||
multiSelectionEnabled: false,
|
||||
showSelectedIcon: true,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return colorScheme.primaryContainer;
|
||||
}
|
||||
return colorScheme.surfaceContainerHighest;
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return colorScheme.onPrimaryContainer;
|
||||
}
|
||||
return colorScheme.onSurfaceVariant;
|
||||
}),
|
||||
side: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
);
|
||||
}
|
||||
return BorderSide(
|
||||
color: colorScheme.outline.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(InvoiceType type) {
|
||||
switch (type) {
|
||||
case InvoiceType.sales:
|
||||
return Icons.shopping_cart_outlined;
|
||||
case InvoiceType.salesReturn:
|
||||
return Icons.keyboard_return_outlined;
|
||||
case InvoiceType.purchase:
|
||||
return Icons.shop_outlined;
|
||||
case InvoiceType.purchaseReturn:
|
||||
return Icons.assignment_return_outlined;
|
||||
case InvoiceType.waste:
|
||||
return Icons.delete_outline;
|
||||
case InvoiceType.directConsumption:
|
||||
return Icons.flash_on_outlined;
|
||||
case InvoiceType.production:
|
||||
return Icons.precision_manufacturing_outlined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import '../../core/auth_store.dart';
|
||||
import '../../models/person_model.dart';
|
||||
import '../../services/person_service.dart';
|
||||
|
||||
class SellerPickerWidget extends StatefulWidget {
|
||||
final Person? selectedSeller;
|
||||
final Function(Person?) onSellerChanged;
|
||||
final int businessId;
|
||||
final AuthStore authStore;
|
||||
final bool isRequired;
|
||||
final String label;
|
||||
final String hintText;
|
||||
|
||||
const SellerPickerWidget({
|
||||
super.key,
|
||||
this.selectedSeller,
|
||||
required this.onSellerChanged,
|
||||
required this.businessId,
|
||||
required this.authStore,
|
||||
this.isRequired = false,
|
||||
this.label = 'فروشنده/بازاریاب',
|
||||
this.hintText = 'جستوجو و انتخاب فروشنده یا بازاریاب',
|
||||
});
|
||||
|
||||
@override
|
||||
State<SellerPickerWidget> createState() => _SellerPickerWidgetState();
|
||||
}
|
||||
|
||||
class _SellerPickerWidgetState extends State<SellerPickerWidget> {
|
||||
final PersonService _personService = PersonService();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
List<Person> _sellers = [];
|
||||
bool _isLoading = false;
|
||||
bool _isSearching = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSellers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadSellers() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await _personService.getPersons(
|
||||
businessId: widget.businessId,
|
||||
filters: {
|
||||
'person_types': ['فروشنده', 'بازاریاب'], // فقط فروشنده و بازاریاب
|
||||
},
|
||||
limit: 100, // دریافت همه فروشندگان/بازاریابها
|
||||
);
|
||||
|
||||
final sellers = _personService.parsePersonsList(response);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sellers = sellers;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا در دریافت لیست فروشندگان: $e'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchSellers(String query) async {
|
||||
if (query.isEmpty) {
|
||||
_loadSellers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await _personService.getPersons(
|
||||
businessId: widget.businessId,
|
||||
search: query,
|
||||
filters: {
|
||||
'person_types': ['فروشنده', 'بازاریاب'],
|
||||
},
|
||||
limit: 50,
|
||||
);
|
||||
|
||||
final sellers = _personService.parsePersonsList(response);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_sellers = sellers;
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isSearching = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('خطا در جستوجو: $e'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showSellerPicker() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _SellerPickerBottomSheet(
|
||||
sellers: _sellers,
|
||||
selectedSeller: widget.selectedSeller,
|
||||
onSellerSelected: (seller) {
|
||||
widget.onSellerChanged(seller);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
searchController: _searchController,
|
||||
onSearchChanged: _searchSellers,
|
||||
isLoading: _isLoading || _isSearching,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: _showSellerPicker,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: widget.selectedSeller != null
|
||||
? Text(
|
||||
widget.selectedSeller!.displayName,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
)
|
||||
: Text(
|
||||
widget.hintText,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
if (widget.selectedSeller != null)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.onSellerChanged(null);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
Icons.clear,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SellerPickerBottomSheet extends StatefulWidget {
|
||||
final List<Person> sellers;
|
||||
final Person? selectedSeller;
|
||||
final Function(Person) onSellerSelected;
|
||||
final TextEditingController searchController;
|
||||
final Function(String) onSearchChanged;
|
||||
final bool isLoading;
|
||||
|
||||
const _SellerPickerBottomSheet({
|
||||
required this.sellers,
|
||||
required this.selectedSeller,
|
||||
required this.onSellerSelected,
|
||||
required this.searchController,
|
||||
required this.onSearchChanged,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SellerPickerBottomSheet> createState() => _SellerPickerBottomSheetState();
|
||||
}
|
||||
|
||||
class _SellerPickerBottomSheetState extends State<_SellerPickerBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// هدر
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'انتخاب فروشنده/بازاریاب',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// فیلد جستوجو
|
||||
TextField(
|
||||
controller: widget.searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'جستوجو در فروشندگان و بازاریابها...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
onChanged: widget.onSearchChanged,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// لیست فروشندگان
|
||||
Expanded(
|
||||
child: widget.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: widget.sellers.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_off,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'فروشنده یا بازاریابی یافت نشد',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: widget.sellers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final seller = widget.sellers[index];
|
||||
final isSelected = widget.selectedSeller?.id == seller.id;
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
title: Text(seller.displayName),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(seller.personTypes.isNotEmpty
|
||||
? seller.personTypes.first.persianName
|
||||
: 'نامشخص'),
|
||||
if (seller.commissionSalePercent != null)
|
||||
Text(
|
||||
'کارمزد فروش: ${seller.commissionSalePercent!.toStringAsFixed(1)}%',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
)
|
||||
: null,
|
||||
onTap: () => widget.onSellerSelected(seller),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -100,10 +100,10 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
|||
_faxController.text = person.fax ?? '';
|
||||
_emailController.text = person.email ?? '';
|
||||
_websiteController.text = person.website ?? '';
|
||||
_selectedPersonType = person.personType;
|
||||
_selectedPersonType = person.personTypes.isNotEmpty ? person.personTypes.first : PersonType.customer;
|
||||
_selectedPersonTypes
|
||||
..clear()
|
||||
..addAll(person.personTypes.isNotEmpty ? person.personTypes : [person.personType]);
|
||||
..addAll(person.personTypes);
|
||||
_isActive = person.isActive;
|
||||
_bankAccounts = List.from(person.bankAccounts);
|
||||
// مقدار اولیه سهام
|
||||
|
|
@ -230,7 +230,6 @@ class _PersonFormDialogState extends State<PersonFormDialog> {
|
|||
aliasName: _aliasNameController.text.trim(),
|
||||
firstName: _firstNameController.text.trim().isEmpty ? null : _firstNameController.text.trim(),
|
||||
lastName: _lastNameController.text.trim().isEmpty ? null : _lastNameController.text.trim(),
|
||||
personType: null,
|
||||
personTypes: _selectedPersonTypes.isNotEmpty ? _selectedPersonTypes.toList() : null,
|
||||
companyName: _companyNameController.text.trim().isEmpty ? null : _companyNameController.text.trim(),
|
||||
paymentId: _paymentIdController.text.trim().isEmpty ? null : _paymentIdController.text.trim(),
|
||||
|
|
|
|||
|
|
@ -172,9 +172,9 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
color: Colors.orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
@ -273,7 +273,7 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [primary.withOpacity(0.90), primary.withOpacity(0.75)],
|
||||
colors: [primary.withValues(alpha: 0.90), primary.withValues(alpha: 0.75)],
|
||||
begin: Alignment.centerRight,
|
||||
end: Alignment.centerLeft,
|
||||
),
|
||||
|
|
@ -284,7 +284,7 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: onPrimary.withOpacity(0.15),
|
||||
color: onPrimary.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.price_change, color: onPrimary),
|
||||
|
|
@ -300,7 +300,7 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
),
|
||||
Text(
|
||||
t.bulkPriceUpdateSubtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: onPrimary.withOpacity(0.9)),
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: onPrimary.withValues(alpha: 0.9)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -366,7 +366,7 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(16, 10, 16, 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: BorderSide(color: theme.dividerColor.withOpacity(0.4))),
|
||||
border: Border(top: BorderSide(color: theme.dividerColor.withValues(alpha: 0.4))),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
|
|
@ -881,7 +881,7 @@ class _BulkPriceUpdateDialogState extends State<BulkPriceUpdateDialog> {
|
|||
width: 28,
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.10),
|
||||
color: theme.colorScheme.primary.withValues(alpha: 0.10),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 16, color: theme.colorScheme.primary),
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ class ProductBasicInfoSection extends StatelessWidget {
|
|||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
color: isSelected
|
||||
? Theme.of(context).primaryColor.withOpacity(0.05)
|
||||
? Theme.of(context).primaryColor.withValues(alpha: 0.05)
|
||||
: Theme.of(context).cardColor,
|
||||
),
|
||||
child: Column(
|
||||
|
|
@ -336,7 +336,7 @@ class ProductBasicInfoSection extends StatelessWidget {
|
|||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.7),
|
||||
color: Theme.of(context).textTheme.bodySmall?.color?.withValues(alpha: 0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -226,9 +226,9 @@ class ProductPricingInventorySection extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.withOpacity(0.1),
|
||||
color: Colors.blue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||
border: Border.all(color: Colors.blue.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue