almost done prodects part
This commit is contained in:
parent
1363270445
commit
c1f3b8287c
|
|
@ -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 لیست محصولات با قابلیت فیلتر، انتخاب ستونها و ترتیب آنها",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1035,6 +1035,9 @@
|
|||
"unit": "واحد",
|
||||
"minQty": "حداقل تعداد",
|
||||
"addPriceTitle": "افزودن قیمت",
|
||||
"editPriceTitle": "ویرایش قیمت"
|
||||
"editPriceTitle": "ویرایش قیمت",
|
||||
"productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد",
|
||||
"productsDeletedSuccessfully": "آیتمهای انتخابشده با موفقیت حذف شدند",
|
||||
"noRowsSelectedError": "هیچ سطری انتخاب نشده است"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2797,4 +2797,14 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get editPriceTitle => 'ویرایش قیمت';
|
||||
|
||||
@override
|
||||
String get productDeletedSuccessfully => 'کالا/خدمت با موفقیت حذف شد';
|
||||
|
||||
@override
|
||||
String get productsDeletedSuccessfully =>
|
||||
'آیتمهای انتخابشده با موفقیت حذف شدند';
|
||||
|
||||
@override
|
||||
String get noRowsSelectedError => 'هیچ سطری انتخاب نشده است';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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'],
|
||||
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<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(
|
||||
message: t.importFromExcel,
|
||||
child: IconButton(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,22 @@ class _DataTableWidgetState<T> extends State<DataTableWidget<T>> {
|
|||
_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
|
||||
void dispose() {
|
||||
_searchCtrl.dispose();
|
||||
|
|
|
|||
Loading…
Reference in a new issue