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)
|
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 لیست محصولات با قابلیت فیلتر، انتخاب ستونها و ترتیب آنها",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1035,6 +1035,9 @@
|
||||||
"unit": "واحد",
|
"unit": "واحد",
|
||||||
"minQty": "حداقل تعداد",
|
"minQty": "حداقل تعداد",
|
||||||
"addPriceTitle": "افزودن قیمت",
|
"addPriceTitle": "افزودن قیمت",
|
||||||
"editPriceTitle": "ویرایش قیمت"
|
"editPriceTitle": "ویرایش قیمت",
|
||||||
|
"productDeletedSuccessfully": "کالا/خدمت با موفقیت حذف شد",
|
||||||
|
"productsDeletedSuccessfully": "آیتمهای انتخابشده با موفقیت حذف شدند",
|
||||||
|
"noRowsSelectedError": "هیچ سطری انتخاب نشده است"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 => 'هیچ سطری انتخاب نشده است';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue