progress in bank and cashdesk
This commit is contained in:
parent
dd3a17fbd8
commit
5cc575e3d9
|
|
@ -250,24 +250,93 @@ def get_business_info_with_permissions(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر"""
|
"""دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"=== get_business_info_with_permissions START ===")
|
||||||
|
logger.info(f"Business ID: {business_id}")
|
||||||
|
logger.info(f"User ID: {ctx.get_user_id()}")
|
||||||
|
logger.info(f"User context business_id: {ctx.business_id}")
|
||||||
|
logger.info(f"Is superadmin: {ctx.is_superadmin()}")
|
||||||
|
logger.info(f"Is business owner: {ctx.is_business_owner(business_id)}")
|
||||||
|
|
||||||
from adapters.db.models.business import Business
|
from adapters.db.models.business import Business
|
||||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||||
|
|
||||||
# دریافت اطلاعات کسب و کار
|
# دریافت اطلاعات کسب و کار
|
||||||
business = db.get(Business, business_id)
|
business = db.get(Business, business_id)
|
||||||
if not business:
|
if not business:
|
||||||
|
logger.error(f"Business {business_id} not found")
|
||||||
from app.core.responses import ApiError
|
from app.core.responses import ApiError
|
||||||
raise ApiError("NOT_FOUND", "Business not found", http_status=404)
|
raise ApiError("NOT_FOUND", "Business not found", http_status=404)
|
||||||
|
|
||||||
|
logger.info(f"Business found: {business.name} (Owner ID: {business.owner_id})")
|
||||||
|
|
||||||
# دریافت دسترسیهای کاربر
|
# دریافت دسترسیهای کاربر
|
||||||
permissions = {}
|
permissions = {}
|
||||||
if not ctx.is_superadmin() and not ctx.is_business_owner(business_id):
|
|
||||||
|
# Debug logging
|
||||||
|
logger.info(f"Checking permissions for user {ctx.get_user_id()}")
|
||||||
|
logger.info(f"Is superadmin: {ctx.is_superadmin()}")
|
||||||
|
logger.info(f"Is business owner of {business_id}: {ctx.is_business_owner(business_id)}")
|
||||||
|
logger.info(f"Context business_id: {ctx.business_id}")
|
||||||
|
|
||||||
|
if ctx.is_superadmin():
|
||||||
|
logger.info("User is superadmin, but superadmin permissions don't apply to business operations")
|
||||||
|
# SuperAdmin فقط برای مدیریت سیستم است، نه برای کسب و کارهای خاص
|
||||||
|
# باید دسترسیهای کسب و کار را از جدول business_permissions دریافت کند
|
||||||
|
permission_repo = BusinessPermissionRepository(db)
|
||||||
|
business_permission = permission_repo.get_by_user_and_business(ctx.get_user_id(), business_id)
|
||||||
|
logger.info(f"Business permission object for superadmin: {business_permission}")
|
||||||
|
|
||||||
|
if business_permission:
|
||||||
|
permissions = business_permission.business_permissions or {}
|
||||||
|
logger.info(f"Superadmin business permissions: {permissions}")
|
||||||
|
else:
|
||||||
|
logger.info("No business permission found for superadmin user")
|
||||||
|
permissions = {}
|
||||||
|
elif ctx.is_business_owner(business_id):
|
||||||
|
logger.info("User is business owner, granting full permissions")
|
||||||
|
# مالک کسب و کار تمام دسترسیها را دارد
|
||||||
|
permissions = {
|
||||||
|
"people": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"products": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"bank_accounts": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"invoices": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"people_transactions": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"expenses_income": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"transfers": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"checks": {"add": True, "edit": True, "view": True, "delete": True, "return": True, "collect": True, "transfer": True},
|
||||||
|
"accounting_documents": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"chart_of_accounts": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"opening_balance": {"edit": True, "view": True},
|
||||||
|
"settings": {"print": True, "users": True, "history": True, "business": True},
|
||||||
|
"categories": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"product_attributes": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"warehouses": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"warehouse_transfers": {"add": True, "edit": True, "view": True, "draft": True, "delete": True},
|
||||||
|
"cash": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"petty_cash": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"wallet": {"view": True, "charge": True},
|
||||||
|
"storage": {"view": True, "delete": True},
|
||||||
|
"marketplace": {"buy": True, "view": True, "invoices": True},
|
||||||
|
"price_lists": {"add": True, "edit": True, "view": True, "delete": True},
|
||||||
|
"sms": {"history": True, "templates": True},
|
||||||
|
"join": True
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.info("User is not superadmin and not business owner, checking permissions")
|
||||||
# دریافت دسترسیهای کسب و کار از business_permissions
|
# دریافت دسترسیهای کسب و کار از business_permissions
|
||||||
permission_repo = BusinessPermissionRepository(db)
|
permission_repo = BusinessPermissionRepository(db)
|
||||||
# ترتیب آرگومانها: (user_id, business_id)
|
# ترتیب آرگومانها: (user_id, business_id)
|
||||||
business_permission = permission_repo.get_by_user_and_business(ctx.get_user_id(), business_id)
|
business_permission = permission_repo.get_by_user_and_business(ctx.get_user_id(), business_id)
|
||||||
|
logger.info(f"Business permission object: {business_permission}")
|
||||||
|
|
||||||
if business_permission:
|
if business_permission:
|
||||||
permissions = business_permission.business_permissions or {}
|
permissions = business_permission.business_permissions or {}
|
||||||
|
logger.info(f"User permissions: {permissions}")
|
||||||
|
else:
|
||||||
|
logger.info("No business permission found for user")
|
||||||
|
|
||||||
business_info = {
|
business_info = {
|
||||||
"id": business.id,
|
"id": business.id,
|
||||||
|
|
@ -281,13 +350,19 @@ def get_business_info_with_permissions(
|
||||||
"created_at": business.created_at.isoformat(),
|
"created_at": business.created_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_owner = ctx.is_business_owner(business_id)
|
||||||
|
has_access = ctx.can_access_business(business_id)
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
"business_info": business_info,
|
"business_info": business_info,
|
||||||
"user_permissions": permissions,
|
"user_permissions": permissions,
|
||||||
"is_owner": ctx.is_business_owner(business_id),
|
"is_owner": is_owner,
|
||||||
"role": "مالک" if ctx.is_business_owner(business_id) else "عضو",
|
"role": "مالک" if is_owner else "عضو",
|
||||||
"has_access": ctx.can_access_business(business_id)
|
"has_access": has_access
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info(f"Response data: {response_data}")
|
||||||
|
logger.info(f"=== get_business_info_with_permissions END ===")
|
||||||
|
|
||||||
formatted_data = format_datetime_fields(response_data, request)
|
formatted_data = format_datetime_fields(response_data, request)
|
||||||
return success_response(formatted_data, request)
|
return success_response(formatted_data, request)
|
||||||
|
|
|
||||||
414
hesabixAPI/adapters/api/v1/cash_registers.py
Normal file
414
hesabixAPI/adapters/api/v1/cash_registers.py
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
from fastapi import APIRouter, Depends, Request, Body
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from adapters.db.session import get_db
|
||||||
|
from app.core.auth_dependency import get_current_user, AuthContext
|
||||||
|
from app.core.responses import success_response, format_datetime_fields, ApiError
|
||||||
|
from app.core.permissions import require_business_management_dep, require_business_access
|
||||||
|
from adapters.api.v1.schemas import QueryInfo
|
||||||
|
from app.services.cash_register_service import (
|
||||||
|
create_cash_register,
|
||||||
|
update_cash_register,
|
||||||
|
delete_cash_register,
|
||||||
|
get_cash_register_by_id,
|
||||||
|
list_cash_registers,
|
||||||
|
bulk_delete_cash_registers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/cash-registers", tags=["cash-registers"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/businesses/{business_id}/cash-registers",
|
||||||
|
summary="لیست صندوقها",
|
||||||
|
description="دریافت لیست صندوقهای یک کسب و کار با امکان جستجو و فیلتر",
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
async def list_cash_registers_endpoint(
|
||||||
|
request: Request,
|
||||||
|
business_id: int,
|
||||||
|
query_info: QueryInfo,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
query_dict: Dict[str, Any] = {
|
||||||
|
"take": query_info.take,
|
||||||
|
"skip": query_info.skip,
|
||||||
|
"sort_by": query_info.sort_by,
|
||||||
|
"sort_desc": query_info.sort_desc,
|
||||||
|
"search": query_info.search,
|
||||||
|
"search_fields": query_info.search_fields,
|
||||||
|
"filters": query_info.filters,
|
||||||
|
}
|
||||||
|
result = list_cash_registers(db, business_id, query_dict)
|
||||||
|
result["items"] = [format_datetime_fields(item, request) for item in result.get("items", [])]
|
||||||
|
return success_response(data=result, request=request, message="CASH_REGISTERS_LIST_FETCHED")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/businesses/{business_id}/cash-registers/create",
|
||||||
|
summary="ایجاد صندوق جدید",
|
||||||
|
description="ایجاد صندوق برای یک کسبوکار مشخص",
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
async def create_cash_register_endpoint(
|
||||||
|
request: Request,
|
||||||
|
business_id: int,
|
||||||
|
body: Dict[str, Any] = Body(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
_: None = Depends(require_business_management_dep),
|
||||||
|
):
|
||||||
|
payload: Dict[str, Any] = dict(body or {})
|
||||||
|
created = create_cash_register(db, business_id, payload)
|
||||||
|
return success_response(data=format_datetime_fields(created, request), request=request, message="CASH_REGISTER_CREATED")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/cash-registers/{cash_id}",
|
||||||
|
summary="جزئیات صندوق",
|
||||||
|
description="دریافت جزئیات صندوق بر اساس شناسه",
|
||||||
|
)
|
||||||
|
async def get_cash_register_endpoint(
|
||||||
|
request: Request,
|
||||||
|
cash_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
_: None = Depends(require_business_management_dep),
|
||||||
|
):
|
||||||
|
result = get_cash_register_by_id(db, cash_id)
|
||||||
|
if not result:
|
||||||
|
raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404)
|
||||||
|
try:
|
||||||
|
biz_id = int(result.get("business_id"))
|
||||||
|
except Exception:
|
||||||
|
biz_id = None
|
||||||
|
if biz_id is not None and not ctx.can_access_business(biz_id):
|
||||||
|
raise ApiError("FORBIDDEN", "Access denied", http_status=403)
|
||||||
|
return success_response(data=format_datetime_fields(result, request), request=request, message="CASH_REGISTER_DETAILS")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/cash-registers/{cash_id}",
|
||||||
|
summary="ویرایش صندوق",
|
||||||
|
description="ویرایش اطلاعات صندوق",
|
||||||
|
)
|
||||||
|
async def update_cash_register_endpoint(
|
||||||
|
request: Request,
|
||||||
|
cash_id: int,
|
||||||
|
body: Dict[str, Any] = Body(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
_: None = Depends(require_business_management_dep),
|
||||||
|
):
|
||||||
|
payload: Dict[str, Any] = dict(body or {})
|
||||||
|
result = update_cash_register(db, cash_id, payload)
|
||||||
|
if result is None:
|
||||||
|
raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404)
|
||||||
|
try:
|
||||||
|
biz_id = int(result.get("business_id"))
|
||||||
|
except Exception:
|
||||||
|
biz_id = None
|
||||||
|
if biz_id is not None and not ctx.can_access_business(biz_id):
|
||||||
|
raise ApiError("FORBIDDEN", "Access denied", http_status=403)
|
||||||
|
return success_response(data=format_datetime_fields(result, request), request=request, message="CASH_REGISTER_UPDATED")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/cash-registers/{cash_id}",
|
||||||
|
summary="حذف صندوق",
|
||||||
|
description="حذف یک صندوق",
|
||||||
|
)
|
||||||
|
async def delete_cash_register_endpoint(
|
||||||
|
request: Request,
|
||||||
|
cash_id: int,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
_: None = Depends(require_business_management_dep),
|
||||||
|
):
|
||||||
|
result = get_cash_register_by_id(db, cash_id)
|
||||||
|
if result:
|
||||||
|
try:
|
||||||
|
biz_id = int(result.get("business_id"))
|
||||||
|
except Exception:
|
||||||
|
biz_id = None
|
||||||
|
if biz_id is not None and not ctx.can_access_business(biz_id):
|
||||||
|
raise ApiError("FORBIDDEN", "Access denied", http_status=403)
|
||||||
|
ok = delete_cash_register(db, cash_id)
|
||||||
|
if not ok:
|
||||||
|
raise ApiError("CASH_REGISTER_NOT_FOUND", "Cash register not found", http_status=404)
|
||||||
|
return success_response(data=None, request=request, message="CASH_REGISTER_DELETED")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/businesses/{business_id}/cash-registers/bulk-delete",
|
||||||
|
summary="حذف گروهی صندوقها",
|
||||||
|
description="حذف چندین صندوق بر اساس شناسهها",
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
async def bulk_delete_cash_registers_endpoint(
|
||||||
|
request: Request,
|
||||||
|
business_id: int,
|
||||||
|
body: Dict[str, Any] = Body(...),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
_: None = Depends(require_business_management_dep),
|
||||||
|
):
|
||||||
|
ids = body.get("ids")
|
||||||
|
if not isinstance(ids, list):
|
||||||
|
ids = []
|
||||||
|
try:
|
||||||
|
ids = [int(x) for x in ids if isinstance(x, (int, str)) and str(x).isdigit()]
|
||||||
|
except Exception:
|
||||||
|
ids = []
|
||||||
|
if not ids:
|
||||||
|
return success_response({"deleted": 0, "skipped": 0, "total_requested": 0}, request, message="NO_VALID_IDS_FOR_DELETE")
|
||||||
|
result = bulk_delete_cash_registers(db, business_id, ids)
|
||||||
|
return success_response(result, request, message="CASH_REGISTERS_BULK_DELETE_DONE")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/businesses/{business_id}/cash-registers/export/excel",
|
||||||
|
summary="خروجی Excel لیست صندوقها",
|
||||||
|
description="خروجی Excel لیست صندوقها با قابلیت فیلتر و مرتبسازی",
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
async def export_cash_registers_excel(
|
||||||
|
business_id: int,
|
||||||
|
request: Request,
|
||||||
|
body: Dict[str, Any] = Body(...),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
import io
|
||||||
|
from fastapi.responses import Response
|
||||||
|
from openpyxl import Workbook
|
||||||
|
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
||||||
|
from app.core.i18n import negotiate_locale
|
||||||
|
|
||||||
|
query_dict = {
|
||||||
|
"take": int(body.get("take", 1000)),
|
||||||
|
"skip": int(body.get("skip", 0)),
|
||||||
|
"sort_by": body.get("sort_by"),
|
||||||
|
"sort_desc": bool(body.get("sort_desc", False)),
|
||||||
|
"search": body.get("search"),
|
||||||
|
"search_fields": body.get("search_fields"),
|
||||||
|
"filters": body.get("filters"),
|
||||||
|
}
|
||||||
|
result = list_cash_registers(db, business_id, query_dict)
|
||||||
|
items: List[Dict[str, Any]] = result.get("items", [])
|
||||||
|
items = [format_datetime_fields(item, request) for item in items]
|
||||||
|
|
||||||
|
headers: List[str] = [
|
||||||
|
"code", "name", "currency", "is_active", "is_default",
|
||||||
|
"payment_switch_number", "payment_terminal_number", "merchant_id",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "CashRegisters"
|
||||||
|
|
||||||
|
locale = negotiate_locale(request.headers.get("Accept-Language"))
|
||||||
|
if locale == 'fa':
|
||||||
|
try:
|
||||||
|
ws.sheet_view.rightToLeft = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
header_font = Font(bold=True, color="FFFFFF")
|
||||||
|
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
|
||||||
|
header_alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
|
||||||
|
|
||||||
|
for col_idx, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.alignment = header_alignment
|
||||||
|
cell.border = border
|
||||||
|
|
||||||
|
for row_idx, item in enumerate(items, 2):
|
||||||
|
for col_idx, key in enumerate(headers, 1):
|
||||||
|
value = item.get(key, "")
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = ", ".join(str(v) for v in value)
|
||||||
|
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
|
cell.border = border
|
||||||
|
if locale == 'fa':
|
||||||
|
cell.alignment = Alignment(horizontal="right")
|
||||||
|
|
||||||
|
for column in ws.columns:
|
||||||
|
max_length = 0
|
||||||
|
column_letter = column[0].column_letter
|
||||||
|
for cell in column:
|
||||||
|
try:
|
||||||
|
if len(str(cell.value)) > max_length:
|
||||||
|
max_length = len(str(cell.value))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ws.column_dimensions[column_letter].width = min(max_length + 2, 50)
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
wb.save(buffer)
|
||||||
|
buffer.seek(0)
|
||||||
|
content = buffer.getvalue()
|
||||||
|
return Response(
|
||||||
|
content=content,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": "attachment; filename=cash_registers.xlsx",
|
||||||
|
"Content-Length": str(len(content)),
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/businesses/{business_id}/cash-registers/export/pdf",
|
||||||
|
summary="خروجی PDF لیست صندوقها",
|
||||||
|
description="خروجی PDF لیست صندوقها با قابلیت فیلتر و مرتبسازی",
|
||||||
|
)
|
||||||
|
@require_business_access("business_id")
|
||||||
|
async def export_cash_registers_pdf(
|
||||||
|
business_id: int,
|
||||||
|
request: Request,
|
||||||
|
body: Dict[str, Any] = Body(...),
|
||||||
|
ctx: AuthContext = Depends(get_current_user),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
from fastapi.responses import Response
|
||||||
|
from weasyprint import HTML
|
||||||
|
from weasyprint.text.fonts import FontConfiguration
|
||||||
|
from app.core.i18n import negotiate_locale
|
||||||
|
|
||||||
|
query_dict = {
|
||||||
|
"take": int(body.get("take", 1000)),
|
||||||
|
"skip": int(body.get("skip", 0)),
|
||||||
|
"sort_by": body.get("sort_by"),
|
||||||
|
"sort_desc": bool(body.get("sort_desc", False)),
|
||||||
|
"search": body.get("search"),
|
||||||
|
"search_fields": body.get("search_fields"),
|
||||||
|
"filters": body.get("filters"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result = list_cash_registers(db, business_id, query_dict)
|
||||||
|
items: List[Dict[str, Any]] = result.get("items", [])
|
||||||
|
items = [format_datetime_fields(item, request) for item in items]
|
||||||
|
|
||||||
|
selected_only = bool(body.get('selected_only', False))
|
||||||
|
selected_indices = body.get('selected_indices')
|
||||||
|
if selected_only and selected_indices is not None:
|
||||||
|
indices = None
|
||||||
|
if isinstance(selected_indices, str):
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
indices = json.loads(selected_indices)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
indices = None
|
||||||
|
elif isinstance(selected_indices, list):
|
||||||
|
indices = selected_indices
|
||||||
|
if isinstance(indices, list):
|
||||||
|
items = [items[i] for i in indices if isinstance(i, int) and 0 <= i < len(items)]
|
||||||
|
|
||||||
|
headers: List[str] = []
|
||||||
|
keys: List[str] = []
|
||||||
|
export_columns = body.get('export_columns')
|
||||||
|
if export_columns:
|
||||||
|
for col in export_columns:
|
||||||
|
key = col.get('key')
|
||||||
|
label = col.get('label', key)
|
||||||
|
if key:
|
||||||
|
keys.append(str(key))
|
||||||
|
headers.append(str(label))
|
||||||
|
else:
|
||||||
|
keys = [
|
||||||
|
"code", "name", "currency_id", "is_active", "is_default",
|
||||||
|
"payment_switch_number", "payment_terminal_number", "merchant_id",
|
||||||
|
"description",
|
||||||
|
]
|
||||||
|
headers = keys
|
||||||
|
|
||||||
|
business_name = ""
|
||||||
|
try:
|
||||||
|
from adapters.db.models.business import Business
|
||||||
|
biz = db.query(Business).filter(Business.id == business_id).first()
|
||||||
|
if biz is not None:
|
||||||
|
business_name = biz.name or ""
|
||||||
|
except Exception:
|
||||||
|
business_name = ""
|
||||||
|
|
||||||
|
locale = negotiate_locale(request.headers.get("Accept-Language"))
|
||||||
|
is_fa = (locale == 'fa')
|
||||||
|
html_lang = 'fa' if is_fa else 'en'
|
||||||
|
html_dir = 'rtl' if is_fa else 'ltr'
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.core.calendar import CalendarConverter
|
||||||
|
calendar_header = request.headers.get("X-Calendar-Type", "jalali").lower()
|
||||||
|
formatted_now = CalendarConverter.format_datetime(
|
||||||
|
__import__("datetime").datetime.now(),
|
||||||
|
"jalali" if calendar_header in ["jalali", "persian", "shamsi"] else "gregorian",
|
||||||
|
)
|
||||||
|
now_str = formatted_now.get('formatted', formatted_now.get('date_time', ''))
|
||||||
|
except Exception:
|
||||||
|
from datetime import datetime
|
||||||
|
now_str = datetime.now().strftime('%Y/%m/%d %H:%M')
|
||||||
|
|
||||||
|
def esc(v: Any) -> str:
|
||||||
|
try:
|
||||||
|
return str(v).replace('&', '&').replace('<', '<').replace('>', '>')
|
||||||
|
except Exception:
|
||||||
|
return str(v)
|
||||||
|
|
||||||
|
rows_html: List[str] = []
|
||||||
|
for item in items:
|
||||||
|
tds = []
|
||||||
|
for key in keys:
|
||||||
|
value = item.get(key)
|
||||||
|
if value is None:
|
||||||
|
value = ""
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = ", ".join(str(v) for v in value)
|
||||||
|
tds.append(f"<td>{esc(value)}</td>")
|
||||||
|
rows_html.append(f"<tr>{''.join(tds)}</tr>")
|
||||||
|
|
||||||
|
headers_html = ''.join(f"<th>{esc(h)}</th>" for h in headers)
|
||||||
|
|
||||||
|
table_html = f"""
|
||||||
|
<html lang="{html_lang}" dir="{html_dir}">
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<style>
|
||||||
|
@page {{ size: A4 landscape; margin: 12mm; }}
|
||||||
|
body {{ font-family: sans-serif; font-size: 11px; color: #222; }}
|
||||||
|
.title {{ font-size: 16px; font-weight: 700; margin-bottom: 10px; }}
|
||||||
|
table.report-table {{ width: 100%; border-collapse: collapse; table-layout: fixed; }}
|
||||||
|
thead th {{ background: #f0f3f7; border: 1px solid #c7cdd6; padding: 6px 4px; text-align: center; white-space: nowrap; }}
|
||||||
|
tbody td {{ border: 1px solid #d7dde6; padding: 5px 4px; vertical-align: top; overflow-wrap: anywhere; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="title">{esc('گزارش صندوقها' if is_fa else 'Cash Registers Report')}</div>
|
||||||
|
<div style="margin-bottom:6px;">{esc('نام کسبوکار' if is_fa else 'Business Name')}: {esc(business_name)} | {esc('تاریخ گزارش' if is_fa else 'Report Date')}: {esc(now_str)}</div>
|
||||||
|
<table class="report-table">
|
||||||
|
<thead><tr>{headers_html}</tr></thead>
|
||||||
|
<tbody>{''.join(rows_html)}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
font_config = FontConfiguration()
|
||||||
|
pdf_bytes = HTML(string=table_html).write_pdf(font_config=font_config)
|
||||||
|
return Response(
|
||||||
|
content=pdf_bytes,
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": "attachment; filename=cash_registers.pdf",
|
||||||
|
"Content-Length": str(len(pdf_bytes)),
|
||||||
|
"Access-Control-Expose-Headers": "Content-Disposition",
|
||||||
|
},
|
||||||
|
)
|
||||||
44
hesabixAPI/adapters/db/models/cash_register.py
Normal file
44
hesabixAPI/adapters/db/models/cash_register.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import String, Integer, DateTime, ForeignKey, UniqueConstraint, Boolean
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from adapters.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
class CashRegister(Base):
|
||||||
|
__tablename__ = "cash_registers"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('business_id', 'code', name='uq_cash_registers_business_code'),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||||
|
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||||
|
|
||||||
|
# مشخصات
|
||||||
|
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="نام صندوق")
|
||||||
|
code: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True, comment="کد یکتا در هر کسبوکار (اختیاری)")
|
||||||
|
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||||
|
|
||||||
|
# تنظیمات
|
||||||
|
currency_id: Mapped[int] = mapped_column(Integer, ForeignKey("currencies.id", ondelete="RESTRICT"), nullable=False, index=True)
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, server_default="1")
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="0")
|
||||||
|
|
||||||
|
# پرداخت
|
||||||
|
payment_switch_number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
payment_terminal_number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
merchant_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
|
|
||||||
|
# زمان بندی
|
||||||
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
# روابط
|
||||||
|
business = relationship("Business", backref="cash_registers")
|
||||||
|
currency = relationship("Currency", backref="cash_registers")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,13 +15,31 @@ class BusinessPermissionRepository(BaseRepository[BusinessPermission]):
|
||||||
|
|
||||||
def get_by_user_and_business(self, user_id: int, business_id: int) -> Optional[BusinessPermission]:
|
def get_by_user_and_business(self, user_id: int, business_id: int) -> Optional[BusinessPermission]:
|
||||||
"""دریافت دسترسیهای کاربر برای کسب و کار خاص"""
|
"""دریافت دسترسیهای کاربر برای کسب و کار خاص"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"=== get_by_user_and_business START ===")
|
||||||
|
logger.info(f"User ID: {user_id}")
|
||||||
|
logger.info(f"Business ID: {business_id}")
|
||||||
|
|
||||||
stmt = select(BusinessPermission).where(
|
stmt = select(BusinessPermission).where(
|
||||||
and_(
|
and_(
|
||||||
BusinessPermission.user_id == user_id,
|
BusinessPermission.user_id == user_id,
|
||||||
BusinessPermission.business_id == business_id
|
BusinessPermission.business_id == business_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return self.db.execute(stmt).scalars().first()
|
|
||||||
|
logger.info(f"SQL Query: {stmt}")
|
||||||
|
|
||||||
|
result = self.db.execute(stmt).scalars().first()
|
||||||
|
|
||||||
|
logger.info(f"Query result: {result}")
|
||||||
|
if result:
|
||||||
|
logger.info(f"Business permissions: {result.business_permissions}")
|
||||||
|
logger.info(f"Type: {type(result.business_permissions)}")
|
||||||
|
|
||||||
|
logger.info(f"=== get_by_user_and_business END ===")
|
||||||
|
return result
|
||||||
|
|
||||||
def create_or_update(self, user_id: int, business_id: int, permissions: dict) -> BusinessPermission:
|
def create_or_update(self, user_id: int, business_id: int, permissions: dict) -> BusinessPermission:
|
||||||
"""ایجاد یا بهروزرسانی دسترسیهای کاربر برای کسب و کار"""
|
"""ایجاد یا بهروزرسانی دسترسیهای کاربر برای کسب و کار"""
|
||||||
|
|
|
||||||
125
hesabixAPI/adapters/db/repositories/cash_register_repository.py
Normal file
125
hesabixAPI/adapters/db/repositories/cash_register_repository.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, or_, func
|
||||||
|
|
||||||
|
from adapters.db.models.cash_register import CashRegister
|
||||||
|
|
||||||
|
|
||||||
|
class CashRegisterRepository:
|
||||||
|
def __init__(self, db: Session) -> None:
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
def create(self, business_id: int, data: Dict[str, Any]) -> CashRegister:
|
||||||
|
obj = CashRegister(
|
||||||
|
business_id=business_id,
|
||||||
|
name=data.get("name"),
|
||||||
|
code=data.get("code"),
|
||||||
|
description=data.get("description"),
|
||||||
|
currency_id=int(data["currency_id"]),
|
||||||
|
is_active=bool(data.get("is_active", True)),
|
||||||
|
is_default=bool(data.get("is_default", False)),
|
||||||
|
payment_switch_number=data.get("payment_switch_number"),
|
||||||
|
payment_terminal_number=data.get("payment_terminal_number"),
|
||||||
|
merchant_id=data.get("merchant_id"),
|
||||||
|
)
|
||||||
|
self.db.add(obj)
|
||||||
|
self.db.flush()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_by_id(self, id_: int) -> Optional[CashRegister]:
|
||||||
|
return self.db.query(CashRegister).filter(CashRegister.id == id_).first()
|
||||||
|
|
||||||
|
def update(self, obj: CashRegister, data: Dict[str, Any]) -> CashRegister:
|
||||||
|
for key in [
|
||||||
|
"name","code","description","currency_id","is_active","is_default",
|
||||||
|
"payment_switch_number","payment_terminal_number","merchant_id",
|
||||||
|
]:
|
||||||
|
if key in data and data[key] is not None:
|
||||||
|
setattr(obj, key, data[key] if key != "currency_id" else int(data[key]))
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def delete(self, obj: CashRegister) -> None:
|
||||||
|
self.db.delete(obj)
|
||||||
|
|
||||||
|
def bulk_delete(self, business_id: int, ids: List[int]) -> Dict[str, int]:
|
||||||
|
items = self.db.query(CashRegister).filter(
|
||||||
|
CashRegister.business_id == business_id,
|
||||||
|
CashRegister.id.in_(ids)
|
||||||
|
).all()
|
||||||
|
deleted = 0
|
||||||
|
skipped = 0
|
||||||
|
for it in items:
|
||||||
|
try:
|
||||||
|
self.db.delete(it)
|
||||||
|
deleted += 1
|
||||||
|
except Exception:
|
||||||
|
skipped += 1
|
||||||
|
return {"deleted": deleted, "skipped": skipped, "total_requested": len(ids)}
|
||||||
|
|
||||||
|
def clear_default(self, business_id: int, except_id: Optional[int] = None) -> None:
|
||||||
|
q = self.db.query(CashRegister).filter(CashRegister.business_id == business_id)
|
||||||
|
if except_id is not None:
|
||||||
|
q = q.filter(CashRegister.id != except_id)
|
||||||
|
q.update({CashRegister.is_default: False})
|
||||||
|
|
||||||
|
def list(self, business_id: int, query: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
q = self.db.query(CashRegister).filter(CashRegister.business_id == business_id)
|
||||||
|
|
||||||
|
# search
|
||||||
|
search = query.get("search")
|
||||||
|
search_fields = query.get("search_fields") or []
|
||||||
|
if search and search_fields:
|
||||||
|
term = f"%{search}%"
|
||||||
|
conditions = []
|
||||||
|
for f in search_fields:
|
||||||
|
if f == "name":
|
||||||
|
conditions.append(CashRegister.name.ilike(term))
|
||||||
|
if f == "code":
|
||||||
|
conditions.append(CashRegister.code.ilike(term))
|
||||||
|
elif f == "description":
|
||||||
|
conditions.append(CashRegister.description.ilike(term))
|
||||||
|
elif f in {"payment_switch_number","payment_terminal_number","merchant_id"}:
|
||||||
|
conditions.append(getattr(CashRegister, f).ilike(term))
|
||||||
|
if conditions:
|
||||||
|
q = q.filter(or_(*conditions))
|
||||||
|
|
||||||
|
# filters
|
||||||
|
for flt in (query.get("filters") or []):
|
||||||
|
prop = flt.get("property")
|
||||||
|
op = flt.get("operator")
|
||||||
|
val = flt.get("value")
|
||||||
|
if not prop or not op:
|
||||||
|
continue
|
||||||
|
if prop in {"is_active","is_default"} and op == "=":
|
||||||
|
q = q.filter(getattr(CashRegister, prop) == val)
|
||||||
|
elif prop == "currency_id" and op == "=":
|
||||||
|
q = q.filter(CashRegister.currency_id == val)
|
||||||
|
|
||||||
|
# sort
|
||||||
|
sort_by = query.get("sort_by") or "created_at"
|
||||||
|
sort_desc = bool(query.get("sort_desc", True))
|
||||||
|
col = getattr(CashRegister, sort_by, CashRegister.created_at)
|
||||||
|
q = q.order_by(col.desc() if sort_desc else col.asc())
|
||||||
|
|
||||||
|
# pagination
|
||||||
|
skip = int(query.get("skip", 0))
|
||||||
|
take = int(query.get("take", 20))
|
||||||
|
total = q.count()
|
||||||
|
items = q.offset(skip).limit(take).all()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"pagination": {
|
||||||
|
"total": total,
|
||||||
|
"page": (skip // take) + 1,
|
||||||
|
"per_page": take,
|
||||||
|
"total_pages": (total + take - 1) // take,
|
||||||
|
"has_next": skip + take < total,
|
||||||
|
"has_prev": skip > 0,
|
||||||
|
},
|
||||||
|
"query_info": query,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -48,21 +48,43 @@ class AuthContext:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_permissions_value(value) -> dict:
|
def _normalize_permissions_value(value) -> dict:
|
||||||
"""نرمالسازی مقدار JSON دسترسیها به dict برای سازگاری با دادههای legacy"""
|
"""نرمالسازی مقدار JSON دسترسیها به dict برای سازگاری با دادههای legacy"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"=== _normalize_permissions_value START ===")
|
||||||
|
logger.info(f"Input value type: {type(value)}")
|
||||||
|
logger.info(f"Input value: {value}")
|
||||||
|
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
|
logger.info("Value is already a dict, returning as is")
|
||||||
|
logger.info(f"=== _normalize_permissions_value END ===")
|
||||||
return value
|
return value
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
|
logger.info("Value is a list, processing...")
|
||||||
try:
|
try:
|
||||||
# لیست جفتها مانند [["join", true], ["sales", {..}]]
|
# لیست جفتها مانند [["join", true], ["sales", {..}]]
|
||||||
if all(isinstance(item, list) and len(item) == 2 for item in value):
|
if all(isinstance(item, list) and len(item) == 2 for item in value):
|
||||||
return {k: v for k, v in value if isinstance(k, str)}
|
logger.info("Detected list of key-value pairs")
|
||||||
|
result = {k: v for k, v in value if isinstance(k, str)}
|
||||||
|
logger.info(f"Converted to dict: {result}")
|
||||||
|
logger.info(f"=== _normalize_permissions_value END ===")
|
||||||
|
return result
|
||||||
# لیست دیکشنریها مانند [{"join": true}, {"sales": {...}}]
|
# لیست دیکشنریها مانند [{"join": true}, {"sales": {...}}]
|
||||||
if all(isinstance(item, dict) for item in value):
|
if all(isinstance(item, dict) for item in value):
|
||||||
|
logger.info("Detected list of dictionaries")
|
||||||
merged = {}
|
merged = {}
|
||||||
for item in value:
|
for item in value:
|
||||||
merged.update({k: v for k, v in item.items()})
|
merged.update({k: v for k, v in item.items()})
|
||||||
|
logger.info(f"Merged to dict: {merged}")
|
||||||
|
logger.info(f"=== _normalize_permissions_value END ===")
|
||||||
return merged
|
return merged
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing list: {e}")
|
||||||
|
logger.info(f"=== _normalize_permissions_value END ===")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
logger.info(f"Unsupported value type {type(value)}, returning empty dict")
|
||||||
|
logger.info(f"=== _normalize_permissions_value END ===")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_translator(self) -> Translator:
|
def get_translator(self) -> Translator:
|
||||||
|
|
@ -101,15 +123,34 @@ class AuthContext:
|
||||||
|
|
||||||
def _get_business_permissions(self) -> dict:
|
def _get_business_permissions(self) -> dict:
|
||||||
"""دریافت دسترسیهای کسب و کار از دیتابیس"""
|
"""دریافت دسترسیهای کسب و کار از دیتابیس"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"=== _get_business_permissions START ===")
|
||||||
|
logger.info(f"User ID: {self.user.id}")
|
||||||
|
logger.info(f"Business ID: {self.business_id}")
|
||||||
|
logger.info(f"DB available: {self.db is not None}")
|
||||||
|
|
||||||
if not self.business_id or not self.db:
|
if not self.business_id or not self.db:
|
||||||
|
logger.info("No business_id or db, returning empty permissions")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||||
repo = BusinessPermissionRepository(self.db)
|
repo = BusinessPermissionRepository(self.db)
|
||||||
permission_obj = repo.get_by_user_and_business(self.user.id, self.business_id)
|
permission_obj = repo.get_by_user_and_business(self.user.id, self.business_id)
|
||||||
|
|
||||||
|
logger.info(f"Permission object found: {permission_obj}")
|
||||||
|
|
||||||
if permission_obj and permission_obj.business_permissions:
|
if permission_obj and permission_obj.business_permissions:
|
||||||
return AuthContext._normalize_permissions_value(permission_obj.business_permissions)
|
raw_permissions = permission_obj.business_permissions
|
||||||
|
logger.info(f"Raw permissions: {raw_permissions}")
|
||||||
|
normalized_permissions = AuthContext._normalize_permissions_value(raw_permissions)
|
||||||
|
logger.info(f"Normalized permissions: {normalized_permissions}")
|
||||||
|
logger.info(f"=== _get_business_permissions END ===")
|
||||||
|
return normalized_permissions
|
||||||
|
|
||||||
|
logger.info("No permissions found, returning empty dict")
|
||||||
|
logger.info(f"=== _get_business_permissions END ===")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# بررسی دسترسیهای اپلیکیشن
|
# بررسی دسترسیهای اپلیکیشن
|
||||||
|
|
@ -146,15 +187,33 @@ class AuthContext:
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"=== is_business_owner START ===")
|
||||||
|
logger.info(f"Requested business_id: {business_id}")
|
||||||
|
logger.info(f"Context business_id: {self.business_id}")
|
||||||
|
logger.info(f"User ID: {self.user.id}")
|
||||||
|
logger.info(f"DB available: {self.db is not None}")
|
||||||
|
|
||||||
target_business_id = business_id or self.business_id
|
target_business_id = business_id or self.business_id
|
||||||
|
logger.info(f"Target business_id: {target_business_id}")
|
||||||
|
|
||||||
if not target_business_id or not self.db:
|
if not target_business_id or not self.db:
|
||||||
logger.info(f"is_business_owner: no business_id ({target_business_id}) or db ({self.db is not None})")
|
logger.info(f"is_business_owner: no business_id ({target_business_id}) or db ({self.db is not None})")
|
||||||
|
logger.info(f"=== is_business_owner END (no business_id or db) ===")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
from adapters.db.models.business import Business
|
from adapters.db.models.business import Business
|
||||||
business = self.db.get(Business, target_business_id)
|
business = self.db.get(Business, target_business_id)
|
||||||
is_owner = business and business.owner_id == self.user.id
|
logger.info(f"Business lookup result: {business}")
|
||||||
logger.info(f"is_business_owner: business_id={target_business_id}, business={business}, owner_id={business.owner_id if business else None}, user_id={self.user.id}, is_owner={is_owner}")
|
|
||||||
|
if business:
|
||||||
|
logger.info(f"Business owner_id: {business.owner_id}")
|
||||||
|
is_owner = business.owner_id == self.user.id
|
||||||
|
logger.info(f"is_owner: {is_owner}")
|
||||||
|
else:
|
||||||
|
logger.info("Business not found")
|
||||||
|
is_owner = False
|
||||||
|
|
||||||
|
logger.info(f"=== is_business_owner END (result: {is_owner}) ===")
|
||||||
return is_owner
|
return is_owner
|
||||||
|
|
||||||
# بررسی دسترسیهای کسب و کار
|
# بررسی دسترسیهای کسب و کار
|
||||||
|
|
@ -250,22 +309,66 @@ class AuthContext:
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
logger.info(f"Checking business access: user {self.user.id}, business {business_id}, context business_id {self.business_id}")
|
logger.info(f"=== can_access_business START ===")
|
||||||
|
logger.info(f"User ID: {self.user.id}")
|
||||||
|
logger.info(f"Requested business ID: {business_id}")
|
||||||
|
logger.info(f"User context business_id: {self.business_id}")
|
||||||
|
logger.info(f"User app permissions: {self.app_permissions}")
|
||||||
|
|
||||||
# SuperAdmin دسترسی به همه کسب و کارها دارد
|
# SuperAdmin دسترسی به همه کسب و کارها دارد
|
||||||
if self.is_superadmin():
|
if self.is_superadmin():
|
||||||
logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}")
|
logger.info(f"User {self.user.id} is superadmin, granting access to business {business_id}")
|
||||||
|
logger.info(f"=== can_access_business END (superadmin) ===")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# اگر مالک کسب و کار است، دسترسی دارد
|
# بررسی مالکیت کسب و کار
|
||||||
if self.is_business_owner() and business_id == self.business_id:
|
if self.db:
|
||||||
logger.info(f"User {self.user.id} is business owner of {business_id}, granting access")
|
from adapters.db.models.business import Business
|
||||||
|
business = self.db.get(Business, business_id)
|
||||||
|
logger.info(f"Business lookup result: {business}")
|
||||||
|
if business:
|
||||||
|
logger.info(f"Business owner ID: {business.owner_id}")
|
||||||
|
if business.owner_id == self.user.id:
|
||||||
|
logger.info(f"User {self.user.id} is business owner of {business_id}, granting access")
|
||||||
|
logger.info(f"=== can_access_business END (owner) ===")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info("No database connection available for business lookup")
|
||||||
|
|
||||||
|
# بررسی عضویت در کسب و کار
|
||||||
|
if self.db:
|
||||||
|
from adapters.db.repositories.business_permission_repo import BusinessPermissionRepository
|
||||||
|
permission_repo = BusinessPermissionRepository(self.db)
|
||||||
|
business_permission = permission_repo.get_by_user_and_business(self.user.id, business_id)
|
||||||
|
logger.info(f"Business permission lookup result: {business_permission}")
|
||||||
|
|
||||||
|
if business_permission:
|
||||||
|
# بررسی دسترسی join
|
||||||
|
permissions = business_permission.business_permissions or {}
|
||||||
|
logger.info(f"User permissions for business {business_id}: {permissions}")
|
||||||
|
join_permission = permissions.get('join')
|
||||||
|
logger.info(f"Join permission: {join_permission}")
|
||||||
|
|
||||||
|
if join_permission == True:
|
||||||
|
logger.info(f"User {self.user.id} is member of business {business_id}, granting access")
|
||||||
|
logger.info(f"=== can_access_business END (member) ===")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"User {self.user.id} does not have join permission for business {business_id}")
|
||||||
|
else:
|
||||||
|
logger.info(f"No business permission found for user {self.user.id} and business {business_id}")
|
||||||
|
else:
|
||||||
|
logger.info("No database connection available for permission lookup")
|
||||||
|
|
||||||
|
# اگر کسب و کار در context کاربر است، دسترسی دارد
|
||||||
|
if business_id == self.business_id:
|
||||||
|
logger.info(f"User {self.user.id} has context access to business {business_id}")
|
||||||
|
logger.info(f"=== can_access_business END (context) ===")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# بررسی دسترسیهای کسب و کار
|
logger.info(f"User {self.user.id} does not have access to business {business_id}")
|
||||||
has_access = business_id == self.business_id
|
logger.info(f"=== can_access_business END (denied) ===")
|
||||||
logger.info(f"Business access check: {business_id} == {self.business_id} = {has_access}")
|
return False
|
||||||
return has_access
|
|
||||||
|
|
||||||
def is_business_member(self, business_id: int) -> bool:
|
def is_business_member(self, business_id: int) -> bool:
|
||||||
"""بررسی اینکه آیا کاربر عضو کسب و کار است یا نه (دسترسی join)"""
|
"""بررسی اینکه آیا کاربر عضو کسب و کار است یا نه (دسترسی join)"""
|
||||||
|
|
@ -378,7 +481,15 @@ def get_current_user(
|
||||||
# تشخیص سال مالی از هدر X-Fiscal-Year-ID (آینده)
|
# تشخیص سال مالی از هدر X-Fiscal-Year-ID (آینده)
|
||||||
fiscal_year_id = _detect_fiscal_year_id(request)
|
fiscal_year_id = _detect_fiscal_year_id(request)
|
||||||
|
|
||||||
return AuthContext(
|
logger.info(f"Creating AuthContext for user {user.id}:")
|
||||||
|
logger.info(f" - Language: {language}")
|
||||||
|
logger.info(f" - Calendar type: {calendar_type}")
|
||||||
|
logger.info(f" - Timezone: {timezone}")
|
||||||
|
logger.info(f" - Business ID: {business_id}")
|
||||||
|
logger.info(f" - Fiscal year ID: {fiscal_year_id}")
|
||||||
|
logger.info(f" - App permissions: {user.app_permissions}")
|
||||||
|
|
||||||
|
auth_context = AuthContext(
|
||||||
user=user,
|
user=user,
|
||||||
api_key_id=obj.id,
|
api_key_id=obj.id,
|
||||||
language=language,
|
language=language,
|
||||||
|
|
@ -388,6 +499,9 @@ def get_current_user(
|
||||||
fiscal_year_id=fiscal_year_id,
|
fiscal_year_id=fiscal_year_id,
|
||||||
db=db
|
db=db
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info(f"AuthContext created successfully")
|
||||||
|
return auth_context
|
||||||
|
|
||||||
|
|
||||||
def _detect_language(request: Request) -> str:
|
def _detect_language(request: Request) -> str:
|
||||||
|
|
@ -409,12 +523,22 @@ def _detect_timezone(request: Request) -> Optional[str]:
|
||||||
|
|
||||||
def _detect_business_id(request: Request) -> Optional[int]:
|
def _detect_business_id(request: Request) -> Optional[int]:
|
||||||
"""تشخیص ID کسب و کار از هدر X-Business-ID (آینده)"""
|
"""تشخیص ID کسب و کار از هدر X-Business-ID (آینده)"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
business_id_str = request.headers.get("X-Business-ID")
|
business_id_str = request.headers.get("X-Business-ID")
|
||||||
|
logger.info(f"X-Business-ID header: {business_id_str}")
|
||||||
|
|
||||||
if business_id_str:
|
if business_id_str:
|
||||||
try:
|
try:
|
||||||
return int(business_id_str)
|
business_id = int(business_id_str)
|
||||||
|
logger.info(f"Detected business ID: {business_id}")
|
||||||
|
return business_id
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logger.warning(f"Invalid business ID format: {business_id_str}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
logger.info("No business ID detected from headers")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,22 @@ def require_business_access(business_id_param: str = "business_id"):
|
||||||
except Exception:
|
except Exception:
|
||||||
business_id = None
|
business_id = None
|
||||||
|
|
||||||
if business_id and not ctx.can_access_business(int(business_id)):
|
if business_id:
|
||||||
logger.warning(f"User {ctx.get_user_id()} does not have access to business {business_id}")
|
logger.info(f"=== require_business_access decorator ===")
|
||||||
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
|
logger.info(f"Checking access for user {ctx.get_user_id()} to business {business_id}")
|
||||||
|
logger.info(f"User context business_id: {ctx.business_id}")
|
||||||
|
logger.info(f"Is superadmin: {ctx.is_superadmin()}")
|
||||||
|
|
||||||
|
has_access = ctx.can_access_business(int(business_id))
|
||||||
|
logger.info(f"Access check result: {has_access}")
|
||||||
|
|
||||||
|
if not has_access:
|
||||||
|
logger.warning(f"User {ctx.get_user_id()} does not have access to business {business_id}")
|
||||||
|
raise ApiError("FORBIDDEN", f"No access to business {business_id}", http_status=403)
|
||||||
|
else:
|
||||||
|
logger.info(f"User {ctx.get_user_id()} has access to business {business_id}")
|
||||||
|
else:
|
||||||
|
logger.info("No business_id provided, skipping access check")
|
||||||
|
|
||||||
# فراخوانی تابع اصلی و await در صورت نیاز
|
# فراخوانی تابع اصلی و await در صورت نیاز
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ 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.price_lists import router as price_lists_router
|
||||||
from adapters.api.v1.persons import router as persons_router
|
from adapters.api.v1.persons import router as persons_router
|
||||||
from adapters.api.v1.bank_accounts import router as bank_accounts_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.tax_units import router as tax_units_router
|
from adapters.api.v1.tax_units import router as tax_units_router
|
||||||
from adapters.api.v1.tax_units import alias_router as units_alias_router
|
from adapters.api.v1.tax_units import alias_router as units_alias_router
|
||||||
from adapters.api.v1.tax_types import router as tax_types_router
|
from adapters.api.v1.tax_types import router as tax_types_router
|
||||||
|
|
@ -294,6 +295,7 @@ def create_app() -> FastAPI:
|
||||||
application.include_router(price_lists_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(persons_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(bank_accounts_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(tax_units_router, prefix=settings.api_v1_prefix)
|
application.include_router(tax_units_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(units_alias_router, prefix=settings.api_v1_prefix)
|
application.include_router(units_alias_router, prefix=settings.api_v1_prefix)
|
||||||
application.include_router(tax_types_router, prefix=settings.api_v1_prefix)
|
application.include_router(tax_types_router, prefix=settings.api_v1_prefix)
|
||||||
|
|
|
||||||
134
hesabixAPI/app/services/cash_register_service.py
Normal file
134
hesabixAPI/app/services/cash_register_service.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, func
|
||||||
|
|
||||||
|
from adapters.db.models.cash_register import CashRegister
|
||||||
|
from adapters.db.repositories.cash_register_repository import CashRegisterRepository
|
||||||
|
from app.core.responses import ApiError
|
||||||
|
|
||||||
|
|
||||||
|
def create_cash_register(db: Session, business_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
# code uniqueness in business if provided; else auto-generate numeric min 3 digits
|
||||||
|
code = data.get("code")
|
||||||
|
if code is not None and str(code).strip() != "":
|
||||||
|
if not str(code).isdigit():
|
||||||
|
raise ApiError("INVALID_CASH_CODE", "Cash register code must be numeric", http_status=400)
|
||||||
|
if len(str(code)) < 3:
|
||||||
|
raise ApiError("INVALID_CASH_CODE", "Cash register code must be at least 3 digits", http_status=400)
|
||||||
|
exists = db.query(CashRegister).filter(and_(CashRegister.business_id == business_id, CashRegister.code == str(code))).first()
|
||||||
|
if exists:
|
||||||
|
raise ApiError("DUPLICATE_CASH_CODE", "Duplicate cash register code", http_status=400)
|
||||||
|
else:
|
||||||
|
max_code = db.query(func.max(CashRegister.code)).filter(CashRegister.business_id == business_id).scalar()
|
||||||
|
try:
|
||||||
|
if max_code is not None and str(max_code).isdigit():
|
||||||
|
next_code_int = int(max_code) + 1
|
||||||
|
else:
|
||||||
|
next_code_int = 100
|
||||||
|
if next_code_int < 100:
|
||||||
|
next_code_int = 100
|
||||||
|
code = str(next_code_int)
|
||||||
|
except Exception:
|
||||||
|
code = "100"
|
||||||
|
|
||||||
|
repo = CashRegisterRepository(db)
|
||||||
|
obj = repo.create(business_id, {
|
||||||
|
"name": data.get("name"),
|
||||||
|
"code": code,
|
||||||
|
"description": data.get("description"),
|
||||||
|
"currency_id": int(data["currency_id"]),
|
||||||
|
"is_active": bool(data.get("is_active", True)),
|
||||||
|
"is_default": bool(data.get("is_default", False)),
|
||||||
|
"payment_switch_number": data.get("payment_switch_number"),
|
||||||
|
"payment_terminal_number": data.get("payment_terminal_number"),
|
||||||
|
"merchant_id": data.get("merchant_id"),
|
||||||
|
})
|
||||||
|
|
||||||
|
# ensure single default
|
||||||
|
if obj.is_default:
|
||||||
|
repo.clear_default(business_id, except_id=obj.id)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return cash_register_to_dict(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cash_register_by_id(db: Session, id_: int) -> Optional[Dict[str, Any]]:
|
||||||
|
obj = db.query(CashRegister).filter(CashRegister.id == id_).first()
|
||||||
|
return cash_register_to_dict(obj) if obj else None
|
||||||
|
|
||||||
|
|
||||||
|
def update_cash_register(db: Session, id_: int, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
repo = CashRegisterRepository(db)
|
||||||
|
obj = repo.get_by_id(id_)
|
||||||
|
if obj is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "code" in data and data["code"] is not None and str(data["code"]).strip() != "":
|
||||||
|
if not str(data["code"]).isdigit():
|
||||||
|
raise ApiError("INVALID_CASH_CODE", "Cash register code must be numeric", http_status=400)
|
||||||
|
if len(str(data["code"])) < 3:
|
||||||
|
raise ApiError("INVALID_CASH_CODE", "Cash register code must be at least 3 digits", http_status=400)
|
||||||
|
exists = db.query(CashRegister).filter(and_(CashRegister.business_id == obj.business_id, CashRegister.code == str(data["code"]), CashRegister.id != obj.id)).first()
|
||||||
|
if exists:
|
||||||
|
raise ApiError("DUPLICATE_CASH_CODE", "Duplicate cash register code", http_status=400)
|
||||||
|
|
||||||
|
repo.update(obj, data)
|
||||||
|
if obj.is_default:
|
||||||
|
repo.clear_default(obj.business_id, except_id=obj.id)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(obj)
|
||||||
|
return cash_register_to_dict(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_cash_register(db: Session, id_: int) -> bool:
|
||||||
|
obj = db.query(CashRegister).filter(CashRegister.id == id_).first()
|
||||||
|
if obj is None:
|
||||||
|
return False
|
||||||
|
db.delete(obj)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def bulk_delete_cash_registers(db: Session, business_id: int, ids: List[int]) -> Dict[str, Any]:
|
||||||
|
repo = CashRegisterRepository(db)
|
||||||
|
result = repo.bulk_delete(business_id, ids)
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise ApiError("BULK_DELETE_FAILED", "Bulk delete failed for cash registers", http_status=500)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_cash_registers(db: Session, business_id: int, query: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
repo = CashRegisterRepository(db)
|
||||||
|
res = repo.list(business_id, query)
|
||||||
|
return {
|
||||||
|
"items": [cash_register_to_dict(i) for i in res["items"]],
|
||||||
|
"pagination": res["pagination"],
|
||||||
|
"query_info": res["query_info"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def cash_register_to_dict(obj: CashRegister) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": obj.id,
|
||||||
|
"business_id": obj.business_id,
|
||||||
|
"name": obj.name,
|
||||||
|
"code": obj.code,
|
||||||
|
"description": obj.description,
|
||||||
|
"currency_id": obj.currency_id,
|
||||||
|
"is_active": bool(obj.is_active),
|
||||||
|
"is_default": bool(obj.is_default),
|
||||||
|
"payment_switch_number": obj.payment_switch_number,
|
||||||
|
"payment_terminal_number": obj.payment_terminal_number,
|
||||||
|
"merchant_id": obj.merchant_id,
|
||||||
|
"created_at": obj.created_at.isoformat(),
|
||||||
|
"updated_at": obj.updated_at.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
0
hesabixAPI/hesabix.db
Normal file
0
hesabixAPI/hesabix.db
Normal file
|
|
@ -9,6 +9,7 @@ adapters/api/v1/bank_accounts.py
|
||||||
adapters/api/v1/business_dashboard.py
|
adapters/api/v1/business_dashboard.py
|
||||||
adapters/api/v1/business_users.py
|
adapters/api/v1/business_users.py
|
||||||
adapters/api/v1/businesses.py
|
adapters/api/v1/businesses.py
|
||||||
|
adapters/api/v1/cash_registers.py
|
||||||
adapters/api/v1/categories.py
|
adapters/api/v1/categories.py
|
||||||
adapters/api/v1/currencies.py
|
adapters/api/v1/currencies.py
|
||||||
adapters/api/v1/health.py
|
adapters/api/v1/health.py
|
||||||
|
|
@ -47,6 +48,7 @@ adapters/db/models/bank_account.py
|
||||||
adapters/db/models/business.py
|
adapters/db/models/business.py
|
||||||
adapters/db/models/business_permission.py
|
adapters/db/models/business_permission.py
|
||||||
adapters/db/models/captcha.py
|
adapters/db/models/captcha.py
|
||||||
|
adapters/db/models/cash_register.py
|
||||||
adapters/db/models/category.py
|
adapters/db/models/category.py
|
||||||
adapters/db/models/currency.py
|
adapters/db/models/currency.py
|
||||||
adapters/db/models/document.py
|
adapters/db/models/document.py
|
||||||
|
|
@ -72,6 +74,7 @@ adapters/db/repositories/api_key_repo.py
|
||||||
adapters/db/repositories/base_repo.py
|
adapters/db/repositories/base_repo.py
|
||||||
adapters/db/repositories/business_permission_repo.py
|
adapters/db/repositories/business_permission_repo.py
|
||||||
adapters/db/repositories/business_repo.py
|
adapters/db/repositories/business_repo.py
|
||||||
|
adapters/db/repositories/cash_register_repository.py
|
||||||
adapters/db/repositories/category_repository.py
|
adapters/db/repositories/category_repository.py
|
||||||
adapters/db/repositories/email_config_repository.py
|
adapters/db/repositories/email_config_repository.py
|
||||||
adapters/db/repositories/file_storage_repository.py
|
adapters/db/repositories/file_storage_repository.py
|
||||||
|
|
@ -109,6 +112,7 @@ app/services/bulk_price_update_service.py
|
||||||
app/services/business_dashboard_service.py
|
app/services/business_dashboard_service.py
|
||||||
app/services/business_service.py
|
app/services/business_service.py
|
||||||
app/services/captcha_service.py
|
app/services/captcha_service.py
|
||||||
|
app/services/cash_register_service.py
|
||||||
app/services/email_service.py
|
app/services/email_service.py
|
||||||
app/services/file_storage_service.py
|
app/services/file_storage_service.py
|
||||||
app/services/person_service.py
|
app/services/person_service.py
|
||||||
|
|
@ -162,9 +166,11 @@ migrations/versions/20251001_000601_update_price_items_currency_unique_not_null.
|
||||||
migrations/versions/20251001_001101_drop_price_list_currency_default_unit.py
|
migrations/versions/20251001_001101_drop_price_list_currency_default_unit.py
|
||||||
migrations/versions/20251001_001201_merge_heads_drop_currency_tax_units.py
|
migrations/versions/20251001_001201_merge_heads_drop_currency_tax_units.py
|
||||||
migrations/versions/20251002_000101_add_bank_accounts_table.py
|
migrations/versions/20251002_000101_add_bank_accounts_table.py
|
||||||
|
migrations/versions/20251003_000201_add_cash_registers_table.py
|
||||||
migrations/versions/4b2ea782bcb3_merge_heads.py
|
migrations/versions/4b2ea782bcb3_merge_heads.py
|
||||||
migrations/versions/5553f8745c6e_add_support_tables.py
|
migrations/versions/5553f8745c6e_add_support_tables.py
|
||||||
migrations/versions/9f9786ae7191_create_tax_units_table.py
|
migrations/versions/9f9786ae7191_create_tax_units_table.py
|
||||||
|
migrations/versions/a1443c153b47_merge_heads.py
|
||||||
migrations/versions/caf3f4ef4b76_add_tax_units_table.py
|
migrations/versions/caf3f4ef4b76_add_tax_units_table.py
|
||||||
migrations/versions/d3e84892c1c2_sync_person_type_enum_values_callable_.py
|
migrations/versions/d3e84892c1c2_sync_person_type_enum_values_callable_.py
|
||||||
migrations/versions/f876bfa36805_merge_multiple_heads.py
|
migrations/versions/f876bfa36805_merge_multiple_heads.py
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -54,6 +54,34 @@ msgstr "Duplicate bank account code"
|
||||||
msgid "BULK_DELETE_FAILED"
|
msgid "BULK_DELETE_FAILED"
|
||||||
msgstr "Bulk delete failed for bank accounts"
|
msgstr "Bulk delete failed for bank accounts"
|
||||||
|
|
||||||
|
# Banking / Cash Registers
|
||||||
|
msgid "CASH_REGISTERS_LIST_FETCHED"
|
||||||
|
msgstr "Cash registers list retrieved successfully"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_CREATED"
|
||||||
|
msgstr "Cash register created successfully"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_DETAILS"
|
||||||
|
msgstr "Cash register details"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_UPDATED"
|
||||||
|
msgstr "Cash register updated successfully"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_DELETED"
|
||||||
|
msgstr "Cash register deleted successfully"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_NOT_FOUND"
|
||||||
|
msgstr "Cash register not found"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTERS_BULK_DELETE_DONE"
|
||||||
|
msgstr "Bulk delete completed for cash registers"
|
||||||
|
|
||||||
|
msgid "INVALID_CASH_CODE"
|
||||||
|
msgstr "Invalid cash register code"
|
||||||
|
|
||||||
|
msgid "DUPLICATE_CASH_CODE"
|
||||||
|
msgstr "Duplicate cash register code"
|
||||||
|
|
||||||
msgid "STRING_TOO_SHORT"
|
msgid "STRING_TOO_SHORT"
|
||||||
msgstr "String is too short"
|
msgstr "String is too short"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -75,6 +75,34 @@ msgstr "کد حساب بانکی تکراری است"
|
||||||
msgid "BULK_DELETE_FAILED"
|
msgid "BULK_DELETE_FAILED"
|
||||||
msgstr "خطا در حذف گروهی حسابهای بانکی"
|
msgstr "خطا در حذف گروهی حسابهای بانکی"
|
||||||
|
|
||||||
|
# Banking / Cash Registers
|
||||||
|
msgid "CASH_REGISTERS_LIST_FETCHED"
|
||||||
|
msgstr "لیست صندوقها با موفقیت دریافت شد"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_CREATED"
|
||||||
|
msgstr "صندوق با موفقیت ایجاد شد"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_DETAILS"
|
||||||
|
msgstr "جزئیات صندوق"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_UPDATED"
|
||||||
|
msgstr "صندوق با موفقیت بهروزرسانی شد"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_DELETED"
|
||||||
|
msgstr "صندوق با موفقیت حذف شد"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTER_NOT_FOUND"
|
||||||
|
msgstr "صندوق یافت نشد"
|
||||||
|
|
||||||
|
msgid "CASH_REGISTERS_BULK_DELETE_DONE"
|
||||||
|
msgstr "حذف گروهی صندوقها انجام شد"
|
||||||
|
|
||||||
|
msgid "INVALID_CASH_CODE"
|
||||||
|
msgstr "کد صندوق نامعتبر است"
|
||||||
|
|
||||||
|
msgid "DUPLICATE_CASH_CODE"
|
||||||
|
msgstr "کد صندوق تکراری است"
|
||||||
|
|
||||||
msgid "STRING_TOO_SHORT"
|
msgid "STRING_TOO_SHORT"
|
||||||
msgstr "رشته خیلی کوتاه است"
|
msgstr "رشته خیلی کوتاه است"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,29 +18,48 @@ depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
op.create_table(
|
bind = op.get_bind()
|
||||||
'bank_accounts',
|
inspector = sa.inspect(bind)
|
||||||
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
|
if 'bank_accounts' not in inspector.get_table_names():
|
||||||
sa.Column('business_id', sa.Integer(), sa.ForeignKey('businesses.id', ondelete='CASCADE'), nullable=False),
|
op.create_table(
|
||||||
sa.Column('code', sa.String(length=50), nullable=True),
|
'bank_accounts',
|
||||||
sa.Column('name', sa.String(length=255), nullable=False),
|
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
|
||||||
sa.Column('description', sa.String(length=500), nullable=True),
|
sa.Column('business_id', sa.Integer(), sa.ForeignKey('businesses.id', ondelete='CASCADE'), nullable=False),
|
||||||
sa.Column('branch', sa.String(length=255), nullable=True),
|
sa.Column('code', sa.String(length=50), nullable=True),
|
||||||
sa.Column('account_number', sa.String(length=50), nullable=True),
|
sa.Column('name', sa.String(length=255), nullable=False),
|
||||||
sa.Column('sheba_number', sa.String(length=30), nullable=True),
|
sa.Column('description', sa.String(length=500), nullable=True),
|
||||||
sa.Column('card_number', sa.String(length=20), nullable=True),
|
sa.Column('branch', sa.String(length=255), nullable=True),
|
||||||
sa.Column('owner_name', sa.String(length=255), nullable=True),
|
sa.Column('account_number', sa.String(length=50), nullable=True),
|
||||||
sa.Column('pos_number', sa.String(length=50), nullable=True),
|
sa.Column('sheba_number', sa.String(length=30), nullable=True),
|
||||||
sa.Column('payment_id', sa.String(length=100), nullable=True),
|
sa.Column('card_number', sa.String(length=20), nullable=True),
|
||||||
sa.Column('currency_id', sa.Integer(), sa.ForeignKey('currencies.id', ondelete='RESTRICT'), nullable=False),
|
sa.Column('owner_name', sa.String(length=255), nullable=True),
|
||||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
|
sa.Column('pos_number', sa.String(length=50), nullable=True),
|
||||||
sa.Column('is_default', sa.Boolean(), nullable=False, server_default=sa.text('0')),
|
sa.Column('payment_id', sa.String(length=100), nullable=True),
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
sa.Column('currency_id', sa.Integer(), sa.ForeignKey('currencies.id', ondelete='RESTRICT'), nullable=False),
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
|
||||||
sa.UniqueConstraint('business_id', 'code', name='uq_bank_accounts_business_code'),
|
sa.Column('is_default', sa.Boolean(), nullable=False, server_default=sa.text('0')),
|
||||||
)
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
op.create_index('ix_bank_accounts_business_id', 'bank_accounts', ['business_id'])
|
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||||
op.create_index('ix_bank_accounts_currency_id', 'bank_accounts', ['currency_id'])
|
sa.UniqueConstraint('business_id', 'code', name='uq_bank_accounts_business_code'),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
op.create_index('ix_bank_accounts_business_id', 'bank_accounts', ['business_id'])
|
||||||
|
op.create_index('ix_bank_accounts_currency_id', 'bank_accounts', ['currency_id'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# تلاش برای ایجاد ایندکسها اگر وجود ندارند
|
||||||
|
existing_indexes = {idx['name'] for idx in inspector.get_indexes('bank_accounts')}
|
||||||
|
if 'ix_bank_accounts_business_id' not in existing_indexes:
|
||||||
|
try:
|
||||||
|
op.create_index('ix_bank_accounts_business_id', 'bank_accounts', ['business_id'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if 'ix_bank_accounts_currency_id' not in existing_indexes:
|
||||||
|
try:
|
||||||
|
op.create_index('ix_bank_accounts_currency_id', 'bank_accounts', ['currency_id'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""add cash_registers table
|
||||||
|
|
||||||
|
Revision ID: 20251003_000201_add_cash_registers_table
|
||||||
|
Revises:
|
||||||
|
Create Date: 2025-10-03 00:02:01.000001
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '20251003_000201_add_cash_registers_table'
|
||||||
|
down_revision = 'a1443c153b47'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
if 'cash_registers' not in inspector.get_table_names():
|
||||||
|
op.create_table(
|
||||||
|
'cash_registers',
|
||||||
|
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
|
||||||
|
sa.Column('business_id', sa.Integer(), sa.ForeignKey('businesses.id', ondelete='CASCADE'), nullable=False),
|
||||||
|
sa.Column('code', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('description', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('currency_id', sa.Integer(), sa.ForeignKey('currencies.id', ondelete='RESTRICT'), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
|
||||||
|
sa.Column('is_default', sa.Boolean(), nullable=False, server_default=sa.text('0')),
|
||||||
|
sa.Column('payment_switch_number', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('payment_terminal_number', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('merchant_id', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.UniqueConstraint('business_id', 'code', name='uq_cash_registers_business_code'),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
op.create_index('ix_cash_registers_business_id', 'cash_registers', ['business_id'])
|
||||||
|
op.create_index('ix_cash_registers_currency_id', 'cash_registers', ['currency_id'])
|
||||||
|
op.create_index('ix_cash_registers_is_active', 'cash_registers', ['is_active'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_cash_registers_is_active', table_name='cash_registers')
|
||||||
|
op.drop_index('ix_cash_registers_currency_id', table_name='cash_registers')
|
||||||
|
op.drop_index('ix_cash_registers_business_id', table_name='cash_registers')
|
||||||
|
op.drop_table('cash_registers')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""add name to cash_registers
|
||||||
|
|
||||||
|
Revision ID: 20251003_010501_add_name_to_cash_registers
|
||||||
|
Revises: 20251003_000201_add_cash_registers_table
|
||||||
|
Create Date: 2025-10-03 01:05:01.000001
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = '20251003_010501_add_name_to_cash_registers'
|
||||||
|
down_revision = '20251003_000201_add_cash_registers_table'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add column if not exists (MySQL safe): try/except
|
||||||
|
conn = op.get_bind()
|
||||||
|
inspector = sa.inspect(conn)
|
||||||
|
cols = [c['name'] for c in inspector.get_columns('cash_registers')]
|
||||||
|
if 'name' not in cols:
|
||||||
|
op.add_column('cash_registers', sa.Column('name', sa.String(length=255), nullable=True))
|
||||||
|
# Fill default empty name from code or merchant_id to avoid nulls
|
||||||
|
try:
|
||||||
|
conn.execute(sa.text("UPDATE cash_registers SET name = COALESCE(name, code)"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Alter to not null
|
||||||
|
with op.batch_alter_table('cash_registers') as batch_op:
|
||||||
|
batch_op.alter_column('name', existing_type=sa.String(length=255), nullable=False)
|
||||||
|
# Create index
|
||||||
|
try:
|
||||||
|
op.create_index('ix_cash_registers_name', 'cash_registers', ['name'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
try:
|
||||||
|
op.drop_index('ix_cash_registers_name', table_name='cash_registers')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
with op.batch_alter_table('cash_registers') as batch_op:
|
||||||
|
try:
|
||||||
|
batch_op.drop_column('name')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
24
hesabixAPI/migrations/versions/a1443c153b47_merge_heads.py
Normal file
24
hesabixAPI/migrations/versions/a1443c153b47_merge_heads.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: a1443c153b47
|
||||||
|
Revises: 20250102_000001, 20251002_000101_add_bank_accounts_table
|
||||||
|
Create Date: 2025-10-03 14:25:49.978103
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'a1443c153b47'
|
||||||
|
down_revision = ('20250102_000001', '20251002_000101_add_bank_accounts_table')
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
@ -251,8 +251,19 @@ class AuthStore with ChangeNotifier {
|
||||||
|
|
||||||
// مدیریت کسب و کار فعلی
|
// مدیریت کسب و کار فعلی
|
||||||
Future<void> setCurrentBusiness(BusinessWithPermission business) async {
|
Future<void> setCurrentBusiness(BusinessWithPermission business) async {
|
||||||
|
print('=== setCurrentBusiness START ===');
|
||||||
|
print('Setting business: ${business.name} (ID: ${business.id})');
|
||||||
|
print('Is owner: ${business.isOwner}');
|
||||||
|
print('Role: ${business.role}');
|
||||||
|
print('Permissions: ${business.permissions}');
|
||||||
|
|
||||||
_currentBusiness = business;
|
_currentBusiness = business;
|
||||||
_businessPermissions = business.permissions;
|
_businessPermissions = business.permissions;
|
||||||
|
|
||||||
|
print('AuthStore updated:');
|
||||||
|
print(' - Current business: ${_currentBusiness?.name}');
|
||||||
|
print(' - Business permissions: $_businessPermissions');
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
// ذخیره در حافظه محلی
|
// ذخیره در حافظه محلی
|
||||||
|
|
@ -260,6 +271,8 @@ class AuthStore with ChangeNotifier {
|
||||||
|
|
||||||
// اگر ارز انتخاب نشده یا ارز انتخابی با کسبوکار ناسازگار است، ارز پیشفرض کسبوکار را ست کن
|
// اگر ارز انتخاب نشده یا ارز انتخابی با کسبوکار ناسازگار است، ارز پیشفرض کسبوکار را ست کن
|
||||||
await _ensureCurrencyForBusiness();
|
await _ensureCurrencyForBusiness();
|
||||||
|
|
||||||
|
print('=== setCurrentBusiness END ===');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearCurrentBusiness() async {
|
Future<void> clearCurrentBusiness() async {
|
||||||
|
|
@ -340,14 +353,36 @@ class AuthStore with ChangeNotifier {
|
||||||
|
|
||||||
// بررسی دسترسیهای کسب و کار
|
// بررسی دسترسیهای کسب و کار
|
||||||
bool hasBusinessPermission(String section, String action) {
|
bool hasBusinessPermission(String section, String action) {
|
||||||
if (_currentBusiness?.isOwner == true) return true;
|
print('=== hasBusinessPermission ===');
|
||||||
if (_businessPermissions == null) return false;
|
print('Section: $section, Action: $action');
|
||||||
|
print('Current business: ${_currentBusiness?.name} (ID: ${_currentBusiness?.id})');
|
||||||
|
print('Is owner: ${_currentBusiness?.isOwner}');
|
||||||
|
print('Business permissions: $_businessPermissions');
|
||||||
|
|
||||||
|
if (_currentBusiness?.isOwner == true) {
|
||||||
|
print('User is owner - GRANTED');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_businessPermissions == null) {
|
||||||
|
print('No business permissions - DENIED');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
final sectionPerms = _businessPermissions![section] as Map<String, dynamic>?;
|
||||||
// اگر سکشن در دسترسیها موجود نیست، هیچ دسترسیای وجود ندارد
|
print('Section permissions for "$section": $sectionPerms');
|
||||||
if (sectionPerms == null) return false;
|
|
||||||
|
|
||||||
return sectionPerms[action] == true;
|
// اگر سکشن در دسترسیها موجود نیست، هیچ دسترسیای وجود ندارد
|
||||||
|
if (sectionPerms == null) {
|
||||||
|
print('Section not found in permissions - DENIED');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasPermission = sectionPerms[action] == true;
|
||||||
|
print('Permission "$action" for section "$section": $hasPermission');
|
||||||
|
print('=== hasBusinessPermission END ===');
|
||||||
|
|
||||||
|
return hasPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
// دسترسیهای کلی
|
// دسترسیهای کلی
|
||||||
|
|
|
||||||
|
|
@ -1055,5 +1055,16 @@
|
||||||
"productDeletedSuccessfully": "Product or service deleted successfully",
|
"productDeletedSuccessfully": "Product or service deleted successfully",
|
||||||
"productsDeletedSuccessfully": "Selected items deleted successfully",
|
"productsDeletedSuccessfully": "Selected items deleted successfully",
|
||||||
"noRowsSelectedError": "No rows selected"
|
"noRowsSelectedError": "No rows selected"
|
||||||
|
,
|
||||||
|
"deleteSelected": "Delete Selected",
|
||||||
|
"deletedSuccessfully": "Deleted successfully",
|
||||||
|
"comingSoon": "Coming soon",
|
||||||
|
"code": "Code",
|
||||||
|
"currency": "Currency",
|
||||||
|
"isDefault": "Default",
|
||||||
|
"description": "Description",
|
||||||
|
"actions": "Actions",
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1038,6 +1038,16 @@
|
||||||
"editPriceTitle": "ویرایش قیمت",
|
"editPriceTitle": "ویرایش قیمت",
|
||||||
"productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد",
|
"productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد",
|
||||||
"productsDeletedSuccessfully": "آیتمهای انتخابشده با موفقیت حذف شدند",
|
"productsDeletedSuccessfully": "آیتمهای انتخابشده با موفقیت حذف شدند",
|
||||||
"noRowsSelectedError": "هیچ سطری انتخاب نشده است"
|
"noRowsSelectedError": "هیچ سطری انتخاب نشده است",
|
||||||
|
"deleteSelected": "حذف انتخابشدهها",
|
||||||
|
"deletedSuccessfully": "با موفقیت حذف شد",
|
||||||
|
"comingSoon": "بهزودی",
|
||||||
|
"code": "کد",
|
||||||
|
"currency": "واحد پول",
|
||||||
|
"isDefault": "پیشفرض",
|
||||||
|
"description": "توضیحات",
|
||||||
|
"actions": "اقدامات",
|
||||||
|
"yes": "بله",
|
||||||
|
"no": "خیر"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4715,7 +4715,7 @@ abstract class AppLocalizations {
|
||||||
/// No description provided for @code.
|
/// No description provided for @code.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'code'**
|
/// **'Code'**
|
||||||
String get code;
|
String get code;
|
||||||
|
|
||||||
/// No description provided for @conflictPolicy.
|
/// No description provided for @conflictPolicy.
|
||||||
|
|
@ -5581,6 +5581,24 @@ abstract class AppLocalizations {
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'No rows selected'**
|
/// **'No rows selected'**
|
||||||
String get noRowsSelectedError;
|
String get noRowsSelectedError;
|
||||||
|
|
||||||
|
/// No description provided for @deleteSelected.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Delete Selected'**
|
||||||
|
String get deleteSelected;
|
||||||
|
|
||||||
|
/// No description provided for @deletedSuccessfully.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Deleted successfully'**
|
||||||
|
String get deletedSuccessfully;
|
||||||
|
|
||||||
|
/// No description provided for @comingSoon.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Coming soon'**
|
||||||
|
String get comingSoon;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|
|
||||||
|
|
@ -2380,7 +2380,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
String get matchBy => 'Match by';
|
String get matchBy => 'Match by';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get code => 'code';
|
String get code => 'Code';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get conflictPolicy => 'Conflict policy';
|
String get conflictPolicy => 'Conflict policy';
|
||||||
|
|
@ -2828,4 +2828,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRowsSelectedError => 'No rows selected';
|
String get noRowsSelectedError => 'No rows selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteSelected => 'Delete Selected';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deletedSuccessfully => 'Deleted successfully';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get comingSoon => 'Coming soon';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1223,7 +1223,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get edit => 'ویرایش';
|
String get edit => 'ویرایش';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get actions => 'عملیات';
|
String get actions => 'اقدامات';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get search => 'جستجو';
|
String get search => 'جستجو';
|
||||||
|
|
@ -2594,7 +2594,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
String get price => 'قیمت';
|
String get price => 'قیمت';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get currency => 'ارز';
|
String get currency => 'واحد پول';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noPriceListsTitle => 'لیست قیمت موجود نیست';
|
String get noPriceListsTitle => 'لیست قیمت موجود نیست';
|
||||||
|
|
@ -2807,4 +2807,13 @@ class AppLocalizationsFa extends AppLocalizations {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noRowsSelectedError => 'هیچ سطری انتخاب نشده است';
|
String get noRowsSelectedError => 'هیچ سطری انتخاب نشده است';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deleteSelected => 'حذف انتخابشدهها';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get deletedSuccessfully => 'با موفقیت حذف شد';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get comingSoon => 'بهزودی';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import 'pages/business/product_attributes_page.dart';
|
||||||
import 'pages/business/products_page.dart';
|
import 'pages/business/products_page.dart';
|
||||||
import 'pages/business/price_lists_page.dart';
|
import 'pages/business/price_lists_page.dart';
|
||||||
import 'pages/business/price_list_items_page.dart';
|
import 'pages/business/price_list_items_page.dart';
|
||||||
|
import 'pages/business/cash_registers_page.dart';
|
||||||
import 'pages/error_404_page.dart';
|
import 'pages/error_404_page.dart';
|
||||||
import 'core/locale_controller.dart';
|
import 'core/locale_controller.dart';
|
||||||
import 'core/calendar_controller.dart';
|
import 'core/calendar_controller.dart';
|
||||||
|
|
@ -561,6 +562,24 @@ class _MyAppState extends State<MyApp> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'cash-box',
|
||||||
|
name: 'business_cash_box',
|
||||||
|
builder: (context, state) {
|
||||||
|
final businessId = int.parse(state.pathParameters['business_id']!);
|
||||||
|
return BusinessShell(
|
||||||
|
businessId: businessId,
|
||||||
|
authStore: _authStore!,
|
||||||
|
localeController: controller,
|
||||||
|
calendarController: _calendarController!,
|
||||||
|
themeController: themeController,
|
||||||
|
child: CashRegistersPage(
|
||||||
|
businessId: businessId,
|
||||||
|
authStore: _authStore!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
name: 'business_settings',
|
name: 'business_settings',
|
||||||
|
|
|
||||||
67
hesabixUI/hesabix_ui/lib/models/cash_register.dart
Normal file
67
hesabixUI/hesabix_ui/lib/models/cash_register.dart
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
class CashRegister {
|
||||||
|
final int? id;
|
||||||
|
final int businessId;
|
||||||
|
final String name;
|
||||||
|
final String? code;
|
||||||
|
final int currencyId;
|
||||||
|
final bool isActive;
|
||||||
|
final bool isDefault;
|
||||||
|
final String? description;
|
||||||
|
final String? paymentSwitchNumber;
|
||||||
|
final String? paymentTerminalNumber;
|
||||||
|
final String? merchantId;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
const CashRegister({
|
||||||
|
this.id,
|
||||||
|
required this.businessId,
|
||||||
|
required this.name,
|
||||||
|
this.code,
|
||||||
|
required this.currencyId,
|
||||||
|
this.isActive = true,
|
||||||
|
this.isDefault = false,
|
||||||
|
this.description,
|
||||||
|
this.paymentSwitchNumber,
|
||||||
|
this.paymentTerminalNumber,
|
||||||
|
this.merchantId,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CashRegister.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CashRegister(
|
||||||
|
id: json['id'] as int?,
|
||||||
|
businessId: (json['business_id'] ?? json['businessId']) as int,
|
||||||
|
name: (json['name'] ?? '') as String,
|
||||||
|
code: json['code'] as String?,
|
||||||
|
currencyId: (json['currency_id'] ?? json['currencyId']) as int,
|
||||||
|
isActive: (json['is_active'] ?? true) as bool,
|
||||||
|
isDefault: (json['is_default'] ?? false) as bool,
|
||||||
|
description: json['description'] as String?,
|
||||||
|
paymentSwitchNumber: json['payment_switch_number'] as String?,
|
||||||
|
paymentTerminalNumber: json['payment_terminal_number'] as String?,
|
||||||
|
merchantId: json['merchant_id'] as String?,
|
||||||
|
createdAt: json['created_at'] != null ? DateTime.tryParse(json['created_at'].toString()) : null,
|
||||||
|
updatedAt: json['updated_at'] != null ? DateTime.tryParse(json['updated_at'].toString()) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'business_id': businessId,
|
||||||
|
'name': name,
|
||||||
|
'code': code,
|
||||||
|
'currency_id': currencyId,
|
||||||
|
'is_active': isActive,
|
||||||
|
'is_default': isDefault,
|
||||||
|
'description': description,
|
||||||
|
'payment_switch_number': paymentSwitchNumber,
|
||||||
|
'payment_terminal_number': paymentTerminalNumber,
|
||||||
|
'merchant_id': merchantId,
|
||||||
|
'created_at': createdAt?.toIso8601String(),
|
||||||
|
'updated_at': updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,10 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
// اطمینان از bind بودن AuthStore برای ApiClient (جهت هدرها و تنظیمات)
|
||||||
|
try {
|
||||||
|
ApiClient.bindAuthStore(widget.authStore);
|
||||||
|
} catch (_) {}
|
||||||
// اضافه کردن listener برای AuthStore
|
// اضافه کردن listener برای AuthStore
|
||||||
widget.authStore.addListener(() {
|
widget.authStore.addListener(() {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -58,14 +62,30 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBusinessInfo() async {
|
Future<void> _loadBusinessInfo() async {
|
||||||
|
print('=== _loadBusinessInfo START ===');
|
||||||
|
print('Current business ID: ${widget.businessId}');
|
||||||
|
print('AuthStore current business ID: ${widget.authStore.currentBusiness?.id}');
|
||||||
|
|
||||||
if (widget.authStore.currentBusiness?.id == widget.businessId) {
|
if (widget.authStore.currentBusiness?.id == widget.businessId) {
|
||||||
|
print('Business info already loaded, skipping...');
|
||||||
return; // اطلاعات قبلاً بارگذاری شده
|
return; // اطلاعات قبلاً بارگذاری شده
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
print('Loading business info for business ID: ${widget.businessId}');
|
||||||
final businessData = await _businessService.getBusinessWithPermissions(widget.businessId);
|
final businessData = await _businessService.getBusinessWithPermissions(widget.businessId);
|
||||||
|
print('Business data loaded successfully:');
|
||||||
|
print(' - Name: ${businessData.name}');
|
||||||
|
print(' - ID: ${businessData.id}');
|
||||||
|
print(' - Is Owner: ${businessData.isOwner}');
|
||||||
|
print(' - Role: ${businessData.role}');
|
||||||
|
print(' - Permissions: ${businessData.permissions}');
|
||||||
|
|
||||||
await widget.authStore.setCurrentBusiness(businessData);
|
await widget.authStore.setCurrentBusiness(businessData);
|
||||||
|
print('Business info set in authStore');
|
||||||
|
print('AuthStore business permissions: ${widget.authStore.businessPermissions}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('Error loading business info: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|
@ -75,6 +95,7 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print('=== _loadBusinessInfo END ===');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -716,7 +737,8 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
} else if (child.label == t.pettyCash) {
|
} else if (child.label == t.pettyCash) {
|
||||||
// Navigate to add petty cash
|
// Navigate to add petty cash
|
||||||
} else if (child.label == t.cashBox) {
|
} else if (child.label == t.cashBox) {
|
||||||
// Navigate to add cash box
|
// For cash box, navigate to the page and use its add
|
||||||
|
context.go('/business/${widget.businessId}/cash-box');
|
||||||
} else if (child.label == t.wallet) {
|
} else if (child.label == t.wallet) {
|
||||||
// Navigate to add wallet
|
// Navigate to add wallet
|
||||||
} else if (child.label == t.checks) {
|
} else if (child.label == t.checks) {
|
||||||
|
|
@ -865,6 +887,8 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
businessId: widget.businessId,
|
businessId: widget.businessId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (item.label == t.cashBox) {
|
||||||
|
context.go('/business/${widget.businessId}/cash-box');
|
||||||
}
|
}
|
||||||
// سایر مسیرهای افزودن در آینده متصل میشوند
|
// سایر مسیرهای افزودن در آینده متصل میشوند
|
||||||
},
|
},
|
||||||
|
|
@ -1073,36 +1097,96 @@ class _BusinessShellState extends State<BusinessShell> {
|
||||||
|
|
||||||
// فیلتر کردن منو بر اساس دسترسیها
|
// فیلتر کردن منو بر اساس دسترسیها
|
||||||
List<_MenuItem> _getFilteredMenuItems(List<_MenuItem> allItems) {
|
List<_MenuItem> _getFilteredMenuItems(List<_MenuItem> allItems) {
|
||||||
return allItems.where((item) {
|
print('=== _getFilteredMenuItems START ===');
|
||||||
if (item.type == _MenuItemType.separator) return true;
|
print('Total menu items: ${allItems.length}');
|
||||||
|
print('Current business: ${widget.authStore.currentBusiness?.name} (ID: ${widget.authStore.currentBusiness?.id})');
|
||||||
|
print('Is owner: ${widget.authStore.currentBusiness?.isOwner}');
|
||||||
|
print('Business permissions: ${widget.authStore.businessPermissions}');
|
||||||
|
|
||||||
|
final filteredItems = allItems.where((item) {
|
||||||
|
if (item.type == _MenuItemType.separator) {
|
||||||
|
print('Separator item: ${item.label} - KEEPING');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (item.type == _MenuItemType.simple) {
|
if (item.type == _MenuItemType.simple) {
|
||||||
return _hasAccessToMenuItem(item);
|
final hasAccess = _hasAccessToMenuItem(item);
|
||||||
|
print('Simple item: ${item.label} - ${hasAccess ? 'KEEPING' : 'REMOVING'}');
|
||||||
|
return hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type == _MenuItemType.expandable) {
|
if (item.type == _MenuItemType.expandable) {
|
||||||
return _hasAccessToExpandableMenuItem(item);
|
final hasAccess = _hasAccessToExpandableMenuItem(item);
|
||||||
|
print('Expandable item: ${item.label} - ${hasAccess ? 'KEEPING' : 'REMOVING'}');
|
||||||
|
return hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print('Unknown item type: ${item.label} - REMOVING');
|
||||||
return false;
|
return false;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
print('Filtered menu items: ${filteredItems.length}');
|
||||||
|
for (final item in filteredItems) {
|
||||||
|
print(' - ${item.label} (${item.type})');
|
||||||
|
}
|
||||||
|
print('=== _getFilteredMenuItems END ===');
|
||||||
|
|
||||||
|
return filteredItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasAccessToMenuItem(_MenuItem item) {
|
bool _hasAccessToMenuItem(_MenuItem item) {
|
||||||
final section = _sectionForLabel(item.label, AppLocalizations.of(context));
|
final section = _sectionForLabel(item.label, AppLocalizations.of(context));
|
||||||
|
print(' Checking access for: ${item.label} -> section: $section');
|
||||||
|
|
||||||
// داشبورد همیشه قابل مشاهده است
|
// داشبورد همیشه قابل مشاهده است
|
||||||
if (item.path != null && item.path!.endsWith('/dashboard')) return true;
|
if (item.path != null && item.path!.endsWith('/dashboard')) {
|
||||||
|
print(' Dashboard item - ALWAYS ACCESSIBLE');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// اگر سکشن تعریف نشده، نمایش داده نشود
|
// اگر سکشن تعریف نشده، نمایش داده نشود
|
||||||
if (section == null) return false;
|
if (section == null) {
|
||||||
// فقط وقتی اجازه خواندن دارد نمایش بده
|
print(' No section mapping found - DENIED');
|
||||||
return widget.authStore.canReadSection(section);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// بررسی دسترسیهای مختلف برای نمایش منو
|
||||||
|
// اگر کاربر مالک است، همه منوها قابل مشاهده هستند
|
||||||
|
if (widget.authStore.currentBusiness?.isOwner == true) {
|
||||||
|
print(' User is owner - GRANTED');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// برای کاربران عضو، بررسی دسترسی view
|
||||||
|
final hasAccess = widget.authStore.canReadSection(section);
|
||||||
|
print(' Checking view permission for section "$section": $hasAccess');
|
||||||
|
|
||||||
|
// Debug: بررسی دقیقتر دسترسیها
|
||||||
|
if (widget.authStore.businessPermissions != null) {
|
||||||
|
final sectionPerms = widget.authStore.businessPermissions![section];
|
||||||
|
print(' Section permissions for "$section": $sectionPerms');
|
||||||
|
if (sectionPerms != null) {
|
||||||
|
final viewPerm = sectionPerms['view'];
|
||||||
|
print(' View permission: $viewPerm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasAccessToExpandableMenuItem(_MenuItem item) {
|
bool _hasAccessToExpandableMenuItem(_MenuItem item) {
|
||||||
if (item.children == null) return false;
|
if (item.children == null) {
|
||||||
|
print(' Expandable item "${item.label}" has no children - DENIED');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
print(' Checking expandable item: ${item.label} with ${item.children!.length} children');
|
||||||
|
|
||||||
// اگر حداقل یکی از زیرآیتمها قابل دسترسی باشد، منو نمایش داده شود
|
// اگر حداقل یکی از زیرآیتمها قابل دسترسی باشد، منو نمایش داده شود
|
||||||
return item.children!.any((child) => _hasAccessToMenuItem(child));
|
final hasAccess = item.children!.any((child) => _hasAccessToMenuItem(child));
|
||||||
|
print(' Expandable item "${item.label}" access: $hasAccess');
|
||||||
|
|
||||||
|
return hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// تبدیل برچسب محلیشده منو به کلید سکشن دسترسی
|
// تبدیل برچسب محلیشده منو به کلید سکشن دسترسی
|
||||||
|
|
|
||||||
286
hesabixUI/hesabix_ui/lib/pages/business/cash_registers_page.dart
Normal file
286
hesabixUI/hesabix_ui/lib/pages/business/cash_registers_page.dart
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import 'package:hesabix_ui/core/api_client.dart';
|
||||||
|
import '../../widgets/data_table/data_table_widget.dart';
|
||||||
|
import '../../widgets/data_table/data_table_config.dart';
|
||||||
|
import '../../widgets/permission/permission_widgets.dart';
|
||||||
|
import '../../core/auth_store.dart';
|
||||||
|
import '../../models/cash_register.dart';
|
||||||
|
import '../../services/cash_register_service.dart';
|
||||||
|
import '../../services/currency_service.dart';
|
||||||
|
import '../../widgets/banking/cash_register_form_dialog.dart';
|
||||||
|
|
||||||
|
class CashRegistersPage extends StatefulWidget {
|
||||||
|
final int businessId;
|
||||||
|
final AuthStore authStore;
|
||||||
|
|
||||||
|
const CashRegistersPage({super.key, required this.businessId, required this.authStore});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CashRegistersPage> createState() => _CashRegistersPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CashRegistersPageState extends State<CashRegistersPage> {
|
||||||
|
final _service = CashRegisterService();
|
||||||
|
final _currencyService = CurrencyService(ApiClient());
|
||||||
|
final GlobalKey _tableKey = GlobalKey();
|
||||||
|
Map<int, String> _currencyNames = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadCurrencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCurrencies() async {
|
||||||
|
try {
|
||||||
|
final currencies = await _currencyService.listBusinessCurrencies(
|
||||||
|
businessId: widget.businessId,
|
||||||
|
);
|
||||||
|
final currencyMap = <int, String>{};
|
||||||
|
for (final currency in currencies) {
|
||||||
|
currencyMap[currency['id'] as int] = '${currency['title']} (${currency['code']})';
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_currencyNames = currencyMap;
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
|
||||||
|
if (!widget.authStore.canReadSection('cash')) {
|
||||||
|
return AccessDeniedPage(message: t.accessDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: DataTableWidget<CashRegister>(
|
||||||
|
key: _tableKey,
|
||||||
|
config: _buildConfig(t),
|
||||||
|
fromJson: CashRegister.fromJson,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DataTableConfig<CashRegister> _buildConfig(AppLocalizations t) {
|
||||||
|
return DataTableConfig<CashRegister>(
|
||||||
|
endpoint: '/api/v1/cash-registers/businesses/${widget.businessId}/cash-registers',
|
||||||
|
title: t.cashBox,
|
||||||
|
excelEndpoint: '/api/v1/cash-registers/businesses/${widget.businessId}/cash-registers/export/excel',
|
||||||
|
pdfEndpoint: '/api/v1/cash-registers/businesses/${widget.businessId}/cash-registers/export/pdf',
|
||||||
|
getExportParams: () => {'business_id': widget.businessId},
|
||||||
|
showBackButton: true,
|
||||||
|
onBack: () => Navigator.of(context).maybePop(),
|
||||||
|
showTableIcon: false,
|
||||||
|
showRowNumbers: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
enableMultiRowSelection: true,
|
||||||
|
columns: [
|
||||||
|
TextColumn(
|
||||||
|
'code',
|
||||||
|
t.code,
|
||||||
|
width: ColumnWidth.small,
|
||||||
|
formatter: (row) => (row.code?.toString() ?? '-'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'name',
|
||||||
|
t.title,
|
||||||
|
width: ColumnWidth.large,
|
||||||
|
formatter: (row) => row.name,
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'currency_id',
|
||||||
|
t.currency,
|
||||||
|
width: ColumnWidth.medium,
|
||||||
|
formatter: (row) => _currencyNames[row.currencyId] ?? (t.localeName == 'fa' ? 'نامشخص' : 'Unknown'),
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'is_active',
|
||||||
|
t.active,
|
||||||
|
width: ColumnWidth.small,
|
||||||
|
formatter: (row) => row.isActive ? t.active : t.inactive,
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'is_default',
|
||||||
|
t.isDefault,
|
||||||
|
width: ColumnWidth.small,
|
||||||
|
formatter: (row) => row.isDefault ? t.yes : t.no,
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'payment_switch_number',
|
||||||
|
(t.localeName == 'fa') ? 'شماره سویچ پرداخت' : 'Payment Switch No.',
|
||||||
|
width: ColumnWidth.large,
|
||||||
|
formatter: (row) => row.paymentSwitchNumber ?? '-',
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'payment_terminal_number',
|
||||||
|
(t.localeName == 'fa') ? 'شماره ترمینال' : 'Terminal No.',
|
||||||
|
width: ColumnWidth.large,
|
||||||
|
formatter: (row) => row.paymentTerminalNumber ?? '-',
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'merchant_id',
|
||||||
|
(t.localeName == 'fa') ? 'پذیرنده' : 'Merchant ID',
|
||||||
|
width: ColumnWidth.large,
|
||||||
|
formatter: (row) => row.merchantId ?? '-',
|
||||||
|
),
|
||||||
|
TextColumn(
|
||||||
|
'description',
|
||||||
|
t.description,
|
||||||
|
width: ColumnWidth.large,
|
||||||
|
formatter: (row) => row.description ?? '-',
|
||||||
|
),
|
||||||
|
ActionColumn(
|
||||||
|
'actions',
|
||||||
|
t.actions,
|
||||||
|
actions: [
|
||||||
|
DataTableAction(
|
||||||
|
icon: Icons.edit,
|
||||||
|
label: t.edit,
|
||||||
|
onTap: (row) => _edit(row),
|
||||||
|
),
|
||||||
|
DataTableAction(
|
||||||
|
icon: Icons.delete,
|
||||||
|
label: t.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
onTap: (row) => _delete(row),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
searchFields: ['code','description','payment_switch_number','payment_terminal_number','merchant_id'],
|
||||||
|
filterFields: ['is_active','is_default','currency_id'],
|
||||||
|
defaultPageSize: 20,
|
||||||
|
customHeaderActions: [
|
||||||
|
PermissionButton(
|
||||||
|
section: 'cash',
|
||||||
|
action: 'add',
|
||||||
|
authStore: widget.authStore,
|
||||||
|
child: Tooltip(
|
||||||
|
message: t.add,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _add,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.authStore.canDeleteSection('cash'))
|
||||||
|
Tooltip(
|
||||||
|
message: t.deleteSelected,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _bulkDelete,
|
||||||
|
icon: const Icon(Icons.delete_sweep_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _add() async {
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => CashRegisterFormDialog(
|
||||||
|
businessId: widget.businessId,
|
||||||
|
onSuccess: () {
|
||||||
|
try { (_tableKey.currentState as dynamic)?.refresh(); } catch (_) {}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _edit(CashRegister row) async {
|
||||||
|
await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => CashRegisterFormDialog(
|
||||||
|
businessId: widget.businessId,
|
||||||
|
register: row,
|
||||||
|
onSuccess: () {
|
||||||
|
try { (_tableKey.currentState as dynamic)?.refresh(); } catch (_) {}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _delete(CashRegister row) async {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
final confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(t.delete),
|
||||||
|
content: Text(t.deleteConfirm(row.code ?? '')),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: Text(t.cancel)),
|
||||||
|
FilledButton.tonal(onPressed: () => Navigator.of(ctx).pop(true), child: Text(t.delete)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirm != true) return;
|
||||||
|
try {
|
||||||
|
await _service.delete(row.id!);
|
||||||
|
try { (_tableKey.currentState as dynamic)?.refresh(); } catch (_) {}
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.deletedSuccessfully)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${t.error}: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _bulkDelete() async {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
try {
|
||||||
|
final state = _tableKey.currentState as dynamic;
|
||||||
|
final selectedIndices = (state?.getSelectedRowIndices() as List<int>?) ?? const <int>[];
|
||||||
|
final items = (state?.getSelectedItems() as List<dynamic>?) ?? const <dynamic>[];
|
||||||
|
if (selectedIndices.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noRowsSelectedError)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ids = <int>[];
|
||||||
|
for (final i in selectedIndices) {
|
||||||
|
if (i >= 0 && i < items.length) {
|
||||||
|
final row = items[i];
|
||||||
|
if (row is CashRegister && row.id != null) {
|
||||||
|
ids.add(row.id!);
|
||||||
|
} else if (row is Map<String, dynamic>) {
|
||||||
|
final id = row['id'];
|
||||||
|
if (id is int) ids.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ids.isEmpty) return;
|
||||||
|
final confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: Text(t.deleteSelected),
|
||||||
|
content: Text(t.deleteSelected),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: Text(t.cancel)),
|
||||||
|
FilledButton.tonal(onPressed: () => Navigator.of(ctx).pop(true), child: Text(t.delete)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirm != true) return;
|
||||||
|
final client = ApiClient();
|
||||||
|
await client.post<Map<String, dynamic>>(
|
||||||
|
'/api/v1/cash-registers/businesses/${widget.businessId}/cash-registers/bulk-delete',
|
||||||
|
data: { 'ids': ids },
|
||||||
|
);
|
||||||
|
try { (_tableKey.currentState as dynamic)?.refresh(); } catch (_) {}
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.deletedSuccessfully)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${t.error}: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -120,43 +120,101 @@ class BusinessDashboardService {
|
||||||
|
|
||||||
/// دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر
|
/// دریافت اطلاعات کسب و کار همراه با دسترسیهای کاربر
|
||||||
Future<BusinessWithPermission> getBusinessWithPermissions(int businessId) async {
|
Future<BusinessWithPermission> getBusinessWithPermissions(int businessId) async {
|
||||||
|
print('=== getBusinessWithPermissions START ===');
|
||||||
|
print('Business ID: $businessId');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
print('Calling API: /api/v1/business/$businessId/info-with-permissions');
|
||||||
final response = await _apiClient.post<Map<String, dynamic>>(
|
final response = await _apiClient.post<Map<String, dynamic>>(
|
||||||
'/api/v1/business/$businessId/info-with-permissions',
|
'/api/v1/business/$businessId/info-with-permissions',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('API Response received:');
|
||||||
|
print(' - Success: ${response.data?['success']}');
|
||||||
|
print(' - Message: ${response.data?['message']}');
|
||||||
|
print(' - Data: ${response.data?['data']}');
|
||||||
|
|
||||||
if (response.data?['success'] == true) {
|
if (response.data?['success'] == true) {
|
||||||
final data = response.data!['data'] as Map<String, dynamic>;
|
final data = response.data!['data'] as Map<String, dynamic>;
|
||||||
|
|
||||||
// تبدیل اطلاعات کسب و کار
|
// تبدیل اطلاعات کسب و کار
|
||||||
final businessInfo = data['business_info'] as Map<String, dynamic>;
|
final businessInfo = data['business_info'] as Map<String, dynamic>;
|
||||||
final userPermissions = data['user_permissions'] as Map<String, dynamic>? ?? {};
|
// نرمالسازی دسترسیها: هم Map پشتیبانی میشود و هم List<String> مثل 'inventory.read'
|
||||||
|
final dynamic userPermissionsRaw = data['user_permissions'];
|
||||||
|
final Map<String, dynamic> userPermissions = <String, dynamic>{};
|
||||||
|
if (userPermissionsRaw is Map<String, dynamic>) {
|
||||||
|
userPermissions.addAll(userPermissionsRaw);
|
||||||
|
} else if (userPermissionsRaw is List) {
|
||||||
|
for (final item in userPermissionsRaw) {
|
||||||
|
if (item is String) {
|
||||||
|
final parts = item.split('.');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
final String section = parts[0];
|
||||||
|
final String action = parts[1];
|
||||||
|
final Map<String, dynamic> sectionPerms =
|
||||||
|
(userPermissions[section] as Map<String, dynamic>?) ?? <String, dynamic>{};
|
||||||
|
sectionPerms[action] = true;
|
||||||
|
userPermissions[section] = sectionPerms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
final isOwner = data['is_owner'] as bool? ?? false;
|
final isOwner = data['is_owner'] as bool? ?? false;
|
||||||
final role = data['role'] as String? ?? 'عضو';
|
final role = data['role'] as String? ?? 'عضو';
|
||||||
final hasAccess = data['has_access'] as bool? ?? false;
|
final hasAccess = data['has_access'] as bool? ?? false;
|
||||||
|
|
||||||
|
print('Parsed data:');
|
||||||
|
print(' - Business Info: $businessInfo');
|
||||||
|
print(' - User Permissions: $userPermissions');
|
||||||
|
print(' - Is Owner: $isOwner');
|
||||||
|
print(' - Role: $role');
|
||||||
|
print(' - Has Access: $hasAccess');
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
|
print('Access denied by API');
|
||||||
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
||||||
}
|
}
|
||||||
|
|
||||||
return BusinessWithPermission(
|
// ساخت یک Map ترکیبی از اطلاعات کسب و کار + متادیتاهای دسترسی کاربر
|
||||||
id: businessInfo['id'] as int,
|
final Map<String, dynamic> combined = <String, dynamic>{
|
||||||
name: businessInfo['name'] as String,
|
...businessInfo,
|
||||||
businessType: businessInfo['business_type'] as String,
|
'is_owner': isOwner,
|
||||||
businessField: businessInfo['business_field'] as String,
|
'role': role,
|
||||||
ownerId: businessInfo['owner_id'] as int,
|
'permissions': userPermissions,
|
||||||
address: businessInfo['address'] as String?,
|
};
|
||||||
phone: businessInfo['phone'] as String?,
|
|
||||||
mobile: businessInfo['mobile'] as String?,
|
// اگر سرور ارز پیشفرض یا لیست ارزها را نیز ارسال کرد، اضافه کنیم
|
||||||
createdAt: businessInfo['created_at'] as String,
|
if (data.containsKey('default_currency')) {
|
||||||
isOwner: isOwner,
|
combined['default_currency'] = data['default_currency'];
|
||||||
role: role,
|
}
|
||||||
permissions: userPermissions,
|
if (data.containsKey('currencies')) {
|
||||||
|
combined['currencies'] = data['currencies'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// استفاده از fromJson برای مدیریت امن انواع (مثلاً created_at میتواند String یا Map باشد)
|
||||||
|
final businessWithPermission = BusinessWithPermission.fromJson(
|
||||||
|
Map<String, dynamic>.from(combined),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('BusinessWithPermission created:');
|
||||||
|
print(' - Name: ${businessWithPermission.name}');
|
||||||
|
print(' - ID: ${businessWithPermission.id}');
|
||||||
|
print(' - Is Owner: ${businessWithPermission.isOwner}');
|
||||||
|
print(' - Role: ${businessWithPermission.role}');
|
||||||
|
print(' - Permissions: ${businessWithPermission.permissions}');
|
||||||
|
|
||||||
|
print('=== getBusinessWithPermissions END ===');
|
||||||
|
return businessWithPermission;
|
||||||
} else {
|
} else {
|
||||||
|
print('API returned error: ${response.data?['message']}');
|
||||||
throw Exception('Failed to load business info: ${response.data?['message']}');
|
throw Exception('Failed to load business info: ${response.data?['message']}');
|
||||||
}
|
}
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
|
print('DioException occurred:');
|
||||||
|
print(' - Status Code: ${e.response?.statusCode}');
|
||||||
|
print(' - Response Data: ${e.response?.data}');
|
||||||
|
print(' - Message: ${e.message}');
|
||||||
|
|
||||||
if (e.response?.statusCode == 403) {
|
if (e.response?.statusCode == 403) {
|
||||||
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
throw Exception('دسترسی غیرمجاز به این کسب و کار');
|
||||||
} else if (e.response?.statusCode == 404) {
|
} else if (e.response?.statusCode == 404) {
|
||||||
|
|
@ -165,6 +223,7 @@ class BusinessDashboardService {
|
||||||
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: ${e.message}');
|
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: ${e.message}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
print('General Exception: $e');
|
||||||
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: $e');
|
throw Exception('خطا در بارگذاری اطلاعات کسب و کار: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
hesabixUI/hesabix_ui/lib/services/cash_register_service.dart
Normal file
57
hesabixUI/hesabix_ui/lib/services/cash_register_service.dart
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../core/api_client.dart';
|
||||||
|
import '../models/cash_register.dart';
|
||||||
|
|
||||||
|
class CashRegisterService {
|
||||||
|
final ApiClient _client;
|
||||||
|
CashRegisterService({ApiClient? client}) : _client = client ?? ApiClient();
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> list({required int businessId, required Map<String, dynamic> queryInfo}) async {
|
||||||
|
final res = await _client.post<Map<String, dynamic>>(
|
||||||
|
'/api/v1/cash-registers/businesses/$businessId/cash-registers',
|
||||||
|
data: queryInfo,
|
||||||
|
);
|
||||||
|
return (res.data ?? <String, dynamic>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CashRegister> create({required int businessId, required Map<String, dynamic> payload}) async {
|
||||||
|
final res = await _client.post<Map<String, dynamic>>(
|
||||||
|
'/api/v1/cash-registers/businesses/$businessId/cash-registers/create',
|
||||||
|
data: payload,
|
||||||
|
);
|
||||||
|
final data = (res.data?['data'] as Map<String, dynamic>? ?? <String, dynamic>{});
|
||||||
|
return CashRegister.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CashRegister> getById(int id) async {
|
||||||
|
final res = await _client.get<Map<String, dynamic>>('/api/v1/cash-registers/cash-registers/$id');
|
||||||
|
final data = (res.data?['data'] as Map<String, dynamic>? ?? <String, dynamic>{});
|
||||||
|
return CashRegister.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CashRegister> update({required int id, required Map<String, dynamic> payload}) async {
|
||||||
|
final res = await _client.put<Map<String, dynamic>>('/api/v1/cash-registers/cash-registers/$id', data: payload);
|
||||||
|
final data = (res.data?['data'] as Map<String, dynamic>? ?? <String, dynamic>{});
|
||||||
|
return CashRegister.fromJson(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(int id) async {
|
||||||
|
await _client.delete<Map<String, dynamic>>('/api/v1/cash-registers/cash-registers/$id');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response<List<int>>> exportExcel({required int businessId, required Map<String, dynamic> body}) async {
|
||||||
|
return _client.post<List<int>>(
|
||||||
|
'/api/v1/cash-registers/businesses/$businessId/cash-registers/export/excel',
|
||||||
|
data: body,
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response<List<int>>> exportPdf({required int businessId, required Map<String, dynamic> body}) async {
|
||||||
|
return _client.post<List<int>>(
|
||||||
|
'/api/v1/cash-registers/businesses/$businessId/cash-registers/export/pdf',
|
||||||
|
data: body,
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hesabix_ui/l10n/app_localizations.dart';
|
||||||
|
import '../../models/cash_register.dart';
|
||||||
|
import '../../services/cash_register_service.dart';
|
||||||
|
import 'currency_picker_widget.dart';
|
||||||
|
|
||||||
|
class CashRegisterFormDialog extends StatefulWidget {
|
||||||
|
final int businessId;
|
||||||
|
final CashRegister? register; // null برای افزودن، مقدار برای ویرایش
|
||||||
|
final VoidCallback? onSuccess;
|
||||||
|
|
||||||
|
const CashRegisterFormDialog({
|
||||||
|
super.key,
|
||||||
|
required this.businessId,
|
||||||
|
this.register,
|
||||||
|
this.onSuccess,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CashRegisterFormDialog> createState() => _CashRegisterFormDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CashRegisterFormDialogState extends State<CashRegisterFormDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _service = CashRegisterService();
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
final _codeController = TextEditingController();
|
||||||
|
bool _autoGenerateCode = true;
|
||||||
|
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _descriptionController = TextEditingController();
|
||||||
|
final _paymentSwitchController = TextEditingController();
|
||||||
|
final _paymentTerminalController = TextEditingController();
|
||||||
|
final _merchantIdController = TextEditingController();
|
||||||
|
|
||||||
|
bool _isActive = true;
|
||||||
|
bool _isDefault = false;
|
||||||
|
int? _currencyId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initializeForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeForm() {
|
||||||
|
if (widget.register != null) {
|
||||||
|
final r = widget.register!;
|
||||||
|
if (r.code != null) {
|
||||||
|
_codeController.text = r.code!;
|
||||||
|
_autoGenerateCode = false;
|
||||||
|
}
|
||||||
|
_nameController.text = r.name;
|
||||||
|
_descriptionController.text = r.description ?? '';
|
||||||
|
_paymentSwitchController.text = r.paymentSwitchNumber ?? '';
|
||||||
|
_paymentTerminalController.text = r.paymentTerminalNumber ?? '';
|
||||||
|
_merchantIdController.text = r.merchantId ?? '';
|
||||||
|
_isActive = r.isActive;
|
||||||
|
_isDefault = r.isDefault;
|
||||||
|
_currencyId = r.currencyId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_codeController.dispose();
|
||||||
|
_nameController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
_paymentSwitchController.dispose();
|
||||||
|
_paymentTerminalController.dispose();
|
||||||
|
_merchantIdController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
if (_currencyId == null) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(t.currency), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() { _isLoading = true; });
|
||||||
|
try {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
'name': _nameController.text.trim(),
|
||||||
|
'code': _autoGenerateCode ? null : (_codeController.text.trim().isEmpty ? null : _codeController.text.trim()),
|
||||||
|
'description': _descriptionController.text.trim().isEmpty ? null : _descriptionController.text.trim(),
|
||||||
|
'payment_switch_number': _paymentSwitchController.text.trim().isEmpty ? null : _paymentSwitchController.text.trim(),
|
||||||
|
'payment_terminal_number': _paymentTerminalController.text.trim().isEmpty ? null : _paymentTerminalController.text.trim(),
|
||||||
|
'merchant_id': _merchantIdController.text.trim().isEmpty ? null : _merchantIdController.text.trim(),
|
||||||
|
'is_active': _isActive,
|
||||||
|
'is_default': _isDefault,
|
||||||
|
'currency_id': _currencyId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (widget.register == null) {
|
||||||
|
await _service.create(businessId: widget.businessId, payload: payload);
|
||||||
|
} else {
|
||||||
|
await _service.update(id: widget.register!.id!, payload: payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
widget.onSuccess?.call();
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(widget.register == null
|
||||||
|
? (t.localeName == 'fa' ? 'صندوق با موفقیت ایجاد شد' : 'Cash register created successfully')
|
||||||
|
: (t.localeName == 'fa' ? 'صندوق با موفقیت بهروزرسانی شد' : 'Cash register updated successfully')
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('${t.error}: $e'), backgroundColor: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() { _isLoading = false; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = AppLocalizations.of(context);
|
||||||
|
final isEditing = widget.register != null;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.9,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(isEditing ? Icons.edit : Icons.add, color: Theme.of(context).primaryColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
isEditing ? (t.localeName == 'fa' ? 'ویرایش صندوق' : 'Edit Cash Register') : (t.localeName == 'fa' ? 'افزودن صندوق' : 'Add Cash Register'),
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(isScrollable: true, tabs: [
|
||||||
|
Tab(text: t.title),
|
||||||
|
Tab(text: t.settings),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: _buildBasicInfo(t),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
child: _buildSettings(t),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: Text(t.cancel)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _isLoading ? null : _save,
|
||||||
|
child: _isLoading
|
||||||
|
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
|
||||||
|
: Text(isEditing ? t.update : t.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(String title) {
|
||||||
|
return Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBasicInfo(AppLocalizations t) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSectionHeader(t.title),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: InputDecoration(labelText: t.title, hintText: t.title),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return t.title;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _codeController,
|
||||||
|
readOnly: _autoGenerateCode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.code,
|
||||||
|
hintText: t.uniqueCodeNumeric,
|
||||||
|
suffixIcon: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 6),
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
),
|
||||||
|
child: ToggleButtons(
|
||||||
|
isSelected: [_autoGenerateCode, !_autoGenerateCode],
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
constraints: const BoxConstraints(minHeight: 32, minWidth: 64),
|
||||||
|
onPressed: (index) {
|
||||||
|
setState(() { _autoGenerateCode = (index == 0); });
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(t.automatic)),
|
||||||
|
Padding(padding: const EdgeInsets.symmetric(horizontal: 6), child: Text(t.manual)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
validator: (value) {
|
||||||
|
if (!_autoGenerateCode) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return t.personCodeRequired;
|
||||||
|
}
|
||||||
|
if (value.trim().length < 3) {
|
||||||
|
return t.passwordMinLength; // fallback
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^\d+$').hasMatch(value.trim())) {
|
||||||
|
return t.codeMustBeNumeric;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
CurrencyPickerWidget(
|
||||||
|
businessId: widget.businessId,
|
||||||
|
selectedCurrencyId: _currencyId,
|
||||||
|
onChanged: (value) { setState(() { _currencyId = value; }); },
|
||||||
|
label: t.currency,
|
||||||
|
hintText: t.currency,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
decoration: InputDecoration(labelText: t.description, hintText: t.description),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _paymentSwitchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: (t.localeName == 'fa') ? 'شماره سویچ پرداخت' : 'Payment Switch Number',
|
||||||
|
hintText: (t.localeName == 'fa') ? 'شماره سویچ پرداخت' : 'Payment Switch Number',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _paymentTerminalController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: (t.localeName == 'fa') ? 'شماره ترمینال' : 'Terminal Number',
|
||||||
|
hintText: (t.localeName == 'fa') ? 'شماره ترمینال' : 'Terminal Number',
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _merchantIdController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: (t.localeName == 'fa') ? 'شماره پذیرنده' : 'Merchant ID',
|
||||||
|
hintText: (t.localeName == 'fa') ? 'شماره پذیرنده' : 'Merchant ID',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSettings(AppLocalizations t) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildSectionHeader(t.settings),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text(t.active),
|
||||||
|
subtitle: Text(t.active),
|
||||||
|
value: _isActive,
|
||||||
|
onChanged: (value) { setState(() { _isActive = value; }); },
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text(t.isDefault),
|
||||||
|
subtitle: Text(t.defaultConfiguration),
|
||||||
|
value: _isDefault,
|
||||||
|
onChanged: (value) { setState(() { _isDefault = value; }); },
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue