diff --git a/hesabixAPI/adapters/api/v1/customers.py b/hesabixAPI/adapters/api/v1/customers.py new file mode 100644 index 0000000..c4ebba3 --- /dev/null +++ b/hesabixAPI/adapters/api/v1/customers.py @@ -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": "دسترسی مجاز است"} diff --git a/hesabixAPI/adapters/db/models/person.py b/hesabixAPI/adapters/db/models/person.py index f130c07..4074d75 100644 --- a/hesabixAPI/adapters/db/models/person.py +++ b/hesabixAPI/adapters/db/models/person.py @@ -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="شناسه پرداخت") # سهام diff --git a/hesabixAPI/app/core/permissions.py b/hesabixAPI/app/core/permissions.py index 352aaa9..9e326b7 100644 --- a/hesabixAPI/app/core/permissions.py +++ b/hesabixAPI/app/core/permissions.py @@ -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 diff --git a/hesabixAPI/app/main.py b/hesabixAPI/app/main.py index 28bd0c5..b54d9e5 100644 --- a/hesabixAPI/app/main.py +++ b/hesabixAPI/app/main.py @@ -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) diff --git a/hesabixAPI/app/services/person_service.py b/hesabixAPI/app/services/person_service.py index 2c8615a..dca391b 100644 --- a/hesabixAPI/app/services/person_service.py +++ b/hesabixAPI/app/services/person_service.py @@ -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() diff --git a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt index 8594726..2d49baa 100644 --- a/hesabixAPI/hesabix_api.egg-info/SOURCES.txt +++ b/hesabixAPI/hesabix_api.egg-info/SOURCES.txt @@ -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 diff --git a/hesabixAPI/migrations/versions/c302bc2f2cb8_remove_person_type_column.py b/hesabixAPI/migrations/versions/c302bc2f2cb8_remove_person_type_column.py new file mode 100644 index 0000000..8ab2629 --- /dev/null +++ b/hesabixAPI/migrations/versions/c302bc2f2cb8_remove_person_type_column.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='نوع شخص' + ) + ) diff --git a/hesabixUI/hesabix_ui/lib/main.dart b/hesabixUI/hesabix_ui/lib/main.dart index a35b0d9..ad53110 100644 --- a/hesabixUI/hesabix_ui/lib/main.dart +++ b/hesabixUI/hesabix_ui/lib/main.dart @@ -652,6 +652,7 @@ class _MyAppState extends State { child: NewInvoicePage( businessId: businessId, authStore: _authStore!, + calendarController: _calendarController!, ), ); }, diff --git a/hesabixUI/hesabix_ui/lib/models/customer_model.dart b/hesabixUI/hesabix_ui/lib/models/customer_model.dart new file mode 100644 index 0000000..3bec55d --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/models/customer_model.dart @@ -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 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 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; +} diff --git a/hesabixUI/hesabix_ui/lib/models/invoice_type_model.dart b/hesabixUI/hesabix_ui/lib/models/invoice_type_model.dart new file mode 100644 index 0000000..cf04b08 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/models/invoice_type_model.dart @@ -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 get allTypes => InvoiceType.values; +} diff --git a/hesabixUI/hesabix_ui/lib/models/person_model.dart b/hesabixUI/hesabix_ui/lib/models/person_model.dart index 5b4b986..991c5c7 100644 --- a/hesabixUI/hesabix_ui/lib/models/person_model.dart +++ b/hesabixUI/hesabix_ui/lib/models/person_model.dart @@ -102,7 +102,6 @@ class Person { final String aliasName; final String? firstName; final String? lastName; - final PersonType personType; final List 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? 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? 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; diff --git a/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart index 4231338..3d14d05 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/business_shell.dart @@ -948,6 +948,9 @@ class _BusinessShellState extends State { 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 { // در حال حاضر فقط اشخاص پشتیبانی می‌شود 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( diff --git a/hesabixUI/hesabix_ui/lib/pages/business/invoice_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/invoice_page.dart index ab5cac2..5ad280b 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/invoice_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/invoice_page.dart @@ -34,20 +34,20 @@ class _InvoicePageState extends State { 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), ), ), ], diff --git a/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page.dart index 1443230..be48229 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page.dart @@ -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 createState() => _NewInvoicePageState(); } -class _NewInvoicePageState extends State { +class _NewInvoicePageState extends State 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 { 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, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page_backup.dart b/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page_backup.dart new file mode 100644 index 0000000..64ad2d6 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/pages/business/new_invoice_page_backup.dart @@ -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 createState() => _NewInvoicePageState(); +} + +class _NewInvoicePageState extends State 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, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart index 9a8d671..482aae4 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/persons_page.dart @@ -103,9 +103,7 @@ class _PersonsPageState extends State { 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', diff --git a/hesabixUI/hesabix_ui/lib/pages/business/price_lists_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/price_lists_page.dart index 91add61..736d4ec 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/price_lists_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/price_lists_page.dart @@ -169,8 +169,8 @@ class _PriceListsPageState extends State { 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 { 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 { 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: [ diff --git a/hesabixUI/hesabix_ui/lib/pages/business/wallet_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/wallet_page.dart index 49c6374..5aa8edb 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/wallet_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/wallet_page.dart @@ -34,20 +34,20 @@ class _WalletPageState extends State { 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), ), ), ], diff --git a/hesabixUI/hesabix_ui/lib/services/customer_service.dart b/hesabixUI/hesabix_ui/lib/services/customer_service.dart new file mode 100644 index 0000000..fa3553a --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/services/customer_service.dart @@ -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> searchCustomers({ + required int businessId, + String? searchQuery, + int page = 1, + int limit = 20, + }) async { + try { + final requestData = { + '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; + + // تبدیل لیست مشتری‌ها + final customersJson = data['customers'] as List; + final customers = customersJson + .map((json) => Customer.fromJson(json as Map)) + .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 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; + return Customer.fromJson(customerJson); + } else { + return null; + } + } catch (e) { + return null; + } + } + + /// بررسی دسترسی کاربر به بخش مشتری‌ها + Future 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; + } + } +} diff --git a/hesabixUI/hesabix_ui/lib/services/person_service.dart b/hesabixUI/hesabix_ui/lib/services/person_service.dart index 079175b..b318a54 100644 --- a/hesabixUI/hesabix_ui/lib/services/person_service.dart +++ b/hesabixUI/hesabix_ui/lib/services/person_service.dart @@ -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( diff --git a/hesabixUI/hesabix_ui/lib/widgets/banking/currency_picker_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/banking/currency_picker_widget.dart index 2929e2a..70aac44 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/banking/currency_picker_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/banking/currency_picker_widget.dart @@ -137,7 +137,7 @@ class _CurrencyPickerWidgetState extends State { 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( diff --git a/hesabixUI/hesabix_ui/lib/widgets/combined_user_menu_button.dart b/hesabixUI/hesabix_ui/lib/widgets/combined_user_menu_button.dart index 03081ef..4590859 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/combined_user_menu_button.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/combined_user_menu_button.dart @@ -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), diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/code_field_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/code_field_widget.dart new file mode 100644 index 0000000..4ecc12c --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/code_field_widget.dart @@ -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 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 createState() => _CodeFieldWidgetState(); +} + +class _CodeFieldWidgetState extends State { + 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; + }, + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_amount_field.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_amount_field.dart new file mode 100644 index 0000000..14bd739 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_amount_field.dart @@ -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 createState() => _CommissionAmountFieldState(); +} + +class _CommissionAmountFieldState extends State { + 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 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(''); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_percentage_field.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_percentage_field.dart new file mode 100644 index 0000000..af9b348 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_percentage_field.dart @@ -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 createState() => _CommissionPercentageFieldState(); +} + +class _CommissionPercentageFieldState extends State { + 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; + }, + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_type_selector.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_type_selector.dart new file mode 100644 index 0000000..4b0ad0e --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/commission_type_selector.dart @@ -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 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 createState() => _CommissionTypeSelectorState(); +} + +class _CommissionTypeSelectorState extends State { + 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( + 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( + 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; + } + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_combobox_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_combobox_widget.dart new file mode 100644 index 0000000..280600d --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_combobox_widget.dart @@ -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 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 createState() => _CustomerComboboxWidgetState(); +} + +class _CustomerComboboxWidgetState extends State { + final CustomerService _customerService = CustomerService(ApiClient()); + final TextEditingController _searchController = TextEditingController(); + List _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 _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; + _isLoading = false; + _hasLoadedRecent = true; + _isSearchMode = false; + }); + } catch (e) { + setState(() { + _isLoading = false; + _hasLoadedRecent = true; + _isSearchMode = false; + }); + } + } + + Future _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; + _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 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), + ); + }, + ), + ), + ], + ), + ); + } + +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_picker_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_picker_widget.dart new file mode 100644 index 0000000..ed255d5 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/customer_picker_widget.dart @@ -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 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 createState() => _CustomerPickerWidgetState(); +} + +class _CustomerPickerWidgetState extends State { + final CustomerService _customerService = CustomerService(ApiClient()); + final TextEditingController _searchController = TextEditingController(); + List _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 _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; + _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, + ), + ), + ), + ], + ), + ), + ), + ], + ], + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_number_field.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_number_field.dart new file mode 100644 index 0000000..dd14b06 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_number_field.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class InvoiceNumberField extends StatefulWidget { + final String? initialValue; + final ValueChanged 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 createState() => _InvoiceNumberFieldState(); +} + +class _InvoiceNumberFieldState extends State { + 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_combobox.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_combobox.dart new file mode 100644 index 0000000..474cfa1 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_combobox.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; +import '../../models/invoice_type_model.dart'; + +class InvoiceTypeCombobox extends StatefulWidget { + final InvoiceType? selectedType; + final ValueChanged onTypeChanged; + final bool isDraft; + final ValueChanged 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 createState() => _InvoiceTypeComboboxState(); +} + +class _InvoiceTypeComboboxState extends State { + 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( + 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( + 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; + } + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_selector.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_selector.dart new file mode 100644 index 0000000..04f9206 --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/invoice_type_selector.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import '../../models/invoice_type_model.dart'; + +class InvoiceTypeSelector extends StatefulWidget { + final InvoiceType? selectedType; + final ValueChanged onTypeChanged; + final bool isRequired; + + const InvoiceTypeSelector({ + super.key, + this.selectedType, + required this.onTypeChanged, + this.isRequired = true, + }); + + @override + State createState() => _InvoiceTypeSelectorState(); +} + +class _InvoiceTypeSelectorState extends State { + 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( + segments: InvoiceType.allTypes.map((type) { + return ButtonSegment( + value: type, + label: Text(type.label), + icon: Icon(_getTypeIcon(type)), + ); + }).toList(), + selected: _selectedType != null ? {_selectedType!} : {}, + onSelectionChanged: (Set 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; + } + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/invoice/seller_picker_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/invoice/seller_picker_widget.dart new file mode 100644 index 0000000..4897bca --- /dev/null +++ b/hesabixUI/hesabix_ui/lib/widgets/invoice/seller_picker_widget.dart @@ -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 createState() => _SellerPickerWidgetState(); +} + +class _SellerPickerWidgetState extends State { + final PersonService _personService = PersonService(); + final TextEditingController _searchController = TextEditingController(); + List _sellers = []; + bool _isLoading = false; + bool _isSearching = false; + + @override + void initState() { + super.initState(); + _loadSellers(); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _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 _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 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), + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart index 55e5c28..5835e8b 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/person/person_form_dialog.dart @@ -100,10 +100,10 @@ class _PersonFormDialogState extends State { _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 { 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(), diff --git a/hesabixUI/hesabix_ui/lib/widgets/product/bulk_price_update_dialog.dart b/hesabixUI/hesabix_ui/lib/widgets/product/bulk_price_update_dialog.dart index a7d5978..d34a226 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/product/bulk_price_update_dialog.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/product/bulk_price_update_dialog.dart @@ -172,9 +172,9 @@ class _BulkPriceUpdateDialogState extends State { 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 { 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 { 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 { ), 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 { 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 { 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), diff --git a/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_basic_info_section.dart b/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_basic_info_section.dart index d95dc0c..660b750 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_basic_info_section.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_basic_info_section.dart @@ -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, ), diff --git a/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_pricing_inventory_section.dart b/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_pricing_inventory_section.dart index ffdd2d2..5048cd0 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_pricing_inventory_section.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/product/sections/product_pricing_inventory_section.dart @@ -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: [