Compare commits

...

5 Commits

Author SHA1 Message Date
bb846f797f Implement Tag Assignment and Device Addition Features:
- Introduced AssignTagsDialog for assigning tags to devices, enhancing user interaction and organization.
- Added AddDeviceTypeWidget for adding new device types, improving the flexibility of device management.
- Created ProductTypeCard and ProductTypeCardCounter for better representation and interaction with device types.
- Enhanced AssignTagsTable for displaying and managing product allocations, improving maintainability and user experience.
2025-07-06 16:54:15 +03:00
e234c9f3b2 Enhance SpaceDetailsActionButtons: Introduced customizable button labels for save and cancel actions, improving flexibility and user experience. Updated button implementations to utilize these new labels, enhancing maintainability and adherence to Clean Architecture principles. 2025-07-06 16:44:40 +03:00
bcd0ae4a2a Refactor Products Module:
- Introduced ProductsBloc and updated ProductsService to remove LoadProductsParam, simplifying the product loading process.
- Updated RemoteProductsService to utilize a new API endpoint for fetching products.
- Adjusted ProductsEvent and ProductsState to reflect changes in the loading mechanism, enhancing maintainability and clarity in the products management flow.
2025-07-06 16:44:26 +03:00
cebce2ce7f Update SpaceDetailsModel: Change default icon from villa to location for improved representation of space details. 2025-07-06 14:50:24 +03:00
97e3fb68bf Enhance Product Model and SpaceDetailsDevicesBox:
- Added 'productType' field to Product model for improved data representation.
- Updated JSON parsing in Product model to handle 'prodType'.
- Refactored SpaceDetailsDevicesBox to utilize productType for dynamic device icon rendering, enhancing UI clarity and maintainability.
2025-07-06 14:49:10 +03:00
17 changed files with 733 additions and 81 deletions

View File

