From c1f3b8287c939170927ea288f06022452f099273 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Thu, 2 Oct 2025 03:57:20 +0330 Subject: [PATCH] almost done prodects part --- hesabixAPI/adapters/api/v1/products.py | 58 ++++++++++++ hesabixUI/hesabix_ui/lib/l10n/app_en.arb | 5 +- hesabixUI/hesabix_ui/lib/l10n/app_fa.arb | 5 +- .../lib/l10n/app_localizations.dart | 18 ++++ .../lib/l10n/app_localizations_en.dart | 11 +++ .../lib/l10n/app_localizations_fa.dart | 10 ++ .../lib/pages/business/products_page.dart | 92 +++++++++++++++++++ .../widgets/data_table/data_table_widget.dart | 16 ++++ 8 files changed, 213 insertions(+), 2 deletions(-) diff --git a/hesabixAPI/adapters/api/v1/products.py b/hesabixAPI/adapters/api/v1/products.py index d4ceda1..d8b734c 100644 --- a/hesabixAPI/adapters/api/v1/products.py +++ b/hesabixAPI/adapters/api/v1/products.py @@ -121,6 +121,64 @@ def delete_product_endpoint( return success_response({"deleted": ok}, request) +@router.post("/business/{business_id}/bulk-delete", + summary="حذف گروهی محصولات", + description="حذف چندین آیتم بر اساس شناسه‌ها یا کدها", +) +@require_business_access("business_id") +def bulk_delete_products_endpoint( + request: Request, + business_id: int, + body: Dict[str, Any], + ctx: AuthContext = Depends(get_current_user), + db: Session = Depends(get_db), +) -> Dict[str, Any]: + if not ctx.has_business_permission("inventory", "delete"): + raise ApiError("FORBIDDEN", "Missing business permission: inventory.delete", http_status=403) + + from sqlalchemy import and_ as _and + from adapters.db.models.product import Product + + ids = body.get("ids") + codes = body.get("codes") + deleted = 0 + skipped = 0 + + if not ids and not codes: + return success_response({"deleted": 0, "skipped": 0}, request) + + # Normalize inputs + if isinstance(ids, list): + ids = [int(x) for x in ids if isinstance(x, (int, str)) and str(x).isdigit()] + else: + ids = [] + if isinstance(codes, list): + codes = [str(x).strip() for x in codes if str(x).strip()] + else: + codes = [] + + # Delete by IDs first + if ids: + for pid in ids: + ok = delete_product(db, pid, business_id) + if ok: + deleted += 1 + else: + skipped += 1 + + # Delete by codes + if codes: + items = db.query(Product).filter(_and(Product.business_id == business_id, Product.code.in_(codes))).all() + for obj in items: + try: + db.delete(obj) + deleted += 1 + except Exception: + skipped += 1 + db.commit() + + return success_response({"deleted": deleted, "skipped": skipped}, request) + @router.post("/business/{business_id}/export/excel", summary="خروجی Excel لیست محصولات", description="خروجی Excel لیست محصولات با قابلیت فیلتر، انتخاب ستون‌ها و ترتیب آن‌ها", diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb index aa7fdf8..d0725d2 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_en.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_en.arb @@ -1051,6 +1051,9 @@ "unit": "Unit", "minQty": "Minimum quantity", "addPriceTitle": "Add price", - "editPriceTitle": "Edit price" + "editPriceTitle": "Edit price", + "productDeletedSuccessfully": "Product or service deleted successfully", + "productsDeletedSuccessfully": "Selected items deleted successfully", + "noRowsSelectedError": "No rows selected" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb index 72042d1..c24d07c 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb +++ b/hesabixUI/hesabix_ui/lib/l10n/app_fa.arb @@ -1035,6 +1035,9 @@ "unit": "واحد", "minQty": "حداقل تعداد", "addPriceTitle": "افزودن قیمت", - "editPriceTitle": "ویرایش قیمت" + "editPriceTitle": "ویرایش قیمت", + "productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد", + "productsDeletedSuccessfully": "آیتم‌های انتخاب‌شده با موفقیت حذف شدند", + "noRowsSelectedError": "هیچ سطری انتخاب نشده است" } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart index 0f32edc..fc36f77 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations.dart @@ -5563,6 +5563,24 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Edit price'** String get editPriceTitle; + + /// No description provided for @productDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Product or service deleted successfully'** + String get productDeletedSuccessfully; + + /// No description provided for @productsDeletedSuccessfully. + /// + /// In en, this message translates to: + /// **'Selected items deleted successfully'** + String get productsDeletedSuccessfully; + + /// No description provided for @noRowsSelectedError. + /// + /// In en, this message translates to: + /// **'No rows selected'** + String get noRowsSelectedError; } class _AppLocalizationsDelegate diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart index 5e93aa8..bb216e8 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_en.dart @@ -2817,4 +2817,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get editPriceTitle => 'Edit price'; + + @override + String get productDeletedSuccessfully => + 'Product or service deleted successfully'; + + @override + String get productsDeletedSuccessfully => + 'Selected items deleted successfully'; + + @override + String get noRowsSelectedError => 'No rows selected'; } diff --git a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart index 0017fd6..397b3ef 100644 --- a/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart +++ b/hesabixUI/hesabix_ui/lib/l10n/app_localizations_fa.dart @@ -2797,4 +2797,14 @@ class AppLocalizationsFa extends AppLocalizations { @override String get editPriceTitle => 'ویرایش قیمت'; + + @override + String get productDeletedSuccessfully => 'کالا/خدمت با موفقیت حذف شد'; + + @override + String get productsDeletedSuccessfully => + 'آیتم‌های انتخاب‌شده با موفقیت حذف شدند'; + + @override + String get noRowsSelectedError => 'هیچ سطری انتخاب نشده است'; } diff --git a/hesabixUI/hesabix_ui/lib/pages/business/products_page.dart b/hesabixUI/hesabix_ui/lib/pages/business/products_page.dart index dae41af..ab9b485 100644 --- a/hesabixUI/hesabix_ui/lib/pages/business/products_page.dart +++ b/hesabixUI/hesabix_ui/lib/pages/business/products_page.dart @@ -5,6 +5,7 @@ import '../../widgets/data_table/data_table_config.dart'; import '../../widgets/product/product_form_dialog.dart'; import '../../widgets/product/bulk_price_update_dialog.dart'; import '../../widgets/product/product_import_dialog.dart'; +import '../../core/api_client.dart'; import 'price_lists_page.dart'; import '../../core/auth_store.dart'; import '../../utils/number_formatters.dart'; @@ -129,12 +130,103 @@ class _ProductsPageState extends State { ); }, ), + DataTableAction( + icon: Icons.delete_outline, + label: AppLocalizations.of(context).delete, + isDestructive: true, + onTap: (row) async { + final t = AppLocalizations.of(context); + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(t.deleteProducts), + content: Text(t.deleteConfirm('"${row['name'] ?? 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 { + final api = ApiClient(); + await api.delete>( + '/products/business/${widget.businessId}/${row['id']}', + ); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.productDeletedSuccessfully))); + try { ( _tableKey.currentState as dynamic)?.refresh(); } catch (_) {} + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${t.error}: $e'))); + } + } + }, + ), ]), ], searchFields: const ['code', 'name', 'description'], filterFields: const ['item_type', 'category_id'], defaultPageSize: 20, customHeaderActions: [ + if (widget.authStore.canDeleteSection('products')) + Tooltip( + message: AppLocalizations.of(context).deleteProducts, + child: IconButton( + onPressed: () async { + final t = AppLocalizations.of(context); + // Collect selected row IDs via DataTableWidget public API + try { + // Access current table state to read selected rows and items + final state = _tableKey.currentState as dynamic; + final selectedIndices = (state?.getSelectedRowIndices() as List?) ?? const []; + final items = (state?.getSelectedItems() as List?) ?? const []; + if (selectedIndices.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.noRowsSelectedError))); + return; + } + final ids = []; + for (final i in selectedIndices) { + if (i >= 0 && i < items.length) { + final row = items[i] as Map; + final id = row['id']; + if (id is int) ids.add(id); + } + } + if (ids.isEmpty) return; + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(t.deleteProducts), + content: Text(t.deleteConfirm('${ids.length}')), + 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 api = ApiClient(); + await api.post>( + '/products/business/${widget.businessId}/bulk-delete', + data: { 'ids': ids }, + ); + try { ( _tableKey.currentState as dynamic)?.refresh(); } catch (_) {} + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.productsDeletedSuccessfully))); + } + } catch (e) { + if (mounted) { + final t = AppLocalizations.of(context); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${t.error}: $e'))); + } + } + }, + icon: const Icon(Icons.delete_sweep_outlined), + ), + ), Tooltip( message: t.importFromExcel, child: IconButton( diff --git a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart index 761f9f8..eb8a286 100644 --- a/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart +++ b/hesabixUI/hesabix_ui/lib/widgets/data_table/data_table_widget.dart @@ -91,6 +91,22 @@ class _DataTableWidgetState extends State> { _fetchData(); } + // Public helpers for external widgets (via GlobalKey) + List getSelectedRowIndices() { + return _selectedRows.toList(); + } + + List getSelectedItems() { + if (_selectedRows.isEmpty) return const []; + final list = []; + for (final i in _selectedRows) { + if (i >= 0 && i < _items.length) { + list.add(_items[i]); + } + } + return list; + } + @override void dispose() { _searchCtrl.dispose();