mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-15 17:47:53 +00:00
Compare commits
36 Commits
Add-Bookin
...
bb846f797f
Author | SHA1 | Date | |
---|---|---|---|
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf | |||
a4024067c7 | |||
95cded4bf5 | |||
757a96ed9f | |||
b857736e10 | |||
1fccd51440 | |||
c07ddb0ccd | |||
58e99f95b2 | |||
227df6fe3d | |||
9451ec0cc4 | |||
fc797c2646 | |||
318e1d9af7 | |||
d47dc349bc | |||
c221c8499f | |||
71cf4b9feb | |||
c43cf9347f | |||
9990b1805e | |||
50f8158830 | |||
009b7c0316 | |||
779c0fe916 | |||
e448eabda6 | |||
9dfb3ed369 | |||
63353af38b | |||
68b6c9b18c | |||
fa6ee9a0af | |||
3601b02bc3 | |||
fdd0526c78 | |||
bdeec7d325 | |||
50ff17a0c1 | |||
87c2e3261d | |||
62a6f9c993 | |||
f7e4d6ff07 |
@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
|||||||
color: const Color(0xFF0026A2),
|
color: const Color(0xFF0026A2),
|
||||||
),
|
),
|
||||||
HomeItemModel(
|
HomeItemModel(
|
||||||
title: 'Devices Management',
|
title: 'Device Management',
|
||||||
icon: Assets.devicesIcon,
|
icon: Assets.devicesIcon,
|
||||||
active: true,
|
active: true,
|
||||||
onPress: (context) {
|
onPress: (context) {
|
||||||
|
@ -7,6 +7,10 @@ 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/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/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/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';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
@ -26,6 +30,16 @@ class SpaceManagementPage extends StatelessWidget {
|
|||||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ProductsBloc(
|
||||||
|
RemoteProductsService(HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: WebScaffold(
|
child: WebScaffold(
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
|
@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.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/create_community/presentation/create_community_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeader extends StatelessWidget {
|
||||||
|
const CommunityStructureHeader({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.shadowBlackColor,
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateCommunityDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CreateCommunityDialog(
|
||||||
|
title: const Text('Edit Community'),
|
||||||
|
onCreateCommunity: (community) {
|
||||||
|
// TODO(FarisArmoush): Implement
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommunityInfo(
|
||||||
|
BuildContext context, ThemeData theme, double screenWidth) {
|
||||||
|
final selectedCommunity =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||||
|
final selectedSpace =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Community Structure',
|
||||||
|
style: theme.textTheme.headlineLarge
|
||||||
|
?.copyWith(color: ColorsManager.blackColor),
|
||||||
|
),
|
||||||
|
if (selectedCommunity != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: SelectableText(
|
||||||
|
selectedCommunity.name,
|
||||||
|
style: theme.textTheme.bodyLarge
|
||||||
|
?.copyWith(color: ColorsManager.blackColor),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showCreateCommunityDialog(context),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.iconEdit,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CommunityStructureHeaderActionButtons(
|
||||||
|
onDelete: (space) {},
|
||||||
|
onDuplicate: (space) {},
|
||||||
|
onEdit: (space) {
|
||||||
|
SpaceDetailsDialogHelper.showEdit(
|
||||||
|
context,
|
||||||
|
spaceModel: selectedSpace!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectedSpace: selectedSpace,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderActionButtons({
|
||||||
|
super.key,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.selectedSpace,
|
||||||
|
required this.onDuplicate,
|
||||||
|
required this.onEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(SpaceModel space) onDelete;
|
||||||
|
final void Function(SpaceModel space) onDuplicate;
|
||||||
|
final void Function(SpaceModel space) onEdit;
|
||||||
|
final SpaceModel? selectedSpace;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.end,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
if (selectedSpace != null) ...[
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Edit',
|
||||||
|
svgAsset: Assets.editSpace,
|
||||||
|
onPressed: () => onEdit(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Duplicate',
|
||||||
|
svgAsset: Assets.duplicate,
|
||||||
|
onPressed: () => onDuplicate(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Delete',
|
||||||
|
svgAsset: Assets.spaceDelete,
|
||||||
|
onPressed: () => onDelete(selectedSpace!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderButton extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.svgAsset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final String? svgAsset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double buttonHeight = 40;
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 130,
|
||||||
|
minHeight: buttonHeight,
|
||||||
|
),
|
||||||
|
child: DefaultButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
borderWidth: 2,
|
||||||
|
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||||
|
foregroundColor: ColorsManager.blackColor,
|
||||||
|
borderRadius: 12.0,
|
||||||
|
padding: 2.0,
|
||||||
|
height: buttonHeight,
|
||||||
|
elevation: 0,
|
||||||
|
borderColor: ColorsManager.lightGrayColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (svgAsset != null)
|
||||||
|
SvgPicture.asset(
|
||||||
|
svgAsset!,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: context.textTheme.bodySmall
|
||||||
|
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/main_module/widgets/community_structure_canvas.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.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/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
|
||||||
@ -18,10 +19,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
|||||||
replacement: const Row(
|
replacement: const Row(
|
||||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||||
),
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const CommunityStructureHeader(),
|
||||||
|
Expanded(
|
||||||
child: CommunityStructureCanvas(
|
child: CommunityStructureCanvas(
|
||||||
community: selectedCommunity,
|
community: selectedCommunity,
|
||||||
selectedSpace: selectedSpace,
|
selectedSpace: selectedSpace,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,16 @@ class SpaceModel extends Equatable {
|
|||||||
required this.parent,
|
required this.parent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory SpaceModel.empty() => const SpaceModel(
|
||||||
|
uuid: '',
|
||||||
|
createdAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
spaceName: '',
|
||||||
|
icon: '',
|
||||||
|
children: [],
|
||||||
|
parent: null,
|
||||||
|
);
|
||||||
|
|
||||||
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
||||||
return SpaceModel(
|
return SpaceModel(
|
||||||
uuid: json['uuid'] as String? ?? '',
|
uuid: json['uuid'] as String? ?? '',
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:dio/dio.dart';
|
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/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/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/api_exception.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||||
|
|
||||||
class RemoteProductsService implements ProductsService {
|
class RemoteProductsService implements ProductsService {
|
||||||
const RemoteProductsService(this._httpService);
|
const RemoteProductsService(this._httpService);
|
||||||
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
|
|||||||
static const _defaultErrorMessage = 'Failed to load devices';
|
static const _defaultErrorMessage = 'Failed to load devices';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Product>> getProducts(LoadProductsParam param) async {
|
Future<List<Product>> getProducts() async {
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.get(
|
final response = await _httpService.get(
|
||||||
path: 'devices',
|
path: ApiEndpoints.listProducts,
|
||||||
queryParameters: {
|
|
||||||
'spaceUuid': param.spaceUuid,
|
|
||||||
if (param.type != null) 'type': param.type,
|
|
||||||
if (param.status != null) 'status': param.status,
|
|
||||||
},
|
|
||||||
expectedResponseModel: (data) {
|
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>))
|
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
},
|
},
|
||||||
|
@ -3,16 +3,18 @@ import 'package:equatable/equatable.dart';
|
|||||||
class Product extends Equatable {
|
class Product extends Equatable {
|
||||||
final String uuid;
|
final String uuid;
|
||||||
final String name;
|
final String name;
|
||||||
|
final String productType;
|
||||||
const Product({
|
const Product({
|
||||||
required this.uuid,
|
required this.uuid,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.productType,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Product.fromJson(Map<String, dynamic> json) {
|
factory Product.fromJson(Map<String, dynamic> json) {
|
||||||
return Product(
|
return Product(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String? ?? '',
|
||||||
name: json['name'] as String,
|
name: json['name'] as String? ?? '',
|
||||||
|
productType: json['prodType'] as String? ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,9 +22,10 @@ class Product extends Equatable {
|
|||||||
return {
|
return {
|
||||||
'uuid': uuid,
|
'uuid': uuid,
|
||||||
'name': name,
|
'name': name,
|
||||||
|
'productType': productType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, name];
|
List<Object?> get props => [uuid, name, productType];
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
class LoadProductsParam {
|
|
||||||
final String spaceUuid;
|
|
||||||
final String? type;
|
|
||||||
final String? status;
|
|
||||||
|
|
||||||
const LoadProductsParam({
|
|
||||||
required this.spaceUuid,
|
|
||||||
this.type,
|
|
||||||
this.status,
|
|
||||||
});
|
|
||||||
}
|
|
@ -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/models/product.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
|
||||||
|
|
||||||
abstract class ProductsService {
|
abstract class ProductsService {
|
||||||
Future<List<Product>> getProducts(LoadProductsParam param);
|
Future<List<Product>> getProducts();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.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/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/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/api_exception.dart';
|
||||||
|
|
||||||
@ -9,20 +8,20 @@ part 'products_event.dart';
|
|||||||
part 'products_state.dart';
|
part 'products_state.dart';
|
||||||
|
|
||||||
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
||||||
final ProductsService _deviceService;
|
ProductsBloc(this._productsService) : super(ProductsInitial()) {
|
||||||
|
|
||||||
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
|
|
||||||
on<LoadProducts>(_onLoadProducts);
|
on<LoadProducts>(_onLoadProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ProductsService _productsService;
|
||||||
|
|
||||||
Future<void> _onLoadProducts(
|
Future<void> _onLoadProducts(
|
||||||
LoadProducts event,
|
LoadProducts event,
|
||||||
Emitter<ProductsState> emit,
|
Emitter<ProductsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(ProductsLoading());
|
emit(ProductsLoading());
|
||||||
try {
|
try {
|
||||||
final devices = await _deviceService.getProducts(event.param);
|
final products = await _productsService.getProducts();
|
||||||
emit(ProductsLoaded(devices));
|
emit(ProductsLoaded(products));
|
||||||
} on APIException catch (e) {
|
} on APIException catch (e) {
|
||||||
emit(ProductsFailure(e.message));
|
emit(ProductsFailure(e.message));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class LoadProducts extends ProductsEvent {
|
final class LoadProducts extends ProductsEvent {
|
||||||
const LoadProducts(this.param);
|
const LoadProducts();
|
||||||
|
|
||||||
final LoadProductsParam param;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object> get props => [param];
|
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class ProductsFailure extends ProductsState {
|
final class ProductsFailure extends ProductsState {
|
||||||
final String message;
|
final String errorMessage;
|
||||||
|
|
||||||
const ProductsFailure(this.message);
|
const ProductsFailure(this.errorMessage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [message];
|
List<Object> get props => [errorMessage];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/bloc/project_manager.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/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
@ -15,12 +16,15 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
|||||||
static const _defaultErrorMessage = 'Failed to load space details';
|
static const _defaultErrorMessage = 'Failed to load space details';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param) async {
|
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
|
||||||
try {
|
try {
|
||||||
final response = await _httpService.get(
|
final response = await _httpService.get(
|
||||||
path: 'endpoint',
|
path: await _makeEndpoint(param),
|
||||||
expectedResponseModel: (data) {
|
expectedResponseModel: (data) {
|
||||||
return SpaceDetailsModel.fromJson(data as Map<String, dynamic>);
|
final response = data as Map<String, dynamic>;
|
||||||
|
return SpaceDetailsModel.fromJson(
|
||||||
|
response['data'] as Map<String, dynamic>,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
|||||||
throw APIException(formattedErrorMessage);
|
throw APIException(formattedErrorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _makeEndpoint(LoadSpaceDetailsParam param) async {
|
||||||
|
final projectUuid = await ProjectManager.getProjectUUID();
|
||||||
|
if (projectUuid == null || projectUuid.isEmpty) {
|
||||||
|
throw APIException('Project UUID is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:equatable/equatable.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/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';
|
||||||
|
|
||||||
class SpaceDetailsModel extends Equatable {
|
class SpaceDetailsModel extends Equatable {
|
||||||
final String uuid;
|
final String uuid;
|
||||||
@ -17,6 +18,13 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
required this.subspaces,
|
required this.subspaces,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||||
|
uuid: '',
|
||||||
|
spaceName: '',
|
||||||
|
icon: Assets.location,
|
||||||
|
productAllocations: [],
|
||||||
|
subspaces: [],
|
||||||
|
);
|
||||||
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
|
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
|
||||||
return SpaceDetailsModel(
|
return SpaceDetailsModel(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
@ -41,6 +49,22 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SpaceDetailsModel copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? spaceName,
|
||||||
|
String? icon,
|
||||||
|
List<ProductAllocation>? productAllocations,
|
||||||
|
List<Subspace>? subspaces,
|
||||||
|
}) {
|
||||||
|
return SpaceDetailsModel(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
spaceName: spaceName ?? this.spaceName,
|
||||||
|
icon: icon ?? this.icon,
|
||||||
|
productAllocations: productAllocations ?? this.productAllocations,
|
||||||
|
subspaces: subspaces ?? this.subspaces,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
|
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
|
||||||
}
|
}
|
||||||
@ -48,12 +72,10 @@ class SpaceDetailsModel extends Equatable {
|
|||||||
class ProductAllocation extends Equatable {
|
class ProductAllocation extends Equatable {
|
||||||
final Product product;
|
final Product product;
|
||||||
final Tag tag;
|
final Tag tag;
|
||||||
final String? location;
|
|
||||||
|
|
||||||
const ProductAllocation({
|
const ProductAllocation({
|
||||||
required this.product,
|
required this.product,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
this.location,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
||||||
@ -70,6 +92,16 @@ class ProductAllocation extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProductAllocation copyWith({
|
||||||
|
Product? product,
|
||||||
|
Tag? tag,
|
||||||
|
}) {
|
||||||
|
return ProductAllocation(
|
||||||
|
product: product ?? this.product,
|
||||||
|
tag: tag ?? this.tag,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [product, tag];
|
List<Object?> get props => [product, tag];
|
||||||
}
|
}
|
||||||
@ -88,7 +120,7 @@ class Subspace extends Equatable {
|
|||||||
factory Subspace.fromJson(Map<String, dynamic> json) {
|
factory Subspace.fromJson(Map<String, dynamic> json) {
|
||||||
return Subspace(
|
return Subspace(
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
name: json['name'] as String,
|
name: json['subspaceName'] 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(),
|
||||||
@ -103,6 +135,18 @@ class Subspace extends Equatable {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Subspace copyWith({
|
||||||
|
String? uuid,
|
||||||
|
String? name,
|
||||||
|
List<ProductAllocation>? productAllocations,
|
||||||
|
}) {
|
||||||
|
return Subspace(
|
||||||
|
uuid: uuid ?? this.uuid,
|
||||||
|
name: name ?? this.name,
|
||||||
|
productAllocations: productAllocations ?? this.productAllocations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [uuid, name, productAllocations];
|
List<Object?> get props => [uuid, name, productAllocations];
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
class LoadSpaceDetailsParam {
|
||||||
|
const LoadSpaceDetailsParam({
|
||||||
|
required this.spaceUuid,
|
||||||
|
required this.communityUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String spaceUuid;
|
||||||
|
final String communityUuid;
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
class LoadSpacesParam {
|
|
||||||
const LoadSpacesParam();
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
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/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
|
|
||||||
abstract class SpaceDetailsService {
|
abstract class SpaceDetailsService {
|
||||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param);
|
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.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/domain/params/load_spaces_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||||
|
|
||||||
@ -9,12 +9,13 @@ part 'space_details_event.dart';
|
|||||||
part 'space_details_state.dart';
|
part 'space_details_state.dart';
|
||||||
|
|
||||||
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
||||||
final SpaceDetailsService _spaceDetailsService;
|
|
||||||
|
|
||||||
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
|
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
|
||||||
on<LoadSpaceDetails>(_onLoadSpaceDetails);
|
on<LoadSpaceDetails>(_onLoadSpaceDetails);
|
||||||
|
on<ClearSpaceDetails>(_onClearSpaceDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final SpaceDetailsService _spaceDetailsService;
|
||||||
|
|
||||||
Future<void> _onLoadSpaceDetails(
|
Future<void> _onLoadSpaceDetails(
|
||||||
LoadSpaceDetails event,
|
LoadSpaceDetails event,
|
||||||
Emitter<SpaceDetailsState> emit,
|
Emitter<SpaceDetailsState> emit,
|
||||||
@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
|||||||
emit(SpaceDetailsFailure(e.toString()));
|
emit(SpaceDetailsFailure(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onClearSpaceDetails(
|
||||||
|
ClearSpaceDetails event,
|
||||||
|
Emitter<SpaceDetailsState> emit,
|
||||||
|
) {
|
||||||
|
emit(SpaceDetailsInitial());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable {
|
|||||||
List<Object> get props => [];
|
List<Object> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoadSpaceDetails extends SpaceDetailsEvent {
|
final class LoadSpaceDetails extends SpaceDetailsEvent {
|
||||||
const LoadSpaceDetails(this.param);
|
const LoadSpaceDetails(this.param);
|
||||||
|
|
||||||
final LoadSpacesParam param;
|
final LoadSpaceDetailsParam param;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [param];
|
List<Object> get props => [param];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ClearSpaceDetails extends SpaceDetailsEvent {
|
||||||
|
const ClearSpaceDetails();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class SpaceDetailsFailure extends SpaceDetailsState {
|
final class SpaceDetailsFailure extends SpaceDetailsState {
|
||||||
final String message;
|
final String errorMessage;
|
||||||
|
|
||||||
const SpaceDetailsFailure(this.message);
|
const SpaceDetailsFailure(this.errorMessage);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [message];
|
List<Object> get props => [errorMessage];
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,46 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.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/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||||
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
|
|
||||||
abstract final class SpaceDetailsDialogHelper {
|
abstract final class SpaceDetailsDialogHelper {
|
||||||
static void showCreate(BuildContext context) {
|
static void showCreate(BuildContext context) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => const SpaceDetailsDialog(),
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
child: SpaceDetailsDialog(
|
||||||
|
context: context,
|
||||||
|
title: const Text('Create Space'),
|
||||||
|
spaceModel: SpaceModel.empty(),
|
||||||
|
onSave: print,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void showEdit(
|
||||||
|
BuildContext context, {
|
||||||
|
required SpaceModel spaceModel,
|
||||||
|
}) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
child: SpaceDetailsDialog(
|
||||||
|
context: context,
|
||||||
|
title: const Text('Edit Space'),
|
||||||
|
spaceModel: spaceModel,
|
||||||
|
onSave: (space) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class ButtonContentWidget extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final String? svgAssets;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
|
const ButtonContentWidget({
|
||||||
|
required this.label,
|
||||||
|
this.svgAssets,
|
||||||
|
this.disabled = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: disabled ? 0.5 : 1.0,
|
||||||
|
child: Container(
|
||||||
|
width: screenWidth * 0.25,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.neutralGray,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (svgAssets != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 6.0),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
svgAssets!,
|
||||||
|
width: screenWidth * 0.015,
|
||||||
|
height: screenWidth * 0.015,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsActionButtons extends StatelessWidget {
|
||||||
|
const SpaceDetailsActionButtons({
|
||||||
|
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) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _buildCancelButton(context)),
|
||||||
|
Expanded(child: _buildSaveButton()),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCancelButton(BuildContext context) {
|
||||||
|
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton() {
|
||||||
|
return DefaultButton(
|
||||||
|
onPressed: onSave,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: ColorsManager.secondaryColor,
|
||||||
|
foregroundColor: ColorsManager.whiteColors,
|
||||||
|
child: Text(saveButtonLabel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,123 @@
|
|||||||
|
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({
|
||||||
|
required this.space,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final productAllocations = space.productAllocations;
|
||||||
|
final subspaces = space.subspaces;
|
||||||
|
final isAnySubspaceHasProductAllocations =
|
||||||
|
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
|
||||||
|
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
...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: () => _showAssignTagsDialog(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return TextButton(
|
||||||
|
onPressed: () => _showAssignTagsDialog(context),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ButtonContentWidget(
|
||||||
|
svgAssets: Assets.addIcon,
|
||||||
|
label: 'Add Devices',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
@ -1,12 +1,101 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.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/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class SpaceDetailsDialog extends StatelessWidget {
|
class SpaceDetailsDialog extends StatefulWidget {
|
||||||
const SpaceDetailsDialog({super.key});
|
const SpaceDetailsDialog({
|
||||||
|
required this.title,
|
||||||
|
required this.spaceModel,
|
||||||
|
required this.onSave,
|
||||||
|
required this.context,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final SpaceModel spaceModel;
|
||||||
|
final void Function(SpaceDetailsModel space) onSave;
|
||||||
|
final BuildContext context;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||||
|
|
||||||
|
if (!isCreateMode) {
|
||||||
|
final param = LoadSpaceDetailsParam(
|
||||||
|
spaceUuid: widget.spaceModel.uuid,
|
||||||
|
communityUuid: widget.context
|
||||||
|
.read<CommunitiesTreeSelectionBloc>()
|
||||||
|
.state
|
||||||
|
.selectedCommunity!
|
||||||
|
.uuid,
|
||||||
|
);
|
||||||
|
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
|
||||||
|
}
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const Dialog(
|
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||||
child: Text('Create Space'),
|
if (isCreateMode) {
|
||||||
|
return SpaceDetailsForm(
|
||||||
|
title: widget.title,
|
||||||
|
space: SpaceDetailsModel.empty(),
|
||||||
|
onSave: widget.onSave,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlocBuilder<SpaceDetailsBloc, SpaceDetailsState>(
|
||||||
|
bloc: widget.context.read<SpaceDetailsBloc>(),
|
||||||
|
builder: (context, state) => switch (state) {
|
||||||
|
SpaceDetailsInitial() => _buildLoadingDialog(),
|
||||||
|
SpaceDetailsLoading() => _buildLoadingDialog(),
|
||||||
|
SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm(
|
||||||
|
title: widget.title,
|
||||||
|
space: spaceDetails,
|
||||||
|
onSave: widget.onSave,
|
||||||
|
),
|
||||||
|
SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog(
|
||||||
|
errorMessage,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingDialog() {
|
||||||
|
return AlertDialog(
|
||||||
|
title: widget.title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: const Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorDialog(String errorMessage) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: widget.title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: Center(
|
||||||
|
child: SelectableText(
|
||||||
|
errorMessage,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: ColorsManager.red,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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_devices_box.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.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/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsForm extends StatelessWidget {
|
||||||
|
const SpaceDetailsForm({
|
||||||
|
required this.title,
|
||||||
|
required this.space,
|
||||||
|
required this.onSave,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget title;
|
||||||
|
final SpaceDetailsModel space;
|
||||||
|
final void Function(SpaceDetailsModel space) onSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsModelBloc(initialState: space),
|
||||||
|
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
|
||||||
|
buildWhen: (previous, current) => previous != current,
|
||||||
|
builder: (context, state) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: title,
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: SizedBox(
|
||||||
|
height: context.screenHeight * 0.3,
|
||||||
|
width: context.screenWidth * 0.5,
|
||||||
|
child: Row(
|
||||||
|
spacing: 20,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: SpaceIconPicker(iconPath: state.icon)),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SpaceNameTextField(
|
||||||
|
initialValue: state.spaceName,
|
||||||
|
isNameFieldExist: (value) => state.subspaces.any(
|
||||||
|
(subspace) => subspace.name == value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
SpaceSubSpacesBox(
|
||||||
|
subspaces: state.subspaces,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SpaceDetailsDevicesBox(space: state),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: () => onSave(state),
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_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/constants/assets.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceIconPicker extends StatelessWidget {
|
||||||
|
const SpaceIconPicker({
|
||||||
|
required this.iconPath,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String iconPath;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Stack(
|
||||||
|
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: context.screenWidth * 0.175,
|
||||||
|
height: context.screenHeight * 0.175,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
iconPath,
|
||||||
|
width: context.screenWidth * 0.08,
|
||||||
|
height: context.screenHeight * 0.08,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.directional(
|
||||||
|
top: 12,
|
||||||
|
start: context.screenHeight * 0.06,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => SpaceIconSelectionDialog(
|
||||||
|
selectedIcon: iconPath,
|
||||||
|
),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
if (context.mounted) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsIcon(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.iconEdit,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceIconSelectionDialog extends StatelessWidget {
|
||||||
|
const SpaceIconSelectionDialog({super.key, required this.selectedIcon});
|
||||||
|
final String selectedIcon;
|
||||||
|
|
||||||
|
static const List<String> _icons = [
|
||||||
|
Assets.location,
|
||||||
|
Assets.villa,
|
||||||
|
Assets.gym,
|
||||||
|
Assets.sauna,
|
||||||
|
Assets.bbq,
|
||||||
|
Assets.building,
|
||||||
|
Assets.desk,
|
||||||
|
Assets.door,
|
||||||
|
Assets.parking,
|
||||||
|
Assets.pool,
|
||||||
|
Assets.stair,
|
||||||
|
Assets.steamRoom,
|
||||||
|
Assets.street,
|
||||||
|
Assets.unit,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: SelectableText(
|
||||||
|
'Space Icon',
|
||||||
|
style: context.textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
content: Container(
|
||||||
|
width: context.screenWidth * 0.45,
|
||||||
|
height: context.screenHeight * 0.275,
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 7,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
),
|
||||||
|
itemCount: _icons.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final isSelected = selectedIcon == _icons[index];
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: isSelected
|
||||||
|
? Border.all(color: ColorsManager.vividBlue, width: 2)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(_icons[index]),
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
_icons[index],
|
||||||
|
width: context.screenWidth * 0.03,
|
||||||
|
height: context.screenHeight * 0.08,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SpaceNameTextField extends StatefulWidget {
|
||||||
|
const SpaceNameTextField({
|
||||||
|
required this.initialValue,
|
||||||
|
required this.isNameFieldExist,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? initialValue;
|
||||||
|
final bool Function(String value) isNameFieldExist;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = TextEditingController(text: widget.initialValue);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
String? _validateName(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return '*Space name should not be empty.';
|
||||||
|
}
|
||||||
|
if (widget.isNameFieldExist(value)) {
|
||||||
|
return '*Name already exists';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsName(value),
|
||||||
|
),
|
||||||
|
validator: _validateName,
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Please enter the name',
|
||||||
|
hintStyle: context.textTheme.bodyMedium!.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: ColorsManager.boxColor,
|
||||||
|
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
|
||||||
|
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
|
||||||
|
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||||
|
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||||
|
errorStyle: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) {
|
||||||
|
return OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.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/constants/assets.dart';
|
||||||
|
|
||||||
|
class SpaceSubSpacesBox extends StatelessWidget {
|
||||||
|
const SpaceSubSpacesBox({super.key, required this.subspaces});
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (subspaces.isEmpty) {
|
||||||
|
return TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
overlayColor: ColorsManager.transparentColor,
|
||||||
|
),
|
||||||
|
onPressed: () => _showSubSpacesDialog(context),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ButtonContentWidget(
|
||||||
|
svgAssets: Assets.addIcon,
|
||||||
|
label: 'Create Sub Spaces',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.textFieldGreyColor,
|
||||||
|
width: 3.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8.0,
|
||||||
|
runSpacing: 8.0,
|
||||||
|
children: [
|
||||||
|
...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)),
|
||||||
|
EditChip(
|
||||||
|
onTap: () => _showSubSpacesDialog(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSubSpacesDialog(BuildContext context) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => SpaceSubSpacesDialog(
|
||||||
|
subspaces: subspaces,
|
||||||
|
onSave: (subspaces) {
|
||||||
|
context.read<SpaceDetailsModelBloc>().add(
|
||||||
|
UpdateSpaceDetailsSubspaces(subspaces),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
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/space_details/presentation/widgets/sub_spaces_input.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class SpaceSubSpacesDialog extends StatefulWidget {
|
||||||
|
const SpaceSubSpacesDialog({
|
||||||
|
required this.subspaces,
|
||||||
|
required this.onSave,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
final void Function(List<Subspace> subspaces) onSave;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceSubSpacesDialog> createState() => _SpaceSubSpacesDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||||
|
late List<Subspace> _subspaces;
|
||||||
|
|
||||||
|
bool get _hasDuplicateNames =>
|
||||||
|
_subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length !=
|
||||||
|
_subspaces.length;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subspaces = List.from(widget.subspaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubspaceAdded(String name) {
|
||||||
|
setState(() {
|
||||||
|
_subspaces = [
|
||||||
|
..._subspaces,
|
||||||
|
Subspace(
|
||||||
|
name: name,
|
||||||
|
uuid: const Uuid().v4(),
|
||||||
|
productAllocations: const [],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSubspaceDeleted(String uuid) => setState(
|
||||||
|
() => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _handleSave() {
|
||||||
|
widget.onSave(_subspaces);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Create Sub Spaces'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SubSpacesInput(
|
||||||
|
subSpaces: _subspaces,
|
||||||
|
onSubspaceAdded: _handleSubspaceAdded,
|
||||||
|
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: Visibility(
|
||||||
|
key: ValueKey(_hasDuplicateNames),
|
||||||
|
visible: _hasDuplicateNames,
|
||||||
|
child: const Text(
|
||||||
|
'Error: Duplicate subspace names are not allowed.',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
SpaceDetailsActionButtons(
|
||||||
|
onSave: _hasDuplicateNames ? null : _handleSave,
|
||||||
|
onCancel: Navigator.of(context).pop,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
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/subspace_chip.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubSpacesInput extends StatefulWidget {
|
||||||
|
const SubSpacesInput({
|
||||||
|
super.key,
|
||||||
|
required this.subSpaces,
|
||||||
|
required this.onSubspaceAdded,
|
||||||
|
required this.onSubspaceDeleted,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Subspace> subSpaces;
|
||||||
|
final void Function(String name) onSubspaceAdded;
|
||||||
|
final void Function(String uuid) onSubspaceDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SubSpacesInputState extends State<SubSpacesInput> {
|
||||||
|
late final TextEditingController _subspaceNameController;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_subspaceNameController = TextEditingController();
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subspaceNameController.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: context.screenWidth * 0.35,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.boxColor,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
...widget.subSpaces.asMap().entries.map(
|
||||||
|
(entry) {
|
||||||
|
final index = entry.key;
|
||||||
|
final subSpace = entry.value;
|
||||||
|
|
||||||
|
final lowerName = subSpace.name.toLowerCase();
|
||||||
|
|
||||||
|
final duplicateIndices = widget.subSpaces
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.where((e) => e.value.name.toLowerCase() == lowerName)
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
final isDuplicate = duplicateIndices.length > 1 &&
|
||||||
|
duplicateIndices.indexOf(index) != 0;
|
||||||
|
return SubspaceChip(
|
||||||
|
subSpace: subSpace,
|
||||||
|
isDuplicate: isDuplicate,
|
||||||
|
onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _subspaceNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
|
||||||
|
hintStyle: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: (value) {
|
||||||
|
final trimmedValue = value.trim();
|
||||||
|
if (trimmedValue.isNotEmpty) {
|
||||||
|
widget.onSubspaceAdded(trimmedValue);
|
||||||
|
_subspaceNameController.clear();
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: context.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
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/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubspaceChip extends StatelessWidget {
|
||||||
|
const SubspaceChip({
|
||||||
|
required this.subSpace,
|
||||||
|
required this.isDuplicate,
|
||||||
|
required this.onDeleted,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Subspace subSpace;
|
||||||
|
final bool isDuplicate;
|
||||||
|
final void Function() onDeleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(
|
||||||
|
subSpace.name,
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(
|
||||||
|
color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor,
|
||||||
|
width: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
deleteIcon: Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDeleted: onDeleted,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,171 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.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/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/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class SubspaceNameDisplayWidget extends StatefulWidget {
|
||||||
|
const SubspaceNameDisplayWidget({super.key, required this.subSpace});
|
||||||
|
|
||||||
|
final Subspace subSpace;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SubspaceNameDisplayWidget> createState() =>
|
||||||
|
_SubspaceNameDisplayWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
bool _isEditing = false;
|
||||||
|
bool _hasDuplicateName = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_controller = TextEditingController(text: widget.subSpace.name);
|
||||||
|
_focusNode = FocusNode();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
_focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _checkForDuplicateName(String name) {
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
return bloc.state.subspaces
|
||||||
|
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||||
|
.any((s) => s.name.toLowerCase() == name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleNameChange(String value) {
|
||||||
|
setState(() {
|
||||||
|
_hasDuplicateName = _checkForDuplicateName(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryToFinishEditing() {
|
||||||
|
if (!_hasDuplicateName) {
|
||||||
|
_onFinishEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryToSubmit(String value) {
|
||||||
|
if (_hasDuplicateName) return;
|
||||||
|
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
bloc.add(
|
||||||
|
UpdateSpaceDetailsSubspaces(
|
||||||
|
bloc.state.subspaces
|
||||||
|
.map(
|
||||||
|
(e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_onFinishEditing();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||||
|
color: ColorsManager.spaceColor,
|
||||||
|
);
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _isEditing = true);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: Chip(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
side: const BorderSide(color: ColorsManager.transparentColor),
|
||||||
|
),
|
||||||
|
onDeleted: () {
|
||||||
|
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||||
|
bloc.add(
|
||||||
|
UpdateSpaceDetailsSubspaces(
|
||||||
|
bloc.state.subspaces
|
||||||
|
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
deleteIcon: Container(
|
||||||
|
padding: const EdgeInsetsDirectional.all(1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const FittedBox(
|
||||||
|
child: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label: Visibility(
|
||||||
|
visible: _isEditing,
|
||||||
|
replacement: Text(
|
||||||
|
widget.subSpace.name,
|
||||||
|
style: textStyle,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: context.screenWidth * 0.065,
|
||||||
|
height: context.screenHeight * 0.025,
|
||||||
|
child: TextField(
|
||||||
|
focusNode: _focusNode,
|
||||||
|
controller: _controller,
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
color: _hasDuplicateName ? Colors.red : null,
|
||||||
|
),
|
||||||
|
decoration: const InputDecoration.collapsed(
|
||||||
|
hintText: '',
|
||||||
|
),
|
||||||
|
onChanged: _handleNameChange,
|
||||||
|
onTapOutside: (_) => _tryToFinishEditing(),
|
||||||
|
onSubmitted: _tryToSubmit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_hasDuplicateName)
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
child: Visibility(
|
||||||
|
key: ValueKey(_hasDuplicateName),
|
||||||
|
visible: _hasDuplicateName,
|
||||||
|
child: Text(
|
||||||
|
'Name already exists',
|
||||||
|
style: textStyle?.copyWith(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onFinishEditing() {
|
||||||
|
setState(() {
|
||||||
|
_isEditing = false;
|
||||||
|
_hasDuplicateName = false;
|
||||||
|
});
|
||||||
|
_focusNode.unfocus();
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
|
||||||
|
part 'space_details_model_event.dart';
|
||||||
|
|
||||||
|
class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsModel> {
|
||||||
|
SpaceDetailsModelBloc({
|
||||||
|
required SpaceDetailsModel initialState,
|
||||||
|
}) : super(initialState) {
|
||||||
|
on<UpdateSpaceDetailsIcon>(_onUpdateSpaceDetailsIcon);
|
||||||
|
on<UpdateSpaceDetailsName>(_onUpdateSpaceDetailsName);
|
||||||
|
on<UpdateSpaceDetailsSubspaces>(_onUpdateSpaceDetailsSubspaces);
|
||||||
|
on<UpdateSpaceDetailsProductAllocations>(
|
||||||
|
_onUpdateSpaceDetailsProductAllocations);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsIcon(
|
||||||
|
UpdateSpaceDetailsIcon event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(icon: event.icon));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsName(
|
||||||
|
UpdateSpaceDetailsName event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(spaceName: event.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsSubspaces(
|
||||||
|
UpdateSpaceDetailsSubspaces event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(subspaces: event.subspaces));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUpdateSpaceDetailsProductAllocations(
|
||||||
|
UpdateSpaceDetailsProductAllocations event,
|
||||||
|
Emitter<SpaceDetailsModel> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(productAllocations: event.productAllocations));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
part of 'space_details_model_bloc.dart';
|
||||||
|
|
||||||
|
sealed class SpaceDetailsModelEvent extends Equatable {
|
||||||
|
const SpaceDetailsModelEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsIcon(this.icon);
|
||||||
|
|
||||||
|
final String icon;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [icon];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsName(this.name);
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [name];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsSubspaces(this.subspaces);
|
||||||
|
|
||||||
|
final List<Subspace> subspaces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [subspaces];
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent {
|
||||||
|
const UpdateSpaceDetailsProductAllocations(this.productAllocations);
|
||||||
|
|
||||||
|
final List<ProductAllocation> productAllocations;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [productAllocations];
|
||||||
|
}
|
@ -52,4 +52,7 @@ final myTheme = ThemeData(
|
|||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
dialogTheme: const DialogThemeData(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user