@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_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/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -33,6 +35,11 @@ class SpaceManagementPage extends StatelessWidget {
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

@ -1,9 +1,9 @@
import 'package:dio/dio.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/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteProductsService implements ProductsService {
const RemoteProductsService(this._httpService);
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
static const _defaultErrorMessage = 'Failed to load devices';
@override
Future<List<Product>> getProducts(LoadProductsParam param) async {
Future<List<Product>> getProducts() async {
try {
final response = await _httpService.get(
path: 'devices',
queryParameters: {
'spaceUuid': param.spaceUuid,
if (param.type != null) 'type': param.type,
if (param.status != null) 'status': param.status,
},
path: ApiEndpoints.listProducts,
expectedResponseModel: (data) {
return (data as List)
final json = data as Map<String, dynamic>;
final products = json['data'] as List<dynamic>;
return products
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
},

View File

@ -3,16 +3,18 @@ import 'package:equatable/equatable.dart';
class Product extends Equatable {
final String uuid;
final String name;
final String productType;
const Product({
required this.uuid,
required this.name,
required this.productType,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
uuid: json['uuid'] as String,
name: json['name'] as String,
uuid: json['uuid'] as String? ?? '',
name: json['name'] as String? ?? '',
productType: json['prodType'] as String? ?? '',
);
}
@ -20,9 +22,10 @@ class Product extends Equatable {
return {
'uuid': uuid,
'name': name,
'productType': productType,
};
}
@override
List<Object?> get props => [uuid, name];
List<Object?> get props => [uuid, name, productType];
}

View File

@ -1,11 +0,0 @@
class LoadProductsParam {
final String spaceUuid;
final String? type;
final String? status;
const LoadProductsParam({
required this.spaceUuid,
this.type,
this.status,
});
}

View File

@ -1,6 +1,5 @@
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/params/load_products_param.dart';
abstract class ProductsService {
Future<List<Product>> getProducts(LoadProductsParam param);
Future<List<Product>> getProducts();
}

View File

@ -1,7 +1,6 @@
import 'package:bloc/bloc.dart';
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/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -9,20 +8,20 @@ part 'products_event.dart';
part 'products_state.dart';
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
final ProductsService _deviceService;
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
ProductsBloc(this._productsService) : super(ProductsInitial()) {
on<LoadProducts>(_onLoadProducts);
}
final ProductsService _productsService;
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductsState> emit,
) async {
emit(ProductsLoading());
try {
final devices = await _deviceService.getProducts(event.param);
emit(ProductsLoaded(devices));
final products = await _productsService.getProducts();
emit(ProductsLoaded(products));
} on APIException catch (e) {
emit(ProductsFailure(e.message));
} catch (e) {

View File

@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
}
final class LoadProducts extends ProductsEvent {
const LoadProducts(this.param);
final LoadProductsParam param;
@override
List<Object> get props => [param];
const LoadProducts();
}

View File

@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
}
final class ProductsFailure extends ProductsState {
final String message;
final String errorMessage;
const ProductsFailure(this.message);
const ProductsFailure(this.errorMessage);
@override
List<Object> get props => [message];
List<Object> get props => [errorMessage];
}

View File

@ -21,7 +21,7 @@ class SpaceDetailsModel extends Equatable {
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.villa,
icon: Assets.location,
productAllocations: [],
subspaces: [],
);

View File

@ -8,10 +8,14 @@ class SpaceDetailsActionButtons extends StatelessWidget {
super.key,
required this.onSave,
required this.onCancel,
this.saveButtonLabel = 'OK',
this.cancelButtonLabel = 'Cancel',
});
final VoidCallback onCancel;
final VoidCallback? onSave;
final String saveButtonLabel;
final String cancelButtonLabel;
@override
Widget build(BuildContext context) {
@ -27,10 +31,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
}
Widget _buildCancelButton(BuildContext context) {
return CancelButton(
onPressed: onCancel,
label: 'Cancel',
);
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
}
Widget _buildSaveButton() {
@ -39,7 +40,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
child: Text(saveButtonLabel),
);
}
}

View File

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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/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/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/enum/device_types.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDevicesBox extends StatelessWidget {
const SpaceDetailsDevicesBox({
@ -35,46 +39,39 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
// ...TagHelper.groupTags([
// ...?tags,
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
// ]).entries.map(
// (entry) => Chip(
// avatar: SizedBox(
// width: 24,
// height: 24,
// child: SvgPicture.asset(
// entry.key.icon ?? 'assets/icons/gateway.svg',
// fit: BoxFit.contain,
// ),
// ),
// label: Text(
// 'x${entry.value}',
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.copyWith(color: ColorsManager.spaceColor),
// ),
// backgroundColor: ColorsManager.whiteColors,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// side: const BorderSide(
// color: ColorsManager.spaceColor,
// ),
// ),
// ),
// ),
...productAllocations.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
_getDeviceIcon(entry.product.productType),
fit: BoxFit.contain,
),
),
label: Text(
entry.product.productType,
style: context.textTheme.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
EditChip(
onTap: () {},
onTap: () => _showAssignTagsDialog(context),
),
],
),
);
} else {
return TextButton(
onPressed: () {},
onPressed: () => _showAssignTagsDialog(context),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
@ -83,10 +80,44 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Add Devices',
// disabled: isTagsAndSubspaceModelDisabled,
),
),
);
}
}
void _showAssignTagsDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AssignTagsDialog(space: space),
);
}
String _getDeviceIcon(String productType) =>
switch (devicesTypesMap[productType]) {
DeviceType.LightBulb => Assets.lightBulb,
DeviceType.CeilingSensor => Assets.sensors,
DeviceType.AC => Assets.ac,
DeviceType.DoorLock => Assets.doorLock,
DeviceType.Curtain => Assets.curtain,
DeviceType.ThreeGang => Assets.gangSwitch,
DeviceType.Gateway => Assets.gateway,
DeviceType.OneGang => Assets.oneGang,
DeviceType.TwoGang => Assets.twoGang,
DeviceType.WH => Assets.waterHeater,
DeviceType.DoorSensor => Assets.openCloseDoor,
DeviceType.GarageDoor => Assets.openedDoor,
DeviceType.WaterLeak => Assets.waterLeakNormal,
DeviceType.Curtain2 => Assets.curtainIcon,
DeviceType.Blind => Assets.curtainIcon,
DeviceType.WallSensor => Assets.sensors,
DeviceType.DS => Assets.openCloseDoor,
DeviceType.OneTouch => Assets.gangSwitch,
DeviceType.TowTouch => Assets.gangSwitch,
DeviceType.ThreeTouch => Assets.gangSwitch,
DeviceType.NCPS => Assets.sensors,
DeviceType.PC => Assets.powerClamp,
DeviceType.Other => Assets.blackLogo,
null => Assets.blackLogo,
};
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.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/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatelessWidget {
const AddDeviceTypeWidget({super.key});
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final crossAxisCount = switch (context.screenWidth) {
> 1200 => 8,
> 800 => 5,
_ => 3,
};
return BlocProvider(
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
..add(const LoadProducts()),
child: Builder(
builder: (context) => AlertDialog(
title: const Text('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) {
return switch (state) {
ProductsInitial() => const Center(
child: CircularProgressIndicator(),
),
ProductsLoading() => const Center(
child: CircularProgressIndicator(),
),
ProductsLoaded(:final products) => SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const SizedBox(height: 16),
Expanded(
child: GridView.builder(
padding: const EdgeInsets.symmetric(
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],
),
),
),
],
),
),
),
ProductsFailure(:final errorMessage) => Center(
child: Text(
errorMessage,
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
};
},
),
),
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.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/tags/presentation/widgets/add_device_type_widget.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';
class AssignTagsDialog extends StatelessWidget {
const AssignTagsDialog({required this.space, super.key});
final SpaceDetailsModel space;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Assign Tags'),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: context.screenWidth * 0.6,
minWidth: context.screenWidth * 0.6,
maxHeight: context.screenHeight * 0.8,
),
child: AssignTagsTable(productAllocations: space.productAllocations),
),
actions: [
SpaceDetailsActionButtons(
onSave: () {},
onCancel: () {
showDialog<void>(
context: context,
builder: (context) => const AddDeviceTypeWidget(),
);
},
cancelButtonLabel: 'Add New Device',
),
],
);
}
}

