almost done prodects part

This commit is contained in:
Hesabix 2025-10-02 03:57:20 +03:30
parent 1363270445
commit c1f3b8287c
8 changed files with 213 additions and 2 deletions

View file

@ -121,6 +121,64 @@ def delete_product_endpoint(
return success_response({"deleted": ok}, request) 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", @router.post("/business/{business_id}/export/excel",
summary="خروجی Excel لیست محصولات", summary="خروجی Excel لیست محصولات",
description="خروجی Excel لیست محصولات با قابلیت فیلتر، انتخاب ستون‌ها و ترتیب آن‌ها", description="خروجی Excel لیست محصولات با قابلیت فیلتر، انتخاب ستون‌ها و ترتیب آن‌ها",

View file

@ -1051,6 +1051,9 @@
"unit": "Unit", "unit": "Unit",
"minQty": "Minimum quantity", "minQty": "Minimum quantity",
"addPriceTitle": "Add price", "addPriceTitle": "Add price",
"editPriceTitle": "Edit price" "editPriceTitle": "Edit price",
"productDeletedSuccessfully": "Product or service deleted successfully",
"productsDeletedSuccessfully": "Selected items deleted successfully",
"noRowsSelectedError": "No rows selected"
} }

View file

@ -1035,6 +1035,9 @@
"unit": "واحد", "unit": "واحد",
"minQty": "حداقل تعداد", "minQty": "حداقل تعداد",
"addPriceTitle": "افزودن قیمت", "addPriceTitle": "افزودن قیمت",
"editPriceTitle": "ویرایش قیمت" "editPriceTitle": "ویرایش قیمت",
"productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد",
"productsDeletedSuccessfully": "آیتم‌های انتخاب‌شده با موفقیت حذف شدند",
"noRowsSelectedError": "هیچ سطری انتخاب نشده است"
} }

View file

@ -5563,6 +5563,24 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Edit price'** /// **'Edit price'**
String get editPriceTitle; 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 class _AppLocalizationsDelegate

View file

@ -2817,4 +2817,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get editPriceTitle => 'Edit price'; 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';
} }

View file

@ -2797,4 +2797,14 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get editPriceTitle => 'ویرایش قیمت'; String get editPriceTitle => 'ویرایش قیمت';
@override
String get productDeletedSuccessfully => 'کالا/خدمت با موفقیت حذف شد';
@override
String get productsDeletedSuccessfully =>
'آیتم‌های انتخاب‌شده با موفقیت حذف شدند';
@override
String get noRowsSelectedError => 'هیچ سطری انتخاب نشده است';
} }

View file

@ -5,6 +5,7 @@ import '../../widgets/data_table/data_table_config.dart';
import '../../widgets/product/product_form_dialog.dart'; import '../../widgets/product/product_form_dialog.dart';
import '../../widgets/product/bulk_price_update_dialog.dart'; import '../../widgets/product/bulk_price_update_dialog.dart';
import '../../widgets/product/product_import_dialog.dart'; import '../../widgets/product/product_import_dialog.dart';
import '../../core/api_client.dart';
import 'price_lists_page.dart'; import 'price_lists_page.dart';
import '../../core/auth_store.dart'; import '../../core/auth_store.dart';
import '../../utils/number_formatters.dart'; import '../../utils/number_formatters.dart';
@ -129,12 +130,103 @@ class _ProductsPageState extends State<ProductsPage> {
); );
}, },
), ),
DataTableAction(
icon: Icons.delete_outline,
label: AppLocalizations.of(context).delete,
isDestructive: true,
onTap: (row) async {
final t = AppLocalizations.of(context);
final confirm = await showDialog<bool>(
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<Map<String, dynamic>>(
'/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'], searchFields: const ['code', 'name', 'description'],
filterFields: const ['item_type', 'category_id'], filterFields: const ['item_type', 'category_id'],
defaultPageSize: 20, defaultPageSize: 20,
customHeaderActions: [ 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<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] as 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.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<Map<String, dynamic>>(
'/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( Tooltip(
message: t.importFromExcel, message: t.importFromExcel,
child: IconButton( child: IconButton(

View file

@ -91,6 +91,22 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
_fetchData(); _fetchData();
} }
// Public helpers for external widgets (via GlobalKey)
List<int> getSelectedRowIndices() {
return _selectedRows.toList();
}
List<T> getSelectedItems() {
if (_selectedRows.isEmpty) return const [];
final list = <T>[];
for (final i in _selectedRows) {
if (i >= 0 && i < _items.length) {
list.add(_items[i]);
}
}
return list;
}
@override @override
void dispose() { void dispose() {
_searchCtrl.dispose(); _searchCtrl.dispose();