mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 07:07:19 +00:00
Enhance Space Management Features with Tag Assignment Improvements:
- Introduced UUID for ProductAllocation to ensure unique identification. - Refactored AssignTagsDialog to manage tag assignments and validation more effectively, including error handling for empty tags and duplicate tag usage. - Updated AssignTagsTable to support dynamic product allocation management and improved UI interactions. - Enhanced AddDeviceTypeWidget to maintain selected products and handle increment/decrement actions, improving user experience during device type selection. - Added AssignTagsErrorMessages widget for better error visibility in tag assignment process.
This commit is contained in:
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class SpaceDetailsModel extends Equatable {
|
class SpaceDetailsModel extends Equatable {
|
||||||
final String uuid;
|
final String uuid;
|
||||||
@ -31,8 +32,8 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
spaceName: json['spaceName'] as String,
|
spaceName: json['spaceName'] as String,
|
||||||
icon: json['icon'] as String,
|
icon: json['icon'] as String,
|
||||||
productAllocations: (json['productAllocations'] as List)
|
productAllocations: (json['productAllocations'] as List)
|
||||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
subspaces: (json['subspaces'] as List)
|
subspaces: (json['subspaces'] as List)
|
||||||
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -70,16 +71,19 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ProductAllocation extends Equatable {
|
class ProductAllocation extends Equatable {
|
||||||
|
final String uuid;
|
||||||
final Product product;
|
final Product product;
|
||||||
final Tag tag;
|
final Tag tag;
|
||||||
|
|
||||||
const ProductAllocation({
|
const ProductAllocation({
|
||||||
|
required this.uuid,
|
||||||
required this.product,
|
required this.product,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
||||||
return ProductAllocation(
|
return ProductAllocation(
|
||||||
|
uuid: json['uuid'] as String? ?? const Uuid().v4(),
|
||||||
product: Product.fromJson(json['product'] as Map<String, dynamic>),
|
product: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||||
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
@ -87,23 +91,26 @@ class ProductAllocation extends Equatable {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
|
'uuid': uuid,
|
||||||
'product': product.toJson(),
|
'product': product.toJson(),
|
||||||
'tag': tag.toJson(),
|
'tag': tag.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ProductAllocation copyWith({
|
ProductAllocation copyWith({
|
||||||
|
String? uuid,
|
||||||
Product? product,
|
Product? product,
|
||||||
Tag? tag,
|
Tag? tag,
|
||||||
}) {
|
}) {
|
||||||
return ProductAllocation(
|
return ProductAllocation(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
product: product ?? this.product,
|
product: product ?? this.product,
|
||||||
tag: tag ?? this.tag,
|
tag: tag ?? this.tag,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [product, tag];
|
List<Object?> get props => [uuid, product, tag];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Subspace extends Equatable {
|
class Subspace extends Equatable {
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:syncrow_web/common/edit_chip.dart';
|
import 'package:syncrow_web/common/edit_chip.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
import 'package:syncrow_web/utils/enum/device_types.dart';
|
import 'package:syncrow_web/utils/enum/device_types.dart';
|
||||||
@ -19,11 +21,18 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final productAllocations = space.productAllocations;
|
final allAllocations = [
|
||||||
final subspaces = space.subspaces;
|
...space.productAllocations,
|
||||||
final isAnySubspaceHasProductAllocations =
|
...space.subspaces.expand((s) => s.productAllocations),
|
||||||
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
|
];
|
||||||
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
|
|
||||||
|
if (allAllocations.isNotEmpty) {
|
||||||
|
final productCounts = <String, int>{};
|
||||||
|
for (final allocation in allAllocations) {
|
||||||
|
final productType = allocation.product.productType;
|
||||||
|
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@ -39,20 +48,23 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
|||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
...productAllocations.map(
|
...productCounts.entries.map((entry) {
|
||||||
(entry) => Chip(
|
final productType = entry.key;
|
||||||
|
final count = entry.value;
|
||||||
|
return Chip(
|
||||||
avatar: SizedBox(
|
avatar: SizedBox(
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
_getDeviceIcon(entry.product.productType),
|
_getDeviceIcon(productType),
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
entry.product.productType,
|
'x$count',
|
||||||
style: context.textTheme.bodySmall
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
?.copyWith(color: ColorsManager.spaceColor),
|
color: ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: ColorsManager.whiteColors,
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@ -61,11 +73,9 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
|||||||
color: ColorsManager.spaceColor,
|
color: ColorsManager.spaceColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
EditChip(
|
EditChip(onTap: () => _showAssignTagsDialog(context)),
|
||||||
onTap: () => _showAssignTagsDialog(context),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -87,10 +97,16 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showAssignTagsDialog(BuildContext context) {
|
void _showAssignTagsDialog(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<SpaceDetailsModel>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AssignTagsDialog(space: space),
|
builder: (context) => AssignTagsDialog(space: space),
|
||||||
);
|
).then((resultSpace) {
|
||||||
|
if (resultSpace != null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getDeviceIcon(String productType) =>
|
String _getDeviceIcon(String productType) =>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
|
||||||
@ -8,9 +9,33 @@ import 'package:syncrow_web/services/api/http_service.dart';
|
|||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class AddDeviceTypeWidget extends StatelessWidget {
|
class AddDeviceTypeWidget extends StatefulWidget {
|
||||||
const AddDeviceTypeWidget({super.key});
|
const AddDeviceTypeWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||||
|
final Map<Product, int> _selectedProducts = {};
|
||||||
|
|
||||||
|
void _onIncrement(Product product) {
|
||||||
|
setState(() {
|
||||||
|
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDecrement(Product product) {
|
||||||
|
setState(() {
|
||||||
|
if ((_selectedProducts[product] ?? 0) > 0) {
|
||||||
|
_selectedProducts[product] = _selectedProducts[product]! - 1;
|
||||||
|
if (_selectedProducts[product] == 0) {
|
||||||
|
_selectedProducts.remove(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
@ -22,10 +47,12 @@ class AddDeviceTypeWidget extends StatelessWidget {
|
|||||||
backgroundColor: ColorsManager.whiteColors,
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
content: BlocBuilder<ProductsBloc, ProductsState>(
|
content: BlocBuilder<ProductsBloc, ProductsState>(
|
||||||
builder: (context, state) => switch (state) {
|
builder: (context, state) => switch (state) {
|
||||||
ProductsInitial() => _buildLoading(context),
|
ProductsInitial() || ProductsLoading() => _buildLoading(context),
|
||||||
ProductsLoading() => _buildLoading(context),
|
|
||||||
ProductsLoaded(:final products) => ProductsGrid(
|
ProductsLoaded(:final products) => ProductsGrid(
|
||||||
products: products,
|
products: products,
|
||||||
|
selectedProducts: _selectedProducts,
|
||||||
|
onIncrement: _onIncrement,
|
||||||
|
onDecrement: _onDecrement,
|
||||||
),
|
),
|
||||||
ProductsFailure(:final errorMessage) => _buildFailure(
|
ProductsFailure(:final errorMessage) => _buildFailure(
|
||||||
context,
|
context,
|
||||||
@ -35,7 +62,12 @@ class AddDeviceTypeWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
SpaceDetailsActionButtons(
|
SpaceDetailsActionButtons(
|
||||||
onSave: () {},
|
onSave: () {
|
||||||
|
final result = _selectedProducts.entries
|
||||||
|
.expand((entry) => List.generate(entry.value, (_) => entry.key))
|
||||||
|
.toList();
|
||||||
|
Navigator.of(context).pop(result);
|
||||||
|
},
|
||||||
onCancel: Navigator.of(context).pop,
|
onCancel: Navigator.of(context).pop,
|
||||||
saveButtonLabel: 'Next',
|
saveButtonLabel: 'Next',
|
||||||
),
|
),
|
||||||
|
@ -1,36 +1,230 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AssignTagsDialog extends StatelessWidget {
|
class AssignTagsDialog extends StatefulWidget {
|
||||||
const AssignTagsDialog({required this.space, super.key});
|
const AssignTagsDialog({required this.space, super.key});
|
||||||
|
|
||||||
final SpaceDetailsModel space;
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||||
|
late SpaceDetailsModel _space;
|
||||||
|
final Map<String, String> _validationErrors = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_space = widget.space.copyWith(
|
||||||
|
productAllocations:
|
||||||
|
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
|
||||||
|
subspaces: widget.space.subspaces
|
||||||
|
.map(
|
||||||
|
(s) => s.copyWith(
|
||||||
|
productAllocations:
|
||||||
|
s.productAllocations.map((e) => e.copyWith()).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _validateAllTags() {
|
||||||
|
final newErrors = <String, String>{};
|
||||||
|
final allAllocations = [
|
||||||
|
..._space.productAllocations,
|
||||||
|
..._space.subspaces.expand((s) => s.productAllocations),
|
||||||
|
];
|
||||||
|
|
||||||
|
final allocationsByProductType = <String, List<ProductAllocation>>{};
|
||||||
|
for (final allocation in allAllocations) {
|
||||||
|
(allocationsByProductType[allocation.product.productType] ??= [])
|
||||||
|
.add(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final productType in allocationsByProductType.keys) {
|
||||||
|
final allocations = allocationsByProductType[productType]!;
|
||||||
|
final tagCounts = <String, int>{};
|
||||||
|
|
||||||
|
for (final allocation in allocations) {
|
||||||
|
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||||
|
if (tagName.isEmpty) {
|
||||||
|
newErrors[allocation.uuid] =
|
||||||
|
'Tag for ${allocation.product.name} cannot be empty.';
|
||||||
|
} else {
|
||||||
|
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final allocation in allocations) {
|
||||||
|
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||||
|
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
|
||||||
|
newErrors[allocation.uuid] =
|
||||||
|
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_validationErrors
|
||||||
|
..clear()
|
||||||
|
..addAll(newErrors);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTagChange(String allocationUuid, Tag newTag) {
|
||||||
|
setState(() {
|
||||||
|
var index =
|
||||||
|
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
final allocation = _space.productAllocations[index];
|
||||||
|
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||||
|
} else {
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
index = subspace.productAllocations
|
||||||
|
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
final allocation = subspace.productAllocations[index];
|
||||||
|
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
|
||||||
|
setState(() {
|
||||||
|
ProductAllocation? allocationToMove;
|
||||||
|
|
||||||
|
var index =
|
||||||
|
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
allocationToMove = _space.productAllocations.removeAt(index);
|
||||||
|
} else {
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
index = subspace.productAllocations
|
||||||
|
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
if (index != -1) {
|
||||||
|
allocationToMove = subspace.productAllocations.removeAt(index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocationToMove == null) return;
|
||||||
|
|
||||||
|
if (newSubspaceUuid == null) {
|
||||||
|
_space.productAllocations.add(allocationToMove);
|
||||||
|
} else {
|
||||||
|
_space.subspaces
|
||||||
|
.firstWhere((s) => s.uuid == newSubspaceUuid)
|
||||||
|
.productAllocations
|
||||||
|
.add(allocationToMove);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleProductDelete(String allocationUuid) {
|
||||||
|
setState(() {
|
||||||
|
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
|
||||||
|
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
subspace.productAllocations.removeWhere(
|
||||||
|
(pa) => pa.uuid == allocationUuid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final allProductAllocations = [
|
||||||
|
..._space.productAllocations,
|
||||||
|
..._space.subspaces.expand((s) => s.productAllocations),
|
||||||
|
];
|
||||||
|
|
||||||
|
final productLocations = <String, String?>{};
|
||||||
|
for (final pa in _space.productAllocations) {
|
||||||
|
productLocations[pa.uuid] = null;
|
||||||
|
}
|
||||||
|
for (final subspace in _space.subspaces) {
|
||||||
|
for (final pa in subspace.productAllocations) {
|
||||||
|
productLocations[pa.uuid] = subspace.uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasErrors = _validationErrors.isNotEmpty;
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Assign Tags'),
|
title: const SelectableText('Assign Tags'),
|
||||||
content: ConstrainedBox(
|
content: ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
maxWidth: context.screenWidth * 0.6,
|
maxWidth: context.screenWidth * 0.6,
|
||||||
minWidth: context.screenWidth * 0.6,
|
minWidth: context.screenWidth * 0.6,
|
||||||
maxHeight: context.screenHeight * 0.8,
|
maxHeight: context.screenHeight * 0.8,
|
||||||
),
|
),
|
||||||
child: AssignTagsTable(productAllocations: space.productAllocations),
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: AssignTagsTable(
|
||||||
|
productAllocations: allProductAllocations,
|
||||||
|
subspaces: _space.subspaces,
|
||||||
|
productLocations: productLocations,
|
||||||
|
onTagSelected: _handleTagChange,
|
||||||
|
onLocationSelected: _handleLocationChange,
|
||||||
|
onProductDeleted: _handleProductDelete,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasErrors)
|
||||||
|
AssignTagsErrorMessages(
|
||||||
|
errorMessages: _validationErrors.values.toSet().toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
SpaceDetailsActionButtons(
|
SpaceDetailsActionButtons(
|
||||||
onSave: () {},
|
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
|
||||||
onCancel: () => showDialog<void>(
|
onCancel: () async {
|
||||||
context: context,
|
final newProducts = await showDialog<List<Product>>(
|
||||||
builder: (context) => const AddDeviceTypeWidget(),
|
context: context,
|
||||||
),
|
builder: (context) => const AddDeviceTypeWidget(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newProducts == null || newProducts.isEmpty) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
for (final product in newProducts) {
|
||||||
|
_space.productAllocations.add(
|
||||||
|
ProductAllocation(
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
product: product,
|
||||||
|
tag: Tag.empty(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_validateAllTags();
|
||||||
|
},
|
||||||
cancelButtonLabel: 'Add New Device',
|
cancelButtonLabel: 'Add New Device',
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class AssignTagsErrorMessages extends StatelessWidget {
|
||||||
|
const AssignTagsErrorMessages({super.key, required this.errorMessages});
|
||||||
|
|
||||||
|
final List<String> errorMessages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: errorMessages
|
||||||
|
.map(
|
||||||
|
(error) => Text(
|
||||||
|
'- $error',
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,50 +3,35 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:syncrow_web/common/dialog_dropdown.dart';
|
import 'package:syncrow_web/common/dialog_dropdown.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class AssignTagsTable extends StatefulWidget {
|
class AssignTagsTable extends StatelessWidget {
|
||||||
const AssignTagsTable({
|
const AssignTagsTable({
|
||||||
required this.productAllocations,
|
required this.productAllocations,
|
||||||
|
required this.subspaces,
|
||||||
|
required this.productLocations,
|
||||||
|
required this.onTagSelected,
|
||||||
|
required this.onLocationSelected,
|
||||||
|
required this.onProductDeleted,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<ProductAllocation> productAllocations;
|
final List<ProductAllocation> productAllocations;
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
final Map<String, String?> productLocations;
|
||||||
|
final void Function(String, Tag) onTagSelected;
|
||||||
|
final void Function(String, String?) onLocationSelected;
|
||||||
|
final void Function(String) onProductDeleted;
|
||||||
|
|
||||||
@override
|
DataColumn _buildDataColumn(BuildContext context, String label) {
|
||||||
State<AssignTagsTable> createState() => _AssignTagsTableState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AssignTagsTableState extends State<AssignTagsTable> {
|
|
||||||
List<TextEditingController> _controllers = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controllers = List.generate(
|
|
||||||
widget.productAllocations.length,
|
|
||||||
(index) => TextEditingController(
|
|
||||||
text: widget.productAllocations[index].product.name,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
for (final controller in _controllers) {
|
|
||||||
controller.dispose();
|
|
||||||
}
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
DataColumn _buildDataColumn(String label) {
|
|
||||||
return DataColumn(
|
return DataColumn(
|
||||||
label: SelectableText(label, style: context.textTheme.bodyMedium));
|
label: SelectableText(label, style: context.textTheme.bodyMedium),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -70,19 +55,19 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
|
|||||||
headingRowColor: WidgetStateProperty.all(
|
headingRowColor: WidgetStateProperty.all(
|
||||||
ColorsManager.dataHeaderGrey,
|
ColorsManager.dataHeaderGrey,
|
||||||
),
|
),
|
||||||
key: ValueKey(widget.productAllocations.length),
|
key: ValueKey(productAllocations.length),
|
||||||
border: TableBorder.all(
|
border: TableBorder.all(
|
||||||
color: ColorsManager.dataHeaderGrey,
|
color: ColorsManager.dataHeaderGrey,
|
||||||
width: 1,
|
width: 1,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
columns: [
|
columns: [
|
||||||
_buildDataColumn('#'),
|
_buildDataColumn(context, '#'),
|
||||||
_buildDataColumn('Device'),
|
_buildDataColumn(context, 'Device'),
|
||||||
_buildDataColumn('Tag'),
|
_buildDataColumn(context, 'Tag'),
|
||||||
_buildDataColumn('Location'),
|
_buildDataColumn(context, 'Location'),
|
||||||
],
|
],
|
||||||
rows: widget.productAllocations.isEmpty
|
rows: productAllocations.isEmpty
|
||||||
? [
|
? [
|
||||||
DataRow(
|
DataRow(
|
||||||
cells: [
|
cells: [
|
||||||
@ -102,11 +87,33 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: List.generate(widget.productAllocations.length, (index) {
|
: List.generate(productAllocations.length, (index) {
|
||||||
final productAllocation = widget.productAllocations[index];
|
final productAllocation = productAllocations[index];
|
||||||
final controller = _controllers[index];
|
final allocationUuid = productAllocation.uuid;
|
||||||
|
|
||||||
|
final availableTags = tags
|
||||||
|
.where(
|
||||||
|
(tag) =>
|
||||||
|
!productAllocations
|
||||||
|
.where((p) =>
|
||||||
|
p.product.productType ==
|
||||||
|
productAllocation.product.productType)
|
||||||
|
.map((p) => p.tag.name.toLowerCase())
|
||||||
|
.contains(tag.name.toLowerCase()) ||
|
||||||
|
tag.uuid == productAllocation.tag.uuid,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final currentLocationUuid =
|
||||||
|
productLocations[allocationUuid];
|
||||||
|
final currentLocationName = currentLocationUuid == null
|
||||||
|
? 'Main Space'
|
||||||
|
: subspaces
|
||||||
|
.firstWhere((s) => s.uuid == currentLocationUuid)
|
||||||
|
.name;
|
||||||
|
|
||||||
return DataRow(
|
return DataRow(
|
||||||
|
key: ValueKey(allocationUuid),
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(Text((index + 1).toString())),
|
DataCell(Text((index + 1).toString())),
|
||||||
DataCell(
|
DataCell(
|
||||||
@ -136,7 +143,7 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
|
|||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Delete the product allocation
|
onProductDeleted(allocationUuid);
|
||||||
},
|
},
|
||||||
tooltip: 'Delete Tag',
|
tooltip: 'Delete Tag',
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
@ -151,14 +158,13 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
|
|||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ProductTagField(
|
child: ProductTagField(
|
||||||
key: ValueKey(
|
key: ValueKey('dropdown_$allocationUuid'),
|
||||||
'dropdown_${const Uuid().v4()}_$index'),
|
|
||||||
productName: productAllocation.product.uuid,
|
productName: productAllocation.product.uuid,
|
||||||
initialValue: null,
|
initialValue: productAllocation.tag,
|
||||||
onSelected: (value) {
|
onSelected: (newTag) {
|
||||||
controller.text = value.name;
|
onTagSelected(allocationUuid, newTag);
|
||||||
},
|
},
|
||||||
items: tags,
|
items: availableTags,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -166,13 +172,22 @@ class _AssignTagsTableState extends State<AssignTagsTable> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DialogDropdown(
|
child: DialogDropdown(
|
||||||
items: const [],
|
items: [
|
||||||
// items: widget.locations,
|
'Main Space',
|
||||||
selectedValue:
|
...subspaces.map((s) => s.name)
|
||||||
productAllocation.tag.name.isEmpty
|
],
|
||||||
? 'Main Space'
|
selectedValue: currentLocationName,
|
||||||
: productAllocation.tag.name,
|
onSelected: (newLocationName) {
|
||||||
onSelected: (value) {},
|
final newSubspaceUuid = newLocationName ==
|
||||||
|
'Main Space'
|
||||||
|
? null
|
||||||
|
: subspaces
|
||||||
|
.firstWhere(
|
||||||
|
(s) => s.name == newLocationName)
|
||||||
|
.uuid;
|
||||||
|
onLocationSelected(
|
||||||
|
allocationUuid, newSubspaceUuid);
|
||||||
|
},
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -26,42 +26,44 @@ class _ProductTagFieldState extends State<ProductTagField> {
|
|||||||
OverlayEntry? _overlayEntry;
|
OverlayEntry? _overlayEntry;
|
||||||
final TextEditingController _controller = TextEditingController();
|
final TextEditingController _controller = TextEditingController();
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
List<Tag> _filteredItems = [];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_controller.text = widget.initialValue?.name ?? '';
|
_controller.text = widget.initialValue?.name ?? '';
|
||||||
|
_focusNode.addListener(_handleFocusChange);
|
||||||
_filterItems();
|
|
||||||
|
|
||||||
_focusNode.addListener(() {
|
|
||||||
if (!_focusNode.hasFocus) {
|
|
||||||
final selectedTag = _filteredItems.firstWhere(
|
|
||||||
(tag) => tag.name == _controller.text,
|
|
||||||
orElse: () => Tag(
|
|
||||||
name: _controller.text,
|
|
||||||
uuid: '',
|
|
||||||
createdAt: '',
|
|
||||||
updatedAt: '',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
widget.onSelected(selectedTag);
|
|
||||||
_closeDropdown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_focusNode.removeListener(_handleFocusChange);
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
_overlayEntry?.remove();
|
||||||
_overlayEntry = null;
|
_overlayEntry = null;
|
||||||
_isOpen = false;
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _filterItems() => setState(() => _filteredItems = widget.items);
|
void _handleFocusChange() {
|
||||||
|
if (!_focusNode.hasFocus) {
|
||||||
|
_submit(_controller.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _submit(String value) {
|
||||||
|
final lowerCaseValue = value.toLowerCase();
|
||||||
|
final selectedTag = widget.items.firstWhere(
|
||||||
|
(tag) => tag.name.toLowerCase() == lowerCaseValue,
|
||||||
|
orElse: () => Tag(
|
||||||
|
name: value,
|
||||||
|
uuid: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
widget.onSelected(selectedTag);
|
||||||
|
_closeDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
void _toggleDropdown() {
|
void _toggleDropdown() {
|
||||||
if (_isOpen) {
|
if (_isOpen) {
|
||||||
@ -74,14 +76,14 @@ class _ProductTagFieldState extends State<ProductTagField> {
|
|||||||
void _openDropdown() {
|
void _openDropdown() {
|
||||||
_overlayEntry = _createOverlayEntry();
|
_overlayEntry = _createOverlayEntry();
|
||||||
Overlay.of(context).insert(_overlayEntry!);
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
_isOpen = true;
|
setState(() => _isOpen = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeDropdown() {
|
void _closeDropdown() {
|
||||||
if (_isOpen && _overlayEntry != null) {
|
if (_isOpen) {
|
||||||
_overlayEntry!.remove();
|
_overlayEntry?.remove();
|
||||||
_overlayEntry = null;
|
_overlayEntry = null;
|
||||||
_isOpen = false;
|
setState(() => _isOpen = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,24 +105,7 @@ class _ProductTagFieldState extends State<ProductTagField> {
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onFieldSubmitted: (value) {
|
onFieldSubmitted: _submit,
|
||||||
final selectedTag = _filteredItems.firstWhere(
|
|
||||||
(tag) => tag.name == value,
|
|
||||||
orElse: () =>
|
|
||||||
Tag(name: value, uuid: '', createdAt: '', updatedAt: ''));
|
|
||||||
widget.onSelected(selectedTag);
|
|
||||||
_closeDropdown();
|
|
||||||
},
|
|
||||||
onTapOutside: (event) {
|
|
||||||
widget.onSelected(_filteredItems.firstWhere(
|
|
||||||
(tag) => tag.name == _controller.text,
|
|
||||||
orElse: () => Tag(
|
|
||||||
name: _controller.text,
|
|
||||||
uuid: '',
|
|
||||||
createdAt: '',
|
|
||||||
updatedAt: '')));
|
|
||||||
_closeDropdown();
|
|
||||||
},
|
|
||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Enter or Select a tag',
|
hintText: 'Enter or Select a tag',
|
||||||
@ -159,41 +144,33 @@ class _ProductTagFieldState extends State<ProductTagField> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
color: ColorsManager.whiteColors,
|
color: ColorsManager.whiteColors,
|
||||||
constraints: const BoxConstraints(maxHeight: 200.0),
|
constraints: const BoxConstraints(maxHeight: 200.0),
|
||||||
child: StatefulBuilder(
|
child: ListView.builder(
|
||||||
builder: (context, setStateDropdown) {
|
shrinkWrap: true,
|
||||||
return ListView.builder(
|
itemCount: widget.items.length,
|
||||||
shrinkWrap: true,
|
itemBuilder: (context, index) {
|
||||||
itemCount: _filteredItems.length,
|
final tag = widget.items[index];
|
||||||
itemBuilder: (context, index) {
|
return Container(
|
||||||
final tag = _filteredItems[index];
|
decoration: const BoxDecoration(
|
||||||
|
border: Border(
|
||||||
return Container(
|
bottom: BorderSide(
|
||||||
decoration: const BoxDecoration(
|
color: ColorsManager.lightGrayBorderColor,
|
||||||
border: Border(
|
width: 1.0,
|
||||||
bottom: BorderSide(
|
|
||||||
color: ColorsManager.lightGrayBorderColor,
|
|
||||||
width: 1.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: ListTile(
|
),
|
||||||
title: Text(
|
),
|
||||||
tag.name,
|
child: ListTile(
|
||||||
style: context.textTheme.bodyMedium?.copyWith(
|
title: Text(
|
||||||
color: ColorsManager.textPrimaryColor,
|
tag.name,
|
||||||
),
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
),
|
color: ColorsManager.textPrimaryColor,
|
||||||
onTap: () {
|
|
||||||
_controller.text = tag.name;
|
|
||||||
widget.onSelected(tag);
|
|
||||||
setState(() {
|
|
||||||
_filteredItems.remove(tag);
|
|
||||||
});
|
|
||||||
_closeDropdown();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
onTap: () {
|
||||||
|
_controller.text = tag.name;
|
||||||
|
_submit(tag.name);
|
||||||
|
_closeDropdown();
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -5,9 +5,18 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
|||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class ProductsGrid extends StatelessWidget {
|
class ProductsGrid extends StatelessWidget {
|
||||||
const ProductsGrid({required this.products, super.key});
|
const ProductsGrid({
|
||||||
|
required this.products,
|
||||||
|
required this.selectedProducts,
|
||||||
|
required this.onIncrement,
|
||||||
|
required this.onDecrement,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final List<Product> products;
|
final List<Product> products;
|
||||||
|
final Map<Product, int> selectedProducts;
|
||||||
|
final void Function(Product) onIncrement;
|
||||||
|
final void Function(Product) onDecrement;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -25,26 +34,27 @@ class ProductsGrid extends StatelessWidget {
|
|||||||
color: ColorsManager.textFieldGreyColor,
|
color: ColorsManager.textFieldGreyColor,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Expanded(
|
child: GridView.builder(
|
||||||
child: GridView.builder(
|
padding: const EdgeInsets.symmetric(
|
||||||
padding: const EdgeInsets.symmetric(
|
horizontal: 20,
|
||||||
horizontal: 20,
|
|
||||||
),
|
|
||||||
shrinkWrap: true,
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
mainAxisSpacing: 6,
|
|
||||||
crossAxisSpacing: 4,
|
|
||||||
childAspectRatio: 0.8,
|
|
||||||
),
|
|
||||||
itemCount: products.length,
|
|
||||||
itemBuilder: (context, index) => ProductTypeCard(
|
|
||||||
product: products[index],
|
|
||||||
count: 0,
|
|
||||||
onIncrement: () {},
|
|
||||||
onDecrement: () {},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
shrinkWrap: true,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
mainAxisSpacing: 6,
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
childAspectRatio: 0.8,
|
||||||
|
),
|
||||||
|
itemCount: products.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = products[index];
|
||||||
|
return ProductTypeCard(
|
||||||
|
product: product,
|
||||||
|
count: selectedProducts[product] ?? 0,
|
||||||
|
onIncrement: () => onIncrement(product),
|
||||||
|
onDecrement: () => onDecrement(product),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user