View File

@ -0,0 +1,170 @@
import 'package:flutter/material.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/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class AssignTagsTable extends StatefulWidget {
const AssignTagsTable({
required this.productAllocations,
super.key,
});
final List<ProductAllocation> productAllocations;
@override
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(label: Text(label, style: context.textTheme.bodyMedium));
}
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(widget.productAllocations.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
_buildDataColumn('#'),
_buildDataColumn('Device'),
_buildDataColumn('Tag'),
_buildDataColumn('Location'),
],
rows: widget.productAllocations.isEmpty
? [
DataRow(
cells: [
DataCell(
Center(
child: Text(
'No Devices Available',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
),
),
DataCell.empty,
DataCell.empty,
DataCell.empty,
],
),
]
: List.generate(widget.productAllocations.length, (index) {
final productAllocation = widget.productAllocations[index];
final controller = _controllers[index];
return DataRow(
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
productAllocation.product.name,
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
// TODO: Delete the product allocation
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment.centerLeft,
width: double.infinity,
child: ProductTagField(
key: ValueKey('dropdown_${const Uuid().v4()}_$index'),
productName: productAllocation.product.uuid,
initialValue: null,
onSelected: (value) {
controller.text = value.name;
},
items: const [
Tag(
uuid: '',
name: 'Tag',
createdAt: '',
updatedAt: '',
),
],
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: const [],
// items: widget.locations,
selectedValue: productAllocation.tag.name.isEmpty
? 'Main Space'
: productAllocation.tag.name,
onSelected: (value) {},
)),
),
],
);
}),
),
);
}
}

View File

@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTagField extends StatefulWidget {
final List<Tag> items;
final ValueChanged<Tag> onSelected;
final Tag? initialValue;
final String productName;
const ProductTagField({
super.key,
required this.items,
required this.onSelected,
this.initialValue,
required this.productName,
});
@override
State<ProductTagField> createState() => _ProductTagFieldState();
}
class _ProductTagFieldState extends State<ProductTagField> {
bool _isOpen = false;
OverlayEntry? _overlayEntry;
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
List<Tag> _filteredItems = [];
@override
void initState() {
super.initState();
_controller.text = widget.initialValue?.name ?? '';
_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
void dispose() {
_controller.dispose();
_focusNode.dispose();
_overlayEntry = null;
_isOpen = false;
super.dispose();
}
void _filterItems() => setState(() => _filteredItems = widget.items);
void _toggleDropdown() {
if (_isOpen) {
_closeDropdown();
} else {
_openDropdown();
}
}
void _openDropdown() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
_isOpen = true;
}
void _closeDropdown() {
if (_isOpen && _overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
_isOpen = false;
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.transparentColor),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextFormField(
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: (value) {
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,
decoration: const InputDecoration(
hintText: 'Enter or Select a tag',
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: _toggleDropdown,
child: const Icon(Icons.arrow_drop_down),
),
],
),
),
);
}
OverlayEntry _createOverlayEntry() {
final renderBox = context.findRenderObject()! as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return GestureDetector(
onTap: _closeDropdown,
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy + size.height,
width: size.width,
child: Material(
elevation: 4.0,
child: Container(
color: ColorsManager.whiteColors,
constraints: const BoxConstraints(maxHeight: 200.0),
child: StatefulBuilder(
builder: (context, setStateDropdown) {
return ListView.builder(
shrinkWrap: true,
itemCount: _filteredItems.length,
itemBuilder: (context, index) {
final tag = _filteredItems[index];
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 1.0,
),
),
),
child: ListTile(
title: Text(
tag.name,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textPrimaryColor,
),
),
onTap: () {
_controller.text = tag.name;
widget.onSelected(tag);
setState(() {
_filteredItems.remove(tag);
});
_closeDropdown();
},
),
);
},
);
},
),
),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTypeCard extends StatelessWidget {
const ProductTypeCard({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
color: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: DeviceIconWidget(
icon: product.name,
),
),
_buildName(context, product.name),
CounterWidget(
isCreate: false,
initialCount: 0,
onCountChanged: (newCount) {},
),
const SizedBox(height: 4),
],
),
),
);
}
Widget _buildName(BuildContext context, String name) {
return Expanded(
child: SizedBox(
height: 35,
child: Text(
name,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ProductTypeCardCounter extends StatefulWidget {
const ProductTypeCardCounter({
super.key,
required this.onIncrement,
required this.onDecrement,
required this.count,
});
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
@override
State<ProductTypeCardCounter> createState() => _ProductTypeCardCounterState();
}
class _ProductTypeCardCounterState extends State<ProductTypeCardCounter> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
_buildCounterButton(
Icons.remove,
widget.onDecrement,
),
Text(
widget.count.toString(),
style: theme.textTheme.bodyLarge?.copyWith(
color: ColorsManager.spaceColor,
),
),
_buildCounterButton(Icons.add, widget.onIncrement),
],
),
);
}
Widget _buildCounterButton(
IconData icon,
VoidCallback onPressed,
) {
return GestureDetector(
onTap: onPressed,
child: Icon(
icon,
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
size: 18,
),
);
}
}