mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-16 01:56:24 +00:00
Merged with dev
This commit is contained in:
@ -0,0 +1,38 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
|
||||
|
||||
class AddDeviceTypeBloc
|
||||
extends Bloc<AddDeviceTypeEvent, List<SelectedProduct>> {
|
||||
AddDeviceTypeBloc(List<SelectedProduct> initialProducts)
|
||||
: super(initialProducts) {
|
||||
on<UpdateProductCountEvent>(_onUpdateProductCount);
|
||||
}
|
||||
|
||||
void _onUpdateProductCount(
|
||||
UpdateProductCountEvent event, Emitter<List<SelectedProduct>> emit) {
|
||||
final existingProduct = state.firstWhere(
|
||||
(p) => p.productId == event.productId,
|
||||
orElse: () => SelectedProduct(productId: event.productId, count: 0,productName: event.productName,product: event.product ),
|
||||
);
|
||||
|
||||
if (event.count > 0) {
|
||||
if (!state.contains(existingProduct)) {
|
||||
emit([
|
||||
...state,
|
||||
SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product)
|
||||
]);
|
||||
} else {
|
||||
final updatedList = state.map((p) {
|
||||
if (p.productId == event.productId) {
|
||||
return SelectedProduct(productId: p.productId, count: event.count, productName: p.productName,product: p.product);
|
||||
}
|
||||
return p;
|
||||
}).toList();
|
||||
emit(updatedList);
|
||||
}
|
||||
} else {
|
||||
emit(state.where((p) => p.productId != event.productId).toList());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
|
||||
abstract class AddDeviceTypeEvent extends Equatable {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class UpdateProductCountEvent extends AddDeviceTypeEvent {
|
||||
final String productId;
|
||||
final int count;
|
||||
final String productName;
|
||||
final ProductModel product;
|
||||
|
||||
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
|
||||
|
||||
@override
|
||||
List<Object> get props => [productId, count];
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/scrollable_grid_view_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/action_button_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
|
||||
class AddDeviceTypeWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
|
||||
final List<SelectedProduct>? initialSelectedProducts;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? spaceTags;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final Function(List<Tag>,List<SubspaceModel>?)? onSave;
|
||||
|
||||
|
||||
const AddDeviceTypeWidget(
|
||||
{super.key,
|
||||
this.products,
|
||||
this.initialSelectedProducts,
|
||||
this.onProductsSelected,
|
||||
this.subspaces,
|
||||
this.allTags,
|
||||
this.spaceTags,
|
||||
this.onSave,
|
||||
required this.spaceName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final crossAxisCount = size.width > 1200
|
||||
? 8
|
||||
: size.width > 800
|
||||
? 5
|
||||
: 3;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AddDeviceTypeBloc(initialSelectedProducts ?? []),
|
||||
child: Builder(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Add Devices'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SingleChildScrollView(
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.height * 0.65,
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: ScrollableGridViewWidget(
|
||||
products: products, crossAxisCount: crossAxisCount),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
ActionButton(
|
||||
label: 'Continue',
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
onPressed: () async {
|
||||
final currentState =
|
||||
context.read<AddDeviceTypeBloc>().state;
|
||||
Navigator.of(context).pop();
|
||||
|
||||
if (currentState.isNotEmpty) {
|
||||
final initialTags = generateInitialTags(
|
||||
spaceTags: spaceTags,
|
||||
subspaces: subspaces,
|
||||
);
|
||||
|
||||
final dialogTitle = initialTags.isNotEmpty
|
||||
? 'Edit Device'
|
||||
: 'Assign Tags';
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AssignTagDialog(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
addedProducts: currentState,
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
initialTags: initialTags,
|
||||
title: dialogTitle,
|
||||
onSave: (tags,subspaces){
|
||||
onSave!(tags,subspaces);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
List<Tag> generateInitialTags({
|
||||
List<Tag>? spaceTags,
|
||||
List<SubspaceModel>? subspaces,
|
||||
}) {
|
||||
final List<Tag> initialTags = [];
|
||||
|
||||
if (spaceTags != null) {
|
||||
initialTags.addAll(spaceTags);
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
initialTags.addAll(
|
||||
subspace.tags!.map(
|
||||
(tag) => tag.copyWith(location: subspace.subspaceName),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialTags;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_type_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.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/pages/spaces_management/tag_model/widgets/device_name_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class DeviceTypeTileWidget extends StatelessWidget {
|
||||
final ProductModel product;
|
||||
final List<SelectedProduct> productCounts;
|
||||
|
||||
const DeviceTypeTileWidget({
|
||||
super.key,
|
||||
required this.product,
|
||||
required this.productCounts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedProduct = productCounts.firstWhere(
|
||||
(p) => p.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: product.uuid,
|
||||
count: 0,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
DeviceIconWidget(icon: product.icon ?? Assets.doorSensor),
|
||||
const SizedBox(height: 4),
|
||||
DeviceNameWidget(name: product.name),
|
||||
const SizedBox(height: 4),
|
||||
CounterWidget(
|
||||
isCreate: false,
|
||||
initialCount: selectedProduct.count,
|
||||
onCountChanged: (newCount) {
|
||||
context.read<AddDeviceTypeBloc>().add(
|
||||
UpdateProductCountEvent(
|
||||
productId: product.uuid,
|
||||
count: newCount,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/add_device_type/widgets/device_type_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
class ScrollableGridViewWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final int crossAxisCount;
|
||||
final List<SelectedProduct>? initialProductCounts;
|
||||
|
||||
const ScrollableGridViewWidget({
|
||||
super.key,
|
||||
required this.products,
|
||||
required this.crossAxisCount,
|
||||
this.initialProductCounts,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: BlocBuilder<AddDeviceTypeBloc, List<SelectedProduct>>(
|
||||
builder: (context, productCounts) {
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 6,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: .8,
|
||||
),
|
||||
itemCount: products?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products![index];
|
||||
final initialProductCount = _findInitialProductCount(product);
|
||||
|
||||
return DeviceTypeTileWidget(
|
||||
product: product,
|
||||
productCounts: initialProductCount != null
|
||||
? [...productCounts, initialProductCount]
|
||||
: productCounts,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SelectedProduct? _findInitialProductCount(ProductModel product) {
|
||||
if (initialProductCounts == null) return null;
|
||||
final matchingProduct = initialProductCounts!.firstWhere(
|
||||
(selectedProduct) => selectedProduct.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: '',
|
||||
count: 0,
|
||||
productName: '',
|
||||
product: null,
|
||||
),
|
||||
);
|
||||
|
||||
return matchingProduct.productId.isNotEmpty ? matchingProduct : null;
|
||||
}
|
||||
}
|
@ -1,20 +1,26 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
import 'package:syncrow_web/services/product_api.dart';
|
||||
import 'package:syncrow_web/services/space_mana_api.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
|
||||
class SpaceManagementBloc
|
||||
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
|
||||
final CommunitySpaceManagementApi _api;
|
||||
final ProductApi _productApi;
|
||||
final SpaceModelManagementApi _spaceModelApi;
|
||||
|
||||
List<ProductModel>? _cachedProducts;
|
||||
|
||||
SpaceManagementBloc(this._api, this._productApi)
|
||||
SpaceManagementBloc(this._api, this._productApi, this._spaceModelApi)
|
||||
: super(SpaceManagementInitial()) {
|
||||
on<LoadCommunityAndSpacesEvent>(_onLoadCommunityAndSpaces);
|
||||
on<UpdateSpacePositionEvent>(_onUpdateSpacePosition);
|
||||
@ -26,6 +32,8 @@ class SpaceManagementBloc
|
||||
on<FetchProductsEvent>(_onFetchProducts);
|
||||
on<SelectSpaceEvent>(_onSelectSpace);
|
||||
on<NewCommunityEvent>(_onNewCommunity);
|
||||
on<BlankStateEvent>(_onBlankState);
|
||||
on<SpaceModelLoadEvent>(_onLoadSpaceModel);
|
||||
}
|
||||
|
||||
void _onUpdateCommunity(
|
||||
@ -47,10 +55,14 @@ class SpaceManagementBloc
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: updatedCommunities,
|
||||
products: previousState.products,
|
||||
selectedCommunity: previousState.selectedCommunity,
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@ -61,6 +73,41 @@ class SpaceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SpaceTemplateModel>> fetchSpaceModels(
|
||||
SpaceManagementState previousState) async {
|
||||
try {
|
||||
List<SpaceTemplateModel> allSpaces = [];
|
||||
List<SpaceTemplateModel> prevSpaceModels = [];
|
||||
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
prevSpaceModels = List<SpaceTemplateModel>.from(
|
||||
(previousState as dynamic).spaceModels ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
if (prevSpaceModels.isEmpty) {
|
||||
bool hasNext = true;
|
||||
int page = 1;
|
||||
|
||||
while (hasNext) {
|
||||
final spaces = await _spaceModelApi.listSpaceModels(page: page);
|
||||
if (spaces.isNotEmpty) {
|
||||
allSpaces.addAll(spaces);
|
||||
page++;
|
||||
} else {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
prevSpaceModels = await _spaceModelApi.listSpaceModels(page: 1);
|
||||
}
|
||||
|
||||
return allSpaces;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void _onloadProducts() async {
|
||||
if (_cachedProducts == null) {
|
||||
final products = await _productApi.fetchProducts();
|
||||
@ -84,19 +131,66 @@ class SpaceManagementBloc
|
||||
return await _api.getSpaceHierarchy(communityUuid);
|
||||
}
|
||||
|
||||
void _onNewCommunity(
|
||||
Future<void> _onNewCommunity(
|
||||
NewCommunityEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) {
|
||||
) async {
|
||||
try {
|
||||
final previousState = state;
|
||||
|
||||
if (event.communities.isEmpty) {
|
||||
emit(const SpaceManagementError('No communities provided.'));
|
||||
return;
|
||||
}
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
emit(BlankState(
|
||||
communities: event.communities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
} catch (error) {
|
||||
emit(SpaceManagementError('Error loading communities: $error'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onBlankState(
|
||||
BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
|
||||
try {
|
||||
final previousState = state;
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
final prevCommunities = (previousState as dynamic).communities ?? [];
|
||||
emit(BlankState(
|
||||
communities: List<CommunityModel>.from(prevCommunities),
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final communities = await _api.fetchCommunities();
|
||||
final updatedCommunities =
|
||||
await Future.wait(communities.map((community) async {
|
||||
final spaces = await _fetchSpacesForCommunity(community.uuid);
|
||||
return CommunityModel(
|
||||
uuid: community.uuid,
|
||||
createdAt: community.createdAt,
|
||||
updatedAt: community.updatedAt,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
spaces: spaces,
|
||||
region: community.region,
|
||||
);
|
||||
}));
|
||||
|
||||
emit(BlankState(
|
||||
spaceModels: prevSpaceModels,
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
));
|
||||
} catch (error) {
|
||||
emit(SpaceManagementError('Error loading communities: $error'));
|
||||
@ -107,6 +201,7 @@ class SpaceManagementBloc
|
||||
LoadCommunityAndSpacesEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) async {
|
||||
var prevState = state;
|
||||
emit(SpaceManagementLoading());
|
||||
try {
|
||||
_onloadProducts();
|
||||
@ -128,8 +223,11 @@ class SpaceManagementBloc
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
final prevSpaceModels = await fetchSpaceModels(prevState);
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: updatedCommunities, products: _cachedProducts ?? []));
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels));
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error loading communities and spaces: $e'));
|
||||
}
|
||||
@ -169,6 +267,7 @@ class SpaceManagementBloc
|
||||
try {
|
||||
CommunityModel? newCommunity =
|
||||
await _api.createCommunity(event.name, event.description);
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
if (newCommunity != null) {
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
@ -178,6 +277,7 @@ class SpaceManagementBloc
|
||||
);
|
||||
final updatedCommunities = prevCommunities..add(newCommunity);
|
||||
emit(SpaceManagementLoaded(
|
||||
spaceModels: prevSpaceModels,
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: newCommunity,
|
||||
@ -195,11 +295,15 @@ class SpaceManagementBloc
|
||||
SelectCommunityEvent event,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) async {
|
||||
_handleCommunitySpaceStateUpdate(
|
||||
emit: emit,
|
||||
selectedCommunity: event.selectedCommunity,
|
||||
selectedSpace: null,
|
||||
);
|
||||
try {
|
||||
_handleCommunitySpaceStateUpdate(
|
||||
emit: emit,
|
||||
selectedCommunity: event.selectedCommunity,
|
||||
selectedSpace: null,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error updating state: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
void _onSelectSpace(
|
||||
@ -223,16 +327,21 @@ class SpaceManagementBloc
|
||||
|
||||
try {
|
||||
if (previousState is SpaceManagementLoaded ||
|
||||
previousState is BlankState) {
|
||||
previousState is BlankState ||
|
||||
previousState is SpaceModelLoaded) {
|
||||
final communities = List<CommunityModel>.from(
|
||||
(previousState as dynamic).communities,
|
||||
);
|
||||
|
||||
final spaceModels = List<SpaceTemplateModel>.from(
|
||||
(previousState as dynamic).spaceModels,
|
||||
);
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
));
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
spaceModels: spaceModels ?? []));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error updating state: $e'));
|
||||
@ -255,7 +364,7 @@ class SpaceManagementBloc
|
||||
emit(SpaceCreationSuccess(spaces: updatedSpaces));
|
||||
|
||||
if (previousState is SpaceManagementLoaded) {
|
||||
_updateLoadedState(
|
||||
await _updateLoadedState(
|
||||
previousState,
|
||||
allSpaces,
|
||||
event.communityUuid,
|
||||
@ -273,23 +382,25 @@ class SpaceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _updateLoadedState(
|
||||
Future<void> _updateLoadedState(
|
||||
SpaceManagementLoaded previousState,
|
||||
List<SpaceModel> allSpaces,
|
||||
String communityUuid,
|
||||
Emitter<SpaceManagementState> emit,
|
||||
) {
|
||||
) async {
|
||||
var prevSpaceModels = await fetchSpaceModels(previousState);
|
||||
|
||||
final communities = List<CommunityModel>.from(previousState.communities);
|
||||
|
||||
for (var community in communities) {
|
||||
if (community.uuid == communityUuid) {
|
||||
community.spaces = allSpaces;
|
||||
emit(SpaceManagementLoaded(
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: community,
|
||||
selectedSpace: null,
|
||||
));
|
||||
communities: communities,
|
||||
products: _cachedProducts ?? [],
|
||||
selectedCommunity: community,
|
||||
selectedSpace: null,
|
||||
spaceModels: prevSpaceModels));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -309,34 +420,53 @@ class SpaceManagementBloc
|
||||
await _api.deleteSpace(communityUuid, parent.uuid!);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow; // Decide whether to stop execution or continue
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
orderedSpaces.removeWhere((space) => parentsToDelete.contains(space));
|
||||
|
||||
for (var space in orderedSpaces) {
|
||||
try {
|
||||
if (space.uuid != null && space.uuid!.isNotEmpty) {
|
||||
final response = await _api.updateSpace(
|
||||
communityId: communityUuid,
|
||||
spaceId: space.uuid!,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
products: space.selectedProducts);
|
||||
communityId: communityUuid,
|
||||
spaceId: space.uuid!,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
);
|
||||
} else {
|
||||
// Call create if the space does not have a UUID
|
||||
final List<CreateTagBodyModel> tagBodyModels = space.tags != null
|
||||
? space.tags!.map((tag) => tag.toCreateTagBodyModel()).toList()
|
||||
: [];
|
||||
|
||||
final createSubspaceBodyModels = space.subspaces?.map((subspace) {
|
||||
final tagBodyModels = subspace.tags
|
||||
?.map((tag) => tag.toCreateTagBodyModel())
|
||||
.toList() ??
|
||||
[];
|
||||
return CreateSubspaceModel()
|
||||
..subspaceName = subspace.subspaceName
|
||||
..tags = tagBodyModels;
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
final response = await _api.createSpace(
|
||||
communityId: communityUuid,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
products: space.selectedProducts);
|
||||
communityId: communityUuid,
|
||||
name: space.name,
|
||||
parentId: space.parent?.uuid,
|
||||
isPrivate: space.isPrivate,
|
||||
position: space.position,
|
||||
icon: space.icon,
|
||||
direction: space.incomingConnection?.direction,
|
||||
spaceModelUuid: space.spaceModel?.uuid,
|
||||
tags: tagBodyModels,
|
||||
subspaces: createSubspaceBodyModels,
|
||||
);
|
||||
space.uuid = response?.uuid;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -370,4 +500,39 @@ class SpaceManagementBloc
|
||||
}
|
||||
return result.toList(); // Convert back to a list
|
||||
}
|
||||
|
||||
void _onLoadSpaceModel(
|
||||
SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
|
||||
emit(SpaceManagementLoading());
|
||||
try {
|
||||
var prevState = state;
|
||||
|
||||
List<CommunityModel> communities = await _api.fetchCommunities();
|
||||
|
||||
List<CommunityModel> updatedCommunities = await Future.wait(
|
||||
communities.map((community) async {
|
||||
List<SpaceModel> spaces =
|
||||
await _fetchSpacesForCommunity(community.uuid);
|
||||
return CommunityModel(
|
||||
uuid: community.uuid,
|
||||
createdAt: community.createdAt,
|
||||
updatedAt: community.updatedAt,
|
||||
name: community.name,
|
||||
description: community.description,
|
||||
spaces: spaces, // New spaces list
|
||||
region: community.region,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
var prevSpaceModels = await fetchSpaceModels(prevState);
|
||||
|
||||
emit(SpaceModelLoaded(
|
||||
communities: updatedCommunities,
|
||||
products: _cachedProducts ?? [],
|
||||
spaceModels: prevSpaceModels));
|
||||
} catch (e) {
|
||||
emit(SpaceManagementError('Error loading communities and spaces: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,3 +140,8 @@ class LoadSpaceHierarchyEvent extends SpaceManagementEvent {
|
||||
@override
|
||||
List<Object> get props => [communityId];
|
||||
}
|
||||
|
||||
|
||||
class BlankStateEvent extends SpaceManagementEvent {}
|
||||
|
||||
class SpaceModelLoadEvent extends SpaceManagementEvent {}
|
||||
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceManagementState extends Equatable {
|
||||
const SpaceManagementState();
|
||||
@ -19,22 +20,27 @@ class SpaceManagementLoaded extends SpaceManagementState {
|
||||
final List<ProductModel> products;
|
||||
CommunityModel? selectedCommunity;
|
||||
SpaceModel? selectedSpace;
|
||||
List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
SpaceManagementLoaded(
|
||||
{required this.communities,
|
||||
required this.products,
|
||||
this.selectedCommunity,
|
||||
this.selectedSpace});
|
||||
this.selectedSpace,
|
||||
this.spaceModels});
|
||||
}
|
||||
|
||||
class SpaceModelManagenetLoaded extends SpaceManagementState {
|
||||
SpaceModelManagenetLoaded();
|
||||
}
|
||||
|
||||
class BlankState extends SpaceManagementState {
|
||||
final List<CommunityModel> communities;
|
||||
final List<ProductModel> products;
|
||||
List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
BlankState({
|
||||
required this.communities,
|
||||
required this.products,
|
||||
});
|
||||
BlankState(
|
||||
{required this.communities, required this.products, this.spaceModels});
|
||||
}
|
||||
|
||||
class SpaceCreationSuccess extends SpaceManagementState {
|
||||
@ -54,3 +60,18 @@ class SpaceManagementError extends SpaceManagementState {
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
||||
class SpaceModelLoaded extends SpaceManagementState {
|
||||
List<SpaceTemplateModel> spaceModels;
|
||||
final List<ProductModel> products;
|
||||
final List<CommunityModel> communities;
|
||||
|
||||
SpaceModelLoaded({
|
||||
required this.communities,
|
||||
required this.products,
|
||||
required this.spaceModels,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [communities, products, spaceModels];
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
|
||||
class CreateSubspaceModel {
|
||||
late String subspaceName;
|
||||
late List<CreateTagBodyModel>? tags;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((tag) => tag.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
@ -19,7 +19,6 @@ class ProductModel {
|
||||
|
||||
// Factory method to create a Product from JSON
|
||||
factory ProductModel.fromMap(Map<String, dynamic> json) {
|
||||
String icon = _mapIconToProduct(json['prodType']);
|
||||
return ProductModel(
|
||||
uuid: json['uuid'],
|
||||
catName: json['catName'],
|
||||
@ -67,4 +66,25 @@ class ProductModel {
|
||||
String toString() {
|
||||
return 'ProductModel(uuid: $uuid, catName: $catName, prodId: $prodId, prodType: $prodType, name: $name, icon: $icon)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ProductModel &&
|
||||
runtimeType == other.runtimeType &&
|
||||
uuid == other.uuid &&
|
||||
catName == other.catName &&
|
||||
prodId == other.prodId &&
|
||||
prodType == other.prodType &&
|
||||
name == other.name &&
|
||||
icon == other.icon;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
uuid.hashCode ^
|
||||
catName.hashCode ^
|
||||
prodId.hashCode ^
|
||||
prodType.hashCode ^
|
||||
name.hashCode ^
|
||||
icon.hashCode;
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
|
||||
class SelectedProduct {
|
||||
final String productId;
|
||||
int count;
|
||||
final String productName;
|
||||
final ProductModel? product;
|
||||
|
||||
SelectedProduct({required this.productId, required this.count});
|
||||
SelectedProduct({required this.productId, required this.count, required this.productName, this.product});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'productId': productId,
|
||||
'count': count,
|
||||
'productName': productName,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import 'dart:ui';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum SpaceStatus { newSpace, modified, unchanged, deleted }
|
||||
enum SpaceStatus { newSpace, modified, unchanged, deleted, parentDeleted }
|
||||
|
||||
class SpaceModel {
|
||||
String? uuid;
|
||||
@ -20,8 +22,10 @@ class SpaceModel {
|
||||
Offset position;
|
||||
bool isHovered;
|
||||
SpaceStatus status;
|
||||
List<SelectedProduct> selectedProducts;
|
||||
String internalId;
|
||||
SpaceTemplateModel? spaceModel;
|
||||
List<Tag>? tags;
|
||||
List<SubspaceModel>? subspaces;
|
||||
|
||||
List<Connection> outgoingConnections = []; // Connections from this space
|
||||
Connection? incomingConnection; // Connections to this space
|
||||
@ -41,7 +45,9 @@ class SpaceModel {
|
||||
this.isHovered = false,
|
||||
this.incomingConnection,
|
||||
this.status = SpaceStatus.unchanged,
|
||||
this.selectedProducts = const [],
|
||||
this.spaceModel,
|
||||
this.tags,
|
||||
this.subspaces,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SpaceModel.fromJson(Map<String, dynamic> json,
|
||||
@ -64,6 +70,11 @@ class SpaceModel {
|
||||
name: json['spaceName'],
|
||||
isPrivate: json['isPrivate'] ?? false,
|
||||
invitationCode: json['invitationCode'],
|
||||
subspaces: (json['subspaces'] as List<dynamic>?)
|
||||
?.where((e) => e is Map<String, dynamic>) // Validate type
|
||||
.map((e) => SubspaceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
parent: parentInternalId != null
|
||||
? SpaceModel(
|
||||
internalId: parentInternalId,
|
||||
@ -85,14 +96,11 @@ class SpaceModel {
|
||||
icon: json['icon'] ?? Assets.location,
|
||||
position: Offset(json['x'] ?? 0, json['y'] ?? 0),
|
||||
isHovered: false,
|
||||
selectedProducts: json['spaceProducts'] != null
|
||||
? (json['spaceProducts'] as List).map((product) {
|
||||
return SelectedProduct(
|
||||
productId: product['product']['uuid'],
|
||||
count: product['productCount'],
|
||||
);
|
||||
}).toList()
|
||||
: [],
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.where((item) => item is Map<String, dynamic>) // Validate type
|
||||
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
|
||||
if (json['incomingConnections'] != null &&
|
||||
@ -118,6 +126,7 @@ class SpaceModel {
|
||||
'isPrivate': isPrivate,
|
||||
'invitationCode': invitationCode,
|
||||
'parent': parent?.uuid,
|
||||
'subspaces': subspaces?.map((e) => e.toJson()).toList(),
|
||||
'community': community?.toMap(),
|
||||
'children': children.map((child) => child.toMap()).toList(),
|
||||
'icon': icon,
|
||||
@ -125,6 +134,7 @@ class SpaceModel {
|
||||
'isHovered': isHovered,
|
||||
'outgoingConnections': outgoingConnections.map((c) => c.toMap()).toList(),
|
||||
'incomingConnection': incomingConnection?.toMap(),
|
||||
'tags': tags?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,3 +142,28 @@ class SpaceModel {
|
||||
outgoingConnections.add(connection);
|
||||
}
|
||||
}
|
||||
|
||||
extension SpaceExtensions on SpaceModel {
|
||||
List<String> listAllTagValues() {
|
||||
final List<String> tagValues = [];
|
||||
|
||||
if (tags != null) {
|
||||
tagValues.addAll(
|
||||
tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty));
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (final subspace in subspaces!) {
|
||||
if (subspace.tags != null) {
|
||||
tagValues.addAll(
|
||||
subspace.tags!
|
||||
.map((tag) => tag.tag ?? '')
|
||||
.where((tag) => tag.isNotEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagValues;
|
||||
}
|
||||
}
|
||||
|
110
lib/pages/spaces_management/all_spaces/model/subspace_model.dart
Normal file
110
lib/pages/spaces_management/all_spaces/model/subspace_model.dart
Normal file
@ -0,0 +1,110 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
import 'tag.dart';
|
||||
|
||||
class SubspaceModel {
|
||||
final String? uuid;
|
||||
String subspaceName;
|
||||
final bool disabled;
|
||||
List<Tag>? tags;
|
||||
|
||||
SubspaceModel({
|
||||
this.uuid,
|
||||
required this.subspaceName,
|
||||
required this.disabled,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory SubspaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return SubspaceModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
disabled: json['disabled'] ?? false,
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((item) => Tag.fromJson(item))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'disabled': disabled,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateSubspaceModel {
|
||||
final String uuid;
|
||||
final Action action;
|
||||
final String? subspaceName;
|
||||
final List<UpdateTag>? tags;
|
||||
UpdateSubspaceModel({
|
||||
required this.action,
|
||||
required this.uuid,
|
||||
this.subspaceName,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory UpdateSubspaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateSubspaceModel(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
tags: (json['tags'] as List)
|
||||
.map((item) => UpdateTag.fromJson(item))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateTag {
|
||||
final Action action;
|
||||
final String? uuid;
|
||||
final String tag;
|
||||
final bool disabled;
|
||||
final ProductModel? product;
|
||||
|
||||
UpdateTag({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
required this.tag,
|
||||
required this.disabled,
|
||||
this.product,
|
||||
});
|
||||
|
||||
factory UpdateTag.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateTag(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
tag: json['tag'] ?? '',
|
||||
disabled: json['disabled'] ?? false,
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'disabled': disabled,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
68
lib/pages/spaces_management/all_spaces/model/tag.dart
Normal file
68
lib/pages/spaces_management/all_spaces/model/tag.dart
Normal file
@ -0,0 +1,68 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Tag {
|
||||
String? uuid;
|
||||
String? tag;
|
||||
final ProductModel? product;
|
||||
String internalId;
|
||||
String? location;
|
||||
|
||||
Tag(
|
||||
{this.uuid,
|
||||
required this.tag,
|
||||
this.product,
|
||||
String? internalId,
|
||||
this.location})
|
||||
: internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return Tag(
|
||||
uuid: json['uuid'] ?? '',
|
||||
internalId: internalId,
|
||||
tag: json['tag'] ?? '',
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Tag copyWith({
|
||||
String? tag,
|
||||
ProductModel? product,
|
||||
String? location,
|
||||
}) {
|
||||
return Tag(
|
||||
tag: tag ?? this.tag,
|
||||
product: product ?? this.product,
|
||||
location: location ?? this.location,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension TagModelExtensions on Tag {
|
||||
TagBodyModel toTagBodyModel() {
|
||||
return TagBodyModel()
|
||||
..uuid = uuid ?? ''
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
|
||||
CreateTagBodyModel toCreateTagBodyModel() {
|
||||
return CreateTagBodyModel()
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/loaded_space_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/view/center_body_widget.dart';
|
||||
import 'package:syncrow_web/services/product_api.dart';
|
||||
import 'package:syncrow_web/services/space_mana_api.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatefulWidget {
|
||||
@ -22,48 +22,59 @@ class SpaceManagementPage extends StatefulWidget {
|
||||
class SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
final CommunitySpaceManagementApi _api = CommunitySpaceManagementApi();
|
||||
final ProductApi _productApi = ProductApi();
|
||||
Map<String, List<SpaceModel>> communitySpaces = {};
|
||||
List<ProductModel> products = [];
|
||||
bool isProductDataLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceManagementBloc(_api, _productApi)
|
||||
..add(LoadCommunityAndSpacesEvent()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => SpaceManagementBloc(_api, _productApi, _spaceModelApi)
|
||||
..add(LoadCommunityAndSpacesEvent()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => CenterBodyBloc(),
|
||||
),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text('Space Management',
|
||||
style: Theme.of(context).textTheme.headlineLarge),
|
||||
enableMenuSidebar: false,
|
||||
centerBody: CenterBodyWidget(),
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: BlocBuilder<SpaceManagementBloc, SpaceManagementState>(
|
||||
builder: (context, state) {
|
||||
if (state is SpaceManagementLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is BlankState) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: null,
|
||||
selectedSpace: null,
|
||||
products: state.products,
|
||||
);
|
||||
} else if (state is SpaceManagementLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: state.selectedCommunity,
|
||||
selectedSpace: state.selectedSpace,
|
||||
products: state.products,
|
||||
);
|
||||
} else if (state is SpaceManagementError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return Container();
|
||||
}),
|
||||
builder: (context, state) {
|
||||
if (state is SpaceManagementLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is BlankState) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: null,
|
||||
selectedSpace: null,
|
||||
products: state.products,
|
||||
shouldNavigateToSpaceModelPage: false,
|
||||
);
|
||||
} else if (state is SpaceManagementLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
selectedCommunity: state.selectedCommunity,
|
||||
selectedSpace: state.selectedSpace,
|
||||
products: state.products,
|
||||
spaceModels: state.spaceModels,
|
||||
shouldNavigateToSpaceModelPage: false,
|
||||
);
|
||||
} else if (state is SpaceModelLoaded) {
|
||||
return LoadedSpaceView(
|
||||
communities: state.communities,
|
||||
products: state.products,
|
||||
spaceModels: state.spaceModels,
|
||||
shouldNavigateToSpaceModelPage: true,
|
||||
);
|
||||
} else if (state is SpaceManagementError) {
|
||||
return Center(child: Text('Error: ${state.errorMessage}'));
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
_buildActionButton('Cancel', ColorsManager.boxColor, ColorsManager.blackColor, () {
|
||||
Navigator.of(context).pop();
|
||||
}),
|
||||
_buildActionButton('Continue', ColorsManager.secondaryColor, Colors.white, () {
|
||||
_buildActionButton('Continue', ColorsManager.secondaryColor, ColorsManager.whiteColors, () {
|
||||
Navigator.of(context).pop();
|
||||
if (widget.onProductsSelected != null) {
|
||||
widget.onProductsSelected!(productCounts);
|
||||
@ -114,7 +114,7 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
Widget _buildDeviceTypeTile(ProductModel product, Size size) {
|
||||
final selectedProduct = productCounts.firstWhere(
|
||||
(p) => p.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(productId: product.uuid, count: 0),
|
||||
orElse: () => SelectedProduct(productId: product.uuid, count: 0, productName: product.catName, product: product),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
@ -137,13 +137,14 @@ class _AddDeviceWidgetState extends State<AddDeviceWidget> {
|
||||
_buildDeviceName(product, size),
|
||||
const SizedBox(height: 4),
|
||||
CounterWidget(
|
||||
isCreate: false,
|
||||
initialCount: selectedProduct.count,
|
||||
onCountChanged: (newCount) {
|
||||
setState(() {
|
||||
if (newCount > 0) {
|
||||
if (!productCounts.contains(selectedProduct)) {
|
||||
productCounts
|
||||
.add(SelectedProduct(productId: product.uuid, count: newCount));
|
||||
.add(SelectedProduct(productId: product.uuid, count: newCount, productName: product.catName, product: product));
|
||||
} else {
|
||||
selectedProduct.count = newCount;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_community/view/create_community_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
@ -17,6 +18,7 @@ class CommunityStructureHeader extends StatefulWidget {
|
||||
final ValueChanged<String> onNameSubmitted;
|
||||
final List<CommunityModel> communities;
|
||||
final CommunityModel? community;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
const CommunityStructureHeader(
|
||||
{super.key,
|
||||
@ -29,7 +31,8 @@ class CommunityStructureHeader extends StatefulWidget {
|
||||
required this.onEditName,
|
||||
required this.onNameSubmitted,
|
||||
this.community,
|
||||
required this.communities});
|
||||
required this.communities,
|
||||
this.selectedSpace});
|
||||
|
||||
@override
|
||||
State<CommunityStructureHeader> createState() =>
|
||||
@ -137,10 +140,8 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.isSave) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButtons(theme),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
_buildActionButtons(theme),
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -152,11 +153,19 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (widget.isSave)
|
||||
_buildButton(
|
||||
label: "Save",
|
||||
icon: const Icon(Icons.save,
|
||||
size: 18, color: ColorsManager.spaceColor),
|
||||
onPressed: widget.onSave,
|
||||
theme: theme),
|
||||
if(widget.selectedSpace!= null)
|
||||
_buildButton(
|
||||
label: "Save",
|
||||
icon: const Icon(Icons.save,
|
||||
size: 18, color: ColorsManager.spaceColor),
|
||||
onPressed: widget.onSave,
|
||||
label: "Delete",
|
||||
icon: const Icon(Icons.delete,
|
||||
size: 18, color: ColorsManager.warningRed),
|
||||
onPressed: widget.onDelete,
|
||||
theme: theme),
|
||||
],
|
||||
);
|
||||
@ -178,7 +187,7 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
|
||||
padding: 2.0,
|
||||
height: buttonHeight,
|
||||
elevation: 0,
|
||||
borderColor: Colors.grey.shade300,
|
||||
borderColor: ColorsManager.lightGrayColor,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
@ -11,12 +11,15 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/connection_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_community_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_header_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_container_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CommunityStructureArea extends StatefulWidget {
|
||||
@ -26,6 +29,7 @@ class CommunityStructureArea extends StatefulWidget {
|
||||
final ValueChanged<SpaceModel?>? onSpaceSelected;
|
||||
final List<CommunityModel> communities;
|
||||
final List<SpaceModel> spaces;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
|
||||
CommunityStructureArea({
|
||||
this.selectedCommunity,
|
||||
@ -34,6 +38,7 @@ class CommunityStructureArea extends StatefulWidget {
|
||||
this.products,
|
||||
required this.spaces,
|
||||
this.onSpaceSelected,
|
||||
this.spaceModels,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -126,6 +131,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
isEditingName: isEditingName,
|
||||
nameController: _nameController,
|
||||
onSave: _saveSpaces,
|
||||
selectedSpace: widget.selectedSpace,
|
||||
onDelete: _onDelete,
|
||||
onEditName: () {
|
||||
setState(() {
|
||||
@ -171,7 +177,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
painter: CurvedLinePainter([connection])),
|
||||
),
|
||||
for (var entry in spaces.asMap().entries)
|
||||
if (entry.value.status != SpaceStatus.deleted)
|
||||
if (entry.value.status != SpaceStatus.deleted &&
|
||||
entry.value.status != SpaceStatus.parentDeleted)
|
||||
Positioned(
|
||||
left: entry.value.position.dx,
|
||||
top: entry.value.position.dy,
|
||||
@ -284,12 +291,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
builder: (BuildContext context) {
|
||||
return CreateSpaceDialog(
|
||||
products: widget.products,
|
||||
spaceModels: widget.spaceModels,
|
||||
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
|
||||
onCreateSpace: (String name, String icon,
|
||||
List<SelectedProduct> selectedProducts) {
|
||||
onCreateSpace: (String name,
|
||||
String icon,
|
||||
List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel,
|
||||
List<SubspaceModel>? subspaces,
|
||||
List<Tag>? tags) {
|
||||
setState(() {
|
||||
// Set the first space in the center or use passed position
|
||||
|
||||
Offset centerPosition =
|
||||
position ?? _getCenterPosition(screenSize);
|
||||
SpaceModel newSpace = SpaceModel(
|
||||
@ -299,7 +310,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
isPrivate: false,
|
||||
children: [],
|
||||
status: SpaceStatus.newSpace,
|
||||
selectedProducts: selectedProducts);
|
||||
spaceModel: spaceModel,
|
||||
subspaces: subspaces,
|
||||
tags: tags);
|
||||
|
||||
if (parentIndex != null && direction != null) {
|
||||
SpaceModel parentSpace = spaces[parentIndex];
|
||||
@ -335,14 +348,19 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
icon: space.icon,
|
||||
editSpace: space,
|
||||
isEdit: true,
|
||||
selectedProducts: space.selectedProducts,
|
||||
onCreateSpace: (String name, String icon,
|
||||
List<SelectedProduct> selectedProducts) {
|
||||
onCreateSpace: (String name,
|
||||
String icon,
|
||||
List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel,
|
||||
List<SubspaceModel>? subspaces,
|
||||
List<Tag>? tags) {
|
||||
setState(() {
|
||||
// Update the space's properties
|
||||
space.name = name;
|
||||
space.icon = icon;
|
||||
space.selectedProducts = selectedProducts;
|
||||
space.spaceModel = spaceModel;
|
||||
space.subspaces = subspaces;
|
||||
space.tags = tags;
|
||||
|
||||
if (space.status != SpaceStatus.newSpace) {
|
||||
space.status = SpaceStatus.modified; // Mark as modified
|
||||
@ -365,10 +383,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
List<SpaceModel> result = [];
|
||||
|
||||
void flatten(SpaceModel space) {
|
||||
if (space.status == SpaceStatus.deleted) return;
|
||||
|
||||
if (space.status == SpaceStatus.deleted ||
|
||||
space.status == SpaceStatus.parentDeleted) {
|
||||
return;
|
||||
}
|
||||
result.add(space);
|
||||
|
||||
for (var child in space.children) {
|
||||
flatten(child);
|
||||
}
|
||||
@ -436,21 +455,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
}
|
||||
|
||||
void _onDelete() {
|
||||
if (widget.selectedCommunity != null &&
|
||||
widget.selectedCommunity?.uuid != null &&
|
||||
widget.selectedSpace == null) {
|
||||
context.read<SpaceManagementBloc>().add(DeleteCommunityEvent(
|
||||
communityUuid: widget.selectedCommunity!.uuid,
|
||||
));
|
||||
}
|
||||
if (widget.selectedSpace != null) {
|
||||
setState(() {
|
||||
for (var space in spaces) {
|
||||
if (space.uuid == widget.selectedSpace?.uuid) {
|
||||
if (space.internalId == widget.selectedSpace?.internalId) {
|
||||
space.status = SpaceStatus.deleted;
|
||||
_markChildrenAsDeleted(space);
|
||||
}
|
||||
}
|
||||
|
||||
_removeConnectionsForDeletedSpaces();
|
||||
});
|
||||
}
|
||||
@ -458,7 +471,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
|
||||
void _markChildrenAsDeleted(SpaceModel parent) {
|
||||
for (var child in parent.children) {
|
||||
child.status = SpaceStatus.deleted;
|
||||
child.status = SpaceStatus.parentDeleted;
|
||||
|
||||
_markChildrenAsDeleted(child);
|
||||
}
|
||||
}
|
||||
@ -466,7 +480,9 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
|
||||
void _removeConnectionsForDeletedSpaces() {
|
||||
connections.removeWhere((connection) {
|
||||
return connection.startSpace.status == SpaceStatus.deleted ||
|
||||
connection.endSpace.status == SpaceStatus.deleted;
|
||||
connection.endSpace.status == SpaceStatus.deleted ||
|
||||
connection.startSpace.status == SpaceStatus.parentDeleted ||
|
||||
connection.endSpace.status == SpaceStatus.parentDeleted;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
class CounterWidget extends StatefulWidget {
|
||||
final int initialCount;
|
||||
final ValueChanged<int> onCountChanged;
|
||||
final bool isCreate;
|
||||
|
||||
const CounterWidget({
|
||||
Key? key,
|
||||
this.initialCount = 0,
|
||||
required this.onCountChanged,
|
||||
}) : super(key: key);
|
||||
const CounterWidget(
|
||||
{Key? key,
|
||||
this.initialCount = 0,
|
||||
required this.onCountChanged,
|
||||
required this.isCreate})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<CounterWidget> createState() => _CounterWidgetState();
|
||||
@ -53,25 +55,26 @@ class _CounterWidgetState extends State<CounterWidget> {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildCounterButton(Icons.remove, _decrementCounter),
|
||||
_buildCounterButton(Icons.remove, _decrementCounter,!widget.isCreate ),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: ColorsManager.spaceColor),
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildCounterButton(Icons.add, _incrementCounter),
|
||||
_buildCounterButton(Icons.add, _incrementCounter, false),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterButton(IconData icon, VoidCallback onPressed) {
|
||||
Widget _buildCounterButton(IconData icon, VoidCallback onPressed, bool isDisabled) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
onTap: isDisabled? null: onPressed,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor,
|
||||
color: isDisabled? ColorsManager.spaceColor.withOpacity(0.3): ColorsManager.spaceColor,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
|
@ -2,19 +2,23 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/constants/space_icon_const.dart';
|
||||
|
||||
class CreateSpaceDialog extends StatefulWidget {
|
||||
final Function(String, String, List<SelectedProduct> selectedProducts)
|
||||
onCreateSpace;
|
||||
final Function(String, String, List<SelectedProduct> selectedProducts,
|
||||
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) onCreateSpace;
|
||||
final List<ProductModel>? products;
|
||||
final String? name;
|
||||
final String? icon;
|
||||
@ -22,6 +26,9 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final SpaceModel? parentSpace;
|
||||
final SpaceModel? editSpace;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? tags;
|
||||
|
||||
const CreateSpaceDialog(
|
||||
{super.key,
|
||||
@ -32,7 +39,10 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
this.icon,
|
||||
this.isEdit = false,
|
||||
this.editSpace,
|
||||
this.selectedProducts = const []});
|
||||
this.selectedProducts = const [],
|
||||
this.spaceModels,
|
||||
this.subspaces,
|
||||
this.tags});
|
||||
|
||||
@override
|
||||
CreateSpaceDialogState createState() => CreateSpaceDialogState();
|
||||
@ -40,12 +50,15 @@ class CreateSpaceDialog extends StatefulWidget {
|
||||
|
||||
class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
String selectedIcon = Assets.location;
|
||||
SpaceTemplateModel? selectedSpaceModel;
|
||||
String enteredName = '';
|
||||
List<SelectedProduct> selectedProducts = [];
|
||||
late TextEditingController nameController;
|
||||
bool isOkButtonEnabled = false;
|
||||
bool isNameFieldInvalid = false;
|
||||
bool isNameFieldExist = false;
|
||||
List<SubspaceModel>? subspaces;
|
||||
List<Tag>? tags;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,196 +71,485 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
enteredName.isNotEmpty || nameController.text.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return AlertDialog(
|
||||
title: widget.isEdit
|
||||
? const Text('Edit Space')
|
||||
: const Text('Create New Space'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.5, // Limit dialog width
|
||||
width: screenWidth * 0.5,
|
||||
child: SingleChildScrollView(
|
||||
// Scrollable content to prevent overflow
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: screenWidth * 0.1, // Adjusted width
|
||||
height: screenWidth * 0.1, // Adjusted height
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SvgPicture.asset(
|
||||
selectedIcon,
|
||||
width: screenWidth * 0.04,
|
||||
height: screenWidth * 0.04,
|
||||
),
|
||||
Positioned(
|
||||
top: 6,
|
||||
right: 6,
|
||||
child: InkWell(
|
||||
onTap: _showIconSelectionDialog,
|
||||
child: Container(
|
||||
width: screenWidth * 0.020,
|
||||
height: screenWidth * 0.020,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: screenWidth * 0.06,
|
||||
height: screenWidth * 0.06,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
// Ensure the text field expands responsively
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
onChanged: (value) {
|
||||
enteredName = value.trim();
|
||||
setState(() {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = false;
|
||||
isNameFieldInvalid = value.isEmpty;
|
||||
|
||||
if (!isNameFieldInvalid) {
|
||||
if (_isNameConflict(value)) {
|
||||
isNameFieldExist = true;
|
||||
isOkButtonEnabled = false;
|
||||
} else {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: isNameFieldInvalid || isNameFieldExist
|
||||
? ColorsManager.red
|
||||
: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
Container(
|
||||
width: screenWidth * 0.1,
|
||||
height: screenWidth * 0.1,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
SvgPicture.asset(
|
||||
selectedIcon,
|
||||
width: screenWidth * 0.04,
|
||||
height: screenWidth * 0.04,
|
||||
),
|
||||
Positioned(
|
||||
top: 20,
|
||||
right: 20,
|
||||
child: InkWell(
|
||||
onTap: _showIconSelectionDialog,
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNameFieldInvalid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Space name should not be empty.',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
if (isNameFieldExist)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Name already exist',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (selectedProducts.isNotEmpty)
|
||||
_buildSelectedProductsButtons(widget.products ?? [])
|
||||
else
|
||||
DefaultButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: widget.products,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
setState(() {
|
||||
selectedProducts = selectedProductsMap;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: Colors.black,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
borderRadius: 16.0,
|
||||
padding: 10.0, // Reduced padding for smaller size
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.addIcon,
|
||||
width: screenWidth *
|
||||
0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Add devices / Assign a space model',
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent overflow
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: nameController,
|
||||
onChanged: (value) {
|
||||
enteredName = value.trim();
|
||||
setState(() {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = false;
|
||||
isNameFieldInvalid = value.isEmpty;
|
||||
|
||||
if (!isNameFieldInvalid) {
|
||||
if (_isNameConflict(value)) {
|
||||
isNameFieldExist = true;
|
||||
isOkButtonEnabled = false;
|
||||
} else {
|
||||
isNameFieldExist = false;
|
||||
isOkButtonEnabled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(
|
||||
color: isNameFieldInvalid || isNameFieldExist
|
||||
? ColorsManager.red
|
||||
: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: const BorderSide(
|
||||
color: ColorsManager.boxColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isNameFieldInvalid)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Space name should not be empty.',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
if (isNameFieldExist)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'*Name already exist',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.red),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
selectedSpaceModel == null
|
||||
? DefaultButton(
|
||||
onPressed: () {
|
||||
_showLinkSpaceModelDialog(context);
|
||||
},
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: Colors.black,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
borderRadius: 16.0,
|
||||
padding: 10.0, // Reduced padding for smaller size
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.link,
|
||||
width: screenWidth *
|
||||
0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Link a space model',
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent overflow
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
Chip(
|
||||
label: Text(
|
||||
selectedSpaceModel?.modelName ?? '',
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => setState(() {
|
||||
this.selectedSpaceModel = null;
|
||||
})),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
const Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: ColorsManager.neutralGray,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(
|
||||
'OR',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Divider(
|
||||
color: ColorsManager.neutralGray,
|
||||
thickness: 1.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
subspaces == null
|
||||
? DefaultButton(
|
||||
onPressed: () {
|
||||
_showSubSpaceDialog(context, enteredName, [],
|
||||
false, widget.products, subspaces);
|
||||
},
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: Colors.black,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
borderRadius: 16.0,
|
||||
padding: 10.0, // Reduced padding for smaller size
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.addIcon,
|
||||
width: screenWidth *
|
||||
0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Create sub space',
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent overflow
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: screenWidth * 0.35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
if (subspaces != null)
|
||||
...subspaces!.map(
|
||||
(subspace) => Chip(
|
||||
label: Text(
|
||||
subspace.subspaceName,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager
|
||||
.spaceColor), // Text color
|
||||
),
|
||||
backgroundColor: ColorsManager
|
||||
.whiteColors, // Chip background color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
16), // Rounded chip
|
||||
side: const BorderSide(
|
||||
color: ColorsManager
|
||||
.spaceColor), // Border color
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
_showSubSpaceDialog(
|
||||
context,
|
||||
enteredName,
|
||||
[],
|
||||
false,
|
||||
widget.products,
|
||||
subspaces);
|
||||
},
|
||||
child: Chip(
|
||||
label: const Text(
|
||||
'Edit',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor:
|
||||
ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
(tags?.isNotEmpty == true ||
|
||||
subspaces?.any((subspace) =>
|
||||
subspace.tags?.isNotEmpty == true) ==
|
||||
true)
|
||||
? SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
..._groupTags([
|
||||
...?tags,
|
||||
...?subspaces?.expand(
|
||||
(subspace) => subspace.tags ?? [])
|
||||
]).entries.map(
|
||||
(entry) => Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
entry.key.icon ??
|
||||
'assets/icons/gateway.svg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x${entry.value}', // Show count
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor:
|
||||
ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
_showTagCreateDialog(context, enteredName,
|
||||
widget.products);
|
||||
// Edit action
|
||||
},
|
||||
child: Chip(
|
||||
label: const Text(
|
||||
'Edit',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor:
|
||||
ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: DefaultButton(
|
||||
onPressed: () {
|
||||
_showTagCreateDialog(
|
||||
context, enteredName, widget.products);
|
||||
},
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
foregroundColor: Colors.black,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
borderRadius: 16.0,
|
||||
padding: 10.0, // Reduced padding for smaller size
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
Assets.addIcon,
|
||||
width: screenWidth *
|
||||
0.015, // Adjust icon size
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Add devices',
|
||||
overflow: TextOverflow
|
||||
.ellipsis, // Prevent overflow
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -277,8 +579,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
? enteredName
|
||||
: (widget.name ?? '');
|
||||
if (newName.isNotEmpty) {
|
||||
widget.onCreateSpace(
|
||||
newName, selectedIcon, selectedProducts);
|
||||
widget.onCreateSpace(newName, selectedIcon,
|
||||
selectedProducts, selectedSpaceModel,subspaces,tags);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@ -313,74 +615,6 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedProductsButtons(List<ProductModel> products) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
width: screenWidth * 0.6,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (var i = 0; i < selectedProducts.length; i++) ...[
|
||||
HoverableButton(
|
||||
iconPath:
|
||||
_mapIconToProduct(selectedProducts[i].productId, products),
|
||||
text: 'x${selectedProducts[i].count}',
|
||||
onTap: () {
|
||||
setState(() {
|
||||
selectedProducts.remove(selectedProducts[i]);
|
||||
});
|
||||
// Handle button tap
|
||||
},
|
||||
),
|
||||
if (i < selectedProducts.length - 1)
|
||||
const SizedBox(
|
||||
width: 2), // Add space except after the last button
|
||||
],
|
||||
const SizedBox(width: 2),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: widget.products,
|
||||
initialSelectedProducts: selectedProducts,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
setState(() {
|
||||
selectedProducts = selectedProductsMap;
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.spaceColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isNameConflict(String value) {
|
||||
return (widget.parentSpace?.children.any((child) => child.name == value) ??
|
||||
false) ||
|
||||
@ -390,20 +624,133 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
|
||||
false);
|
||||
}
|
||||
|
||||
String _mapIconToProduct(String uuid, List<ProductModel> products) {
|
||||
// Find the product with the matching UUID
|
||||
final product = products.firstWhere(
|
||||
(product) => product.uuid == uuid,
|
||||
orElse: () => ProductModel(
|
||||
uuid: '',
|
||||
catName: '',
|
||||
prodId: '',
|
||||
prodType: '',
|
||||
name: '',
|
||||
icon: Assets.presenceSensor,
|
||||
),
|
||||
void _showLinkSpaceModelDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return LinkSpaceModelDialog(
|
||||
spaceModels: widget.spaceModels ?? [],
|
||||
onSave: (selectedModel) {
|
||||
if (selectedModel != null) {
|
||||
setState(() {
|
||||
selectedSpaceModel = selectedModel;
|
||||
subspaces = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return product.icon ?? Assets.presenceSensor;
|
||||
void _showSubSpaceDialog(
|
||||
BuildContext context,
|
||||
String name,
|
||||
final List<Tag>? spaceTags,
|
||||
bool isEdit,
|
||||
List<ProductModel>? products,
|
||||
final List<SubspaceModel>? existingSubSpaces) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSubSpaceDialog(
|
||||
spaceName: name,
|
||||
dialogTitle: 'Create Sub-space',
|
||||
spaceTags: spaceTags,
|
||||
isEdit: isEdit,
|
||||
products: products,
|
||||
existingSubSpaces: existingSubSpaces,
|
||||
onSave: (slectedSubspaces) {
|
||||
if (slectedSubspaces != null) {
|
||||
setState(() {
|
||||
subspaces = slectedSubspaces;
|
||||
selectedSpaceModel = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showTagCreateDialog(
|
||||
BuildContext context, String name, List<ProductModel>? products) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AddDeviceTypeWidget(
|
||||
spaceName: name,
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
spaceTags: tags,
|
||||
allTags: [],
|
||||
initialSelectedProducts:
|
||||
createInitialSelectedProducts(tags, subspaces),
|
||||
onSave: (selectedSpaceTags, selectedSubspaces) {
|
||||
setState(() {
|
||||
tags = selectedSpaceTags;
|
||||
selectedSpaceModel = null;
|
||||
|
||||
if (selectedSubspaces != null) {
|
||||
if (subspaces != null) {
|
||||
for (final subspace in subspaces!) {
|
||||
for (final selectedSubspace in selectedSubspaces) {
|
||||
if (subspace.subspaceName ==
|
||||
selectedSubspace.subspaceName) {
|
||||
subspace.tags = selectedSubspace.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<SelectedProduct> createInitialSelectedProducts(
|
||||
List<Tag>? tags, List<SubspaceModel>? subspaces) {
|
||||
final Map<ProductModel, int> productCounts = {};
|
||||
|
||||
if (tags != null) {
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] =
|
||||
(productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return productCounts.entries
|
||||
.map((entry) => SelectedProduct(
|
||||
productId: entry.key.uuid,
|
||||
count: entry.value,
|
||||
productName: entry.key.name ?? 'Unnamed',
|
||||
product: entry.key,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Map<ProductModel, int> _groupTags(List<Tag> tags) {
|
||||
final Map<ProductModel, int> groupedTags = {};
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
groupedTags[tag.product!] = (groupedTags[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return groupedTags;
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ void showDeleteConfirmationDialog(BuildContext context, VoidCallback onConfirm,
|
||||
Navigator.of(context).pop(); // Close the first dialog
|
||||
showProcessingPopup(context, isSpace, onConfirm);
|
||||
},
|
||||
style: _dialogButtonStyle(Colors.blue),
|
||||
child: const Text('Continue', style: TextStyle(color: Colors.white)),
|
||||
style: _dialogButtonStyle(ColorsManager.spaceColor),
|
||||
child: const Text('Continue', style: TextStyle(color: ColorsManager.whiteColors)),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -83,7 +83,7 @@ void showProcessingPopup(BuildContext context, bool isSpace, VoidCallback onDele
|
||||
ElevatedButton(
|
||||
onPressed: onDelete,
|
||||
style: _dialogButtonStyle(ColorsManager.warningRed),
|
||||
child: const Text('Delete', style: TextStyle(color: Colors.white)),
|
||||
child: const Text('Delete', style: TextStyle(color: ColorsManager.whiteColors)),
|
||||
),
|
||||
CancelButton(
|
||||
label: 'Cancel',
|
||||
@ -108,7 +108,7 @@ Widget _buildWarningIcon() {
|
||||
color: ColorsManager.warningRed,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.close, color: Colors.white, size: 40),
|
||||
child: const Icon(Icons.close, color: ColorsManager.whiteColors, size: 40),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ class IconSelectionDialog extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2), // Shadow color
|
||||
color: ColorsManager.blackColor.withOpacity(0.2), // Shadow color
|
||||
blurRadius: 20, // Spread of the blur
|
||||
offset: const Offset(0, 8), // Offset of the shadow
|
||||
),
|
||||
|
@ -1,16 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/gradient_canvas_border_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/sidebar_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/view/space_model_page.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
|
||||
class LoadedSpaceView extends StatefulWidget {
|
||||
class LoadedSpaceView extends StatelessWidget {
|
||||
final List<CommunityModel> communities;
|
||||
final CommunityModel? selectedCommunity;
|
||||
final SpaceModel? selectedSpace;
|
||||
final List<ProductModel>? products;
|
||||
final List<SpaceTemplateModel>? spaceModels;
|
||||
final bool shouldNavigateToSpaceModelPage;
|
||||
|
||||
const LoadedSpaceView({
|
||||
super.key,
|
||||
@ -18,33 +25,43 @@ class LoadedSpaceView extends StatefulWidget {
|
||||
this.selectedCommunity,
|
||||
this.selectedSpace,
|
||||
this.products,
|
||||
this.spaceModels,
|
||||
required this.shouldNavigateToSpaceModelPage
|
||||
});
|
||||
|
||||
@override
|
||||
_LoadedStateViewState createState() => _LoadedStateViewState();
|
||||
}
|
||||
|
||||
class _LoadedStateViewState extends State<LoadedSpaceView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SidebarWidget(
|
||||
communities: widget.communities,
|
||||
selectedSpaceUuid: widget.selectedSpace?.uuid ??
|
||||
widget.selectedCommunity?.uuid ??
|
||||
'',
|
||||
),
|
||||
CommunityStructureArea(
|
||||
selectedCommunity: widget.selectedCommunity,
|
||||
selectedSpace: widget.selectedSpace,
|
||||
spaces: widget.selectedCommunity?.spaces ?? [],
|
||||
products: widget.products,
|
||||
communities: widget.communities,
|
||||
communities: communities,
|
||||
selectedSpaceUuid:
|
||||
selectedSpace?.uuid ?? selectedCommunity?.uuid ?? '',
|
||||
),
|
||||
shouldNavigateToSpaceModelPage
|
||||
? Expanded(
|
||||
child: BlocProvider(
|
||||
create: (context) => SpaceModelBloc(
|
||||
api: SpaceModelManagementApi(),
|
||||
initialSpaceModels: spaceModels ?? [],
|
||||
),
|
||||
child: SpaceModelPage(
|
||||
products: products,
|
||||
),
|
||||
),
|
||||
)
|
||||
: CommunityStructureArea(
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
spaces: selectedCommunity?.spaces ?? [],
|
||||
products: products,
|
||||
communities: communities,
|
||||
spaceModels: spaceModels,
|
||||
),
|
||||
],
|
||||
),
|
||||
const GradientCanvasBorderWidget(),
|
||||
|
@ -45,7 +45,7 @@ class PlusButtonWidget extends StatelessWidget {
|
||||
color: ColorsManager.spaceColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.add, color: Colors.white, size: 20),
|
||||
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/hoverable_button.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class SelectedProductsButtons extends StatelessWidget {
|
||||
final List<ProductModel> products;
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final Function(List<SelectedProduct>) onProductsUpdated;
|
||||
final BuildContext context;
|
||||
|
||||
const SelectedProductsButtons({
|
||||
Key? key,
|
||||
required this.products,
|
||||
required this.selectedProducts,
|
||||
required this.onProductsUpdated,
|
||||
required this.context,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Container(
|
||||
width: screenWidth * 0.6,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
..._buildSelectedProductButtons(),
|
||||
_buildAddButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildSelectedProductButtons() {
|
||||
return [
|
||||
for (var i = 0; i < selectedProducts.length; i++) ...[
|
||||
HoverableButton(
|
||||
iconPath: _mapIconToProduct(selectedProducts[i].productId, products),
|
||||
text: 'x${selectedProducts[i].count}',
|
||||
onTap: () {
|
||||
_removeProduct(i);
|
||||
},
|
||||
),
|
||||
if (i < selectedProducts.length - 1)
|
||||
const SizedBox(width: 2), // Add space except after the last button
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildAddButton() {
|
||||
return GestureDetector(
|
||||
onTap: _showAddDeviceDialog,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.spaceColor,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddDeviceDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AddDeviceWidget(
|
||||
products: products,
|
||||
initialSelectedProducts: selectedProducts,
|
||||
onProductsSelected: (selectedProductsMap) {
|
||||
onProductsUpdated(selectedProductsMap);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _removeProduct(int index) {
|
||||
final updatedProducts = [...selectedProducts];
|
||||
updatedProducts.removeAt(index);
|
||||
onProductsUpdated(updatedProducts);
|
||||
}
|
||||
|
||||
String _mapIconToProduct(String uuid, List<ProductModel> products) {
|
||||
// Find the product with the matching UUID
|
||||
final product = products.firstWhere(
|
||||
(product) => product.uuid == uuid,
|
||||
orElse: () => ProductModel(
|
||||
uuid: '',
|
||||
catName: '',
|
||||
prodId: '',
|
||||
prodType: '',
|
||||
name: '',
|
||||
icon: Assets.presenceSensor,
|
||||
),
|
||||
);
|
||||
|
||||
return product.icon ?? Assets.presenceSensor;
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_m
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/community_tile.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
@ -116,7 +118,7 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
||||
children: [
|
||||
Text('Communities',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
color: ColorsManager.blackColor,
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToBlank(context),
|
||||
@ -184,6 +186,8 @@ class _SidebarWidgetState extends State<SidebarWidget> {
|
||||
_selectedSpaceUuid = null; // Update the selected community
|
||||
});
|
||||
|
||||
context.read<CenterBodyBloc>().add(CommunitySelectedEvent());
|
||||
|
||||
context.read<SpaceManagementBloc>().add(
|
||||
SelectCommunityEvent(selectedCommunity: community),
|
||||
);
|
||||
|
@ -78,7 +78,7 @@ class SpaceContainerWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: ColorsManager.lightGrayColor.withOpacity(0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3), // Shadow position
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceWidget extends StatelessWidget {
|
||||
final String name;
|
||||
@ -23,11 +24,11 @@ class SpaceWidget extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
color: ColorsManager.lightGrayColor.withOpacity(0.5),
|
||||
spreadRadius: 5,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
@ -36,7 +37,7 @@ class SpaceWidget extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, color: Colors.blue),
|
||||
const Icon(Icons.location_on, color: ColorsManager.spaceColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(name, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
|
148
lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart
Normal file
148
lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart
Normal file
@ -0,0 +1,148 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
|
||||
|
||||
class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
|
||||
AssignTagBloc() : super(AssignTagInitial()) {
|
||||
on<InitializeTags>((event, emit) {
|
||||
final initialTags = event.initialTags ?? [];
|
||||
|
||||
final existingTagCounts = <String, int>{};
|
||||
for (var tag in initialTags) {
|
||||
if (tag.product != null) {
|
||||
existingTagCounts[tag.product!.uuid] =
|
||||
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final allTags = <Tag>[];
|
||||
|
||||
for (var selectedProduct in event.addedProducts) {
|
||||
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
|
||||
|
||||
if (selectedProduct.count == 0 ||
|
||||
selectedProduct.count <= existingCount) {
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
continue;
|
||||
}
|
||||
|
||||
final missingCount = selectedProduct.count - existingCount;
|
||||
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
|
||||
if (missingCount > 0) {
|
||||
allTags.addAll(List.generate(
|
||||
missingCount,
|
||||
(index) => Tag(
|
||||
tag: '',
|
||||
product: selectedProduct.product,
|
||||
location: 'Main Space',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: allTags,
|
||||
isSaveEnabled: _validateTags(allTags),
|
||||
));
|
||||
});
|
||||
|
||||
on<UpdateTagEvent>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
tags[event.index].tag = event.tag;
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateLocation>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
|
||||
// Use copyWith for immutability
|
||||
tags[event.index] =
|
||||
tags[event.index].copyWith(location: event.location);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<ValidateTags>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final tags = List<Tag>.from(currentState.tags);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<DeleteTag>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
|
||||
final updatedTags = List<Tag>.from(currentState.tags)
|
||||
..remove(event.tagToDelete);
|
||||
|
||||
emit(AssignTagLoaded(
|
||||
tags: updatedTags,
|
||||
isSaveEnabled: _validateTags(updatedTags),
|
||||
));
|
||||
} else {
|
||||
emit(const AssignTagLoaded(
|
||||
tags: [],
|
||||
isSaveEnabled: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _validateTags(List<Tag> tags) {
|
||||
if (tags.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
return uniqueTags.length == tags.length && !hasEmptyTag;
|
||||
}
|
||||
|
||||
String? _getValidationError(List<Tag> tags) {
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
if (hasEmptyTag) return 'Tags cannot be empty.';
|
||||
final duplicateTags = tags
|
||||
.map((tag) => tag.tag?.trim() ?? '')
|
||||
.fold<Map<String, int>>({}, (map, tag) {
|
||||
map[tag] = (map[tag] ?? 0) + 1;
|
||||
return map;
|
||||
})
|
||||
.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (duplicateTags.isNotEmpty) {
|
||||
return 'Duplicate tags found: ${duplicateTags.join(', ')}';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
abstract class AssignTagEvent extends Equatable {
|
||||
const AssignTagEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitializeTags extends AssignTagEvent {
|
||||
final List<Tag>? initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeTags({
|
||||
required this.initialTags,
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags ?? [], addedProducts];
|
||||
}
|
||||
|
||||
class UpdateTagEvent extends AssignTagEvent {
|
||||
final int index;
|
||||
final String tag;
|
||||
|
||||
const UpdateTagEvent({required this.index, required this.tag});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, tag];
|
||||
}
|
||||
|
||||
class UpdateLocation extends AssignTagEvent {
|
||||
final int index;
|
||||
final String location;
|
||||
|
||||
const UpdateLocation({required this.index, required this.location});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, location];
|
||||
}
|
||||
|
||||
class ValidateTags extends AssignTagEvent {}
|
||||
|
||||
class DeleteTag extends AssignTagEvent {
|
||||
final Tag tagToDelete;
|
||||
final List<Tag> tags;
|
||||
|
||||
const DeleteTag({required this.tagToDelete, required this.tags});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tagToDelete, tags];
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AssignTagState extends Equatable {
|
||||
const AssignTagState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AssignTagInitial extends AssignTagState {}
|
||||
|
||||
class AssignTagLoading extends AssignTagState {}
|
||||
|
||||
class AssignTagLoaded extends AssignTagState {
|
||||
final List<Tag> tags;
|
||||
final bool isSaveEnabled;
|
||||
final String? errorMessage;
|
||||
|
||||
const AssignTagLoaded({
|
||||
required this.tags,
|
||||
required this.isSaveEnabled,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tags, isSaveEnabled];
|
||||
}
|
||||
|
||||
class AssignTagError extends AssignTagState {
|
||||
final String errorMessage;
|
||||
|
||||
const AssignTagError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AssignTagDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceModel>? subspaces;
|
||||
final List<Tag>? initialTags;
|
||||
final ValueChanged<List<Tag>>? onTagsAssigned;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final String title;
|
||||
final Function(List<Tag>, List<SubspaceModel>?)? onSave;
|
||||
|
||||
const AssignTagDialog(
|
||||
{Key? key,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.addedProducts,
|
||||
this.initialTags,
|
||||
this.onTagsAssigned,
|
||||
this.allTags,
|
||||
required this.spaceName,
|
||||
required this.title,
|
||||
this.onSave})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> locations =
|
||||
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList();
|
||||
return BlocProvider(
|
||||
create: (_) => AssignTagBloc()
|
||||
..add(InitializeTags(
|
||||
initialTags: initialTags,
|
||||
addedProducts: addedProducts,
|
||||
)),
|
||||
child: BlocBuilder<AssignTagBloc, AssignTagState>(
|
||||
builder: (context, state) {
|
||||
if (state is AssignTagLoaded) {
|
||||
final controllers = List.generate(
|
||||
state.tags.length,
|
||||
(index) => TextEditingController(text: state.tags[index].tag),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text('#',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Device',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Tag',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Location',
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium)),
|
||||
],
|
||||
rows: state.tags.isEmpty
|
||||
? [
|
||||
const DataRow(cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'No Data Available',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(SizedBox()),
|
||||
DataCell(SizedBox()),
|
||||
DataCell(SizedBox()),
|
||||
])
|
||||
]
|
||||
: List.generate(state.tags.length, (index) {
|
||||
final tag = state.tags[index];
|
||||
final controller = controllers[index];
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text(index.toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tag.product?.name ?? 'Unknown',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close,
|
||||
color: ColorsManager.warningRed,
|
||||
size: 16),
|
||||
onPressed: () {
|
||||
context.read<AssignTagBloc>().add(
|
||||
DeleteTag(
|
||||
tagToDelete: tag,
|
||||
tags: state.tags));
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(UpdateTagEvent(
|
||||
index: index,
|
||||
tag: value.trim(),
|
||||
));
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter Tag',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context)
|
||||
.size
|
||||
.width *
|
||||
0.15,
|
||||
child: PopupMenuButton<String>(
|
||||
color: ColorsManager.whiteColors,
|
||||
icon: const Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color:
|
||||
ColorsManager.blackColor),
|
||||
onSelected: (value) {
|
||||
controller.text = value;
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(UpdateTagEvent(
|
||||
index: index,
|
||||
tag: value,
|
||||
));
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
return (allTags ?? [])
|
||||
.where((tagValue) => !state
|
||||
.tags
|
||||
.map((e) => e.tag)
|
||||
.contains(tagValue))
|
||||
.map((tagValue) {
|
||||
return PopupMenuItem<String>(
|
||||
textStyle: const TextStyle(
|
||||
color: ColorsManager
|
||||
.textPrimaryColor),
|
||||
value: tagValue,
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(
|
||||
minWidth: MediaQuery.of(
|
||||
context)
|
||||
.size
|
||||
.width *
|
||||
0.15,
|
||||
maxWidth: MediaQuery.of(
|
||||
context)
|
||||
.size
|
||||
.width *
|
||||
0.15,
|
||||
),
|
||||
child: Text(
|
||||
tagValue,
|
||||
overflow: TextOverflow
|
||||
.ellipsis,
|
||||
),
|
||||
));
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton<String>(
|
||||
value: tag.location ?? 'Main',
|
||||
dropdownColor: ColorsManager
|
||||
.whiteColors, // Dropdown background
|
||||
style: const TextStyle(
|
||||
color: Colors
|
||||
.black), // Style for selected text
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'Main Space',
|
||||
child: Text(
|
||||
'Main Space',
|
||||
style: TextStyle(
|
||||
color: ColorsManager
|
||||
.textPrimaryColor),
|
||||
),
|
||||
),
|
||||
...locations.map((location) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: location,
|
||||
child: Text(
|
||||
location,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager
|
||||
.textPrimaryColor),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
context
|
||||
.read<AssignTagBloc>()
|
||||
.add(UpdateLocation(
|
||||
index: index,
|
||||
location: value,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (state.errorMessage != null)
|
||||
Text(
|
||||
state.errorMessage!,
|
||||
style: const TextStyle(color: ColorsManager.warningRed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
borderRadius: 10,
|
||||
backgroundColor: state.isSaveEnabled
|
||||
? ColorsManager.secondaryColor
|
||||
: ColorsManager.grayColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
onPressed: state.isSaveEnabled
|
||||
? () async {
|
||||
Navigator.of(context).pop();
|
||||
final assignedTags = <Tag>{};
|
||||
for (var tag in state.tags) {
|
||||
if (tag.location == null ||
|
||||
subspaces == null) {
|
||||
continue;
|
||||
}
|
||||
for (var subspace in subspaces!) {
|
||||
if (tag.location == subspace.subspaceName) {
|
||||
subspace.tags ??= [];
|
||||
subspace.tags!.add(tag);
|
||||
assignedTags.add(tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onSave!(state.tags,subspaces);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is AssignTagLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return const Center(child: Text('Something went wrong.'));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
class AssignTagModelBloc
|
||||
extends Bloc<AssignTagModelEvent, AssignTagModelState> {
|
||||
AssignTagModelBloc() : super(AssignTagModelInitial()) {
|
||||
on<InitializeTagModels>((event, emit) {
|
||||
final initialTags = event.initialTags ?? [];
|
||||
|
||||
final existingTagCounts = <String, int>{};
|
||||
for (var tag in initialTags) {
|
||||
if (tag.product != null) {
|
||||
existingTagCounts[tag.product!.uuid] =
|
||||
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
final allTags = <TagModel>[];
|
||||
|
||||
for (var selectedProduct in event.addedProducts) {
|
||||
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
|
||||
|
||||
if (selectedProduct.count == 0 ||
|
||||
selectedProduct.count <= existingCount) {
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
continue;
|
||||
}
|
||||
|
||||
final missingCount = selectedProduct.count - existingCount;
|
||||
|
||||
allTags.addAll(initialTags
|
||||
.where((tag) => tag.product?.uuid == selectedProduct.productId));
|
||||
|
||||
if (missingCount > 0) {
|
||||
allTags.addAll(List.generate(
|
||||
missingCount,
|
||||
(index) => TagModel(
|
||||
tag: '',
|
||||
product: selectedProduct.product,
|
||||
location: 'Main Space',
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: allTags,
|
||||
isSaveEnabled: _validateTags(allTags),
|
||||
errorMessage: ''));
|
||||
});
|
||||
|
||||
on<UpdateTag>((event, emit) {
|
||||
final currentState = state;
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
tags[event.index].tag = event.tag;
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateLocation>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
|
||||
// Use copyWith for immutability
|
||||
tags[event.index] =
|
||||
tags[event.index].copyWith(location: event.location);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<ValidateTagModels>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final tags = List<TagModel>.from(currentState.tags);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: tags,
|
||||
isSaveEnabled: _validateTags(tags),
|
||||
errorMessage: _getValidationError(tags),
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
on<DeleteTagModel>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AssignTagModelLoaded &&
|
||||
currentState.tags.isNotEmpty) {
|
||||
final updatedTags = List<TagModel>.from(currentState.tags)
|
||||
..remove(event.tagToDelete);
|
||||
|
||||
emit(AssignTagModelLoaded(
|
||||
tags: updatedTags,
|
||||
isSaveEnabled: _validateTags(updatedTags),
|
||||
));
|
||||
} else {
|
||||
emit(const AssignTagModelLoaded(
|
||||
tags: [],
|
||||
isSaveEnabled: false,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _validateTags(List<TagModel> tags) {
|
||||
|
||||
final uniqueTags = tags.map((tag) => tag.tag?.trim() ?? '').toSet();
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
String? _getValidationError(List<TagModel> tags) {
|
||||
final hasEmptyTag = tags.any((tag) => (tag.tag?.trim() ?? '').isEmpty);
|
||||
if (hasEmptyTag) {
|
||||
return 'Tags cannot be empty.';
|
||||
}
|
||||
|
||||
// Check for duplicate tags
|
||||
final duplicateTags = tags
|
||||
.map((tag) => tag.tag?.trim() ?? '')
|
||||
.fold<Map<String, int>>({}, (map, tag) {
|
||||
map[tag] = (map[tag] ?? 0) + 1;
|
||||
return map;
|
||||
})
|
||||
.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (duplicateTags.isNotEmpty) {
|
||||
return 'Duplicate tags found: ${duplicateTags.join(', ')}';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
|
||||
abstract class AssignTagModelEvent extends Equatable {
|
||||
const AssignTagModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitializeTagModels extends AssignTagModelEvent {
|
||||
final List<TagModel> initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeTagModels({
|
||||
this.initialTags = const [],
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags, addedProducts];
|
||||
}
|
||||
|
||||
class UpdateTag extends AssignTagModelEvent {
|
||||
final int index;
|
||||
final String tag;
|
||||
|
||||
const UpdateTag({required this.index, required this.tag});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, tag];
|
||||
}
|
||||
|
||||
class UpdateLocation extends AssignTagModelEvent {
|
||||
final int index;
|
||||
final String location;
|
||||
|
||||
const UpdateLocation({required this.index, required this.location});
|
||||
|
||||
@override
|
||||
List<Object> get props => [index, location];
|
||||
}
|
||||
|
||||
class ValidateTagModels extends AssignTagModelEvent {}
|
||||
|
||||
class DeleteTagModel extends AssignTagModelEvent {
|
||||
final TagModel tagToDelete;
|
||||
final List<TagModel> tags;
|
||||
|
||||
const DeleteTagModel({required this.tagToDelete, required this.tags});
|
||||
|
||||
@override
|
||||
List<Object> get props => [tagToDelete, tags];
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AssignTagModelState extends Equatable {
|
||||
const AssignTagModelState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AssignTagModelInitial extends AssignTagModelState {}
|
||||
|
||||
class AssignTagModelLoading extends AssignTagModelState {}
|
||||
|
||||
class AssignTagModelLoaded extends AssignTagModelState {
|
||||
final List<TagModel> tags;
|
||||
final bool isSaveEnabled;
|
||||
final String? errorMessage;
|
||||
|
||||
const AssignTagModelLoaded({
|
||||
required this.tags,
|
||||
required this.isSaveEnabled,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tags, isSaveEnabled, errorMessage];
|
||||
}
|
||||
|
||||
class AssignTagModelError extends AssignTagModelState {
|
||||
final String errorMessage;
|
||||
|
||||
const AssignTagModelError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,433 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/dialog_dropdown.dart';
|
||||
import 'package:syncrow_web/common/dialog_textfield_dropdown.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/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AssignTagModelsDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceTemplateModel>? subspaces;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
|
||||
final List<TagModel> initialTags;
|
||||
final ValueChanged<List<TagModel>>? onTagsAssigned;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final String title;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
|
||||
const AssignTagModelsDialog(
|
||||
{Key? key,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.addedProducts,
|
||||
required this.initialTags,
|
||||
this.onTagsAssigned,
|
||||
this.allTags,
|
||||
required this.spaceName,
|
||||
required this.title,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels,
|
||||
this.spaceModel})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<String> locations = (subspaces ?? [])
|
||||
.map((subspace) => subspace.subspaceName)
|
||||
.toList()
|
||||
..add('Main Space');
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AssignTagModelBloc()
|
||||
..add(InitializeTagModels(
|
||||
initialTags: initialTags,
|
||||
addedProducts: addedProducts,
|
||||
)),
|
||||
child: BlocListener<AssignTagModelBloc, AssignTagModelState>(
|
||||
listener: (context, state) {},
|
||||
child: BlocBuilder<AssignTagModelBloc, AssignTagModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is AssignTagModelLoaded) {
|
||||
final controllers = List.generate(
|
||||
state.tags.length,
|
||||
(index) => TextEditingController(text: state.tags[index].tag),
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: Text('#',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Device',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
numeric: false,
|
||||
headingRowAlignment: MainAxisAlignment.start,
|
||||
label: Text('Tag',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
DataColumn(
|
||||
label: Text('Location',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium)),
|
||||
],
|
||||
rows: state.tags.isEmpty
|
||||
? [
|
||||
const DataRow(cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'No Data Available',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color:
|
||||
ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(SizedBox()),
|
||||
DataCell(SizedBox()),
|
||||
DataCell(SizedBox()),
|
||||
])
|
||||
]
|
||||
: List.generate(state.tags.length, (index) {
|
||||
final tag = state.tags[index];
|
||||
final controller = controllers[index];
|
||||
final availableTags = getAvailableTags(
|
||||
allTags ?? [], state.tags, tag);
|
||||
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Text((index + 1).toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tag.product?.name ?? 'Unknown',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager
|
||||
.lightGrayColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager
|
||||
.lightGreyColor,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(DeleteTagModel(
|
||||
tagToDelete: tag,
|
||||
tags: state.tags));
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints:
|
||||
const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
alignment: Alignment
|
||||
.centerLeft, // Align cell content to the left
|
||||
child: SizedBox(
|
||||
width: double
|
||||
.infinity, // Ensure full width for dropdown
|
||||
child: DialogTextfieldDropdown(
|
||||
items: availableTags,
|
||||
initialValue: tag.tag,
|
||||
onSelected: (value) {
|
||||
controller.text = value;
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(UpdateTag(
|
||||
index: index,
|
||||
tag: value,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DialogDropdown(
|
||||
items: locations,
|
||||
selectedValue:
|
||||
tag.location ?? 'Main Space',
|
||||
onSelected: (value) {
|
||||
context
|
||||
.read<
|
||||
AssignTagModelBloc>()
|
||||
.add(UpdateLocation(
|
||||
index: index,
|
||||
location: value,
|
||||
));
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (state.errorMessage != null)
|
||||
Text(
|
||||
state.errorMessage!,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.warningRed),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (buttonContext) => CancelButton(
|
||||
label: 'Add New Device',
|
||||
onPressed: () async {
|
||||
for (var tag in state.tags) {
|
||||
if (tag.location == null ||
|
||||
subspaces == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final previousTagSubspace =
|
||||
checkTagExistInSubspace(
|
||||
tag, subspaces ?? []);
|
||||
|
||||
if (tag.location == 'Main Space') {
|
||||
removeTagFromSubspace(
|
||||
tag, previousTagSubspace);
|
||||
} else if (tag.location !=
|
||||
previousTagSubspace?.subspaceName) {
|
||||
removeTagFromSubspace(
|
||||
tag, previousTagSubspace);
|
||||
moveToNewSubspace(tag, subspaces ?? []);
|
||||
state.tags.removeWhere(
|
||||
(t) => t.internalId == tag.internalId);
|
||||
} else {
|
||||
updateTagInSubspace(
|
||||
tag, previousTagSubspace);
|
||||
state.tags.removeWhere(
|
||||
(t) => t.internalId == tag.internalId);
|
||||
}
|
||||
}
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (dialogContext) =>
|
||||
AddDeviceTypeModelWidget(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
isCreate: false,
|
||||
initialSelectedProducts:
|
||||
addedProducts,
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
spaceTagModels: state.tags,
|
||||
pageContext: pageContext,
|
||||
spaceModel: SpaceTemplateModel(
|
||||
modelName: spaceName,
|
||||
tags: state.tags,
|
||||
uuid: spaceModel?.uuid,
|
||||
internalId:
|
||||
spaceModel?.internalId,
|
||||
subspaceModels: subspaces)),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
borderRadius: 10,
|
||||
backgroundColor: state.isSaveEnabled
|
||||
? ColorsManager.secondaryColor
|
||||
: ColorsManager.grayColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
onPressed: state.isSaveEnabled
|
||||
? () async {
|
||||
for (var tag in state.tags) {
|
||||
if (tag.location == null ||
|
||||
subspaces == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final previousTagSubspace =
|
||||
checkTagExistInSubspace(
|
||||
tag, subspaces ?? []);
|
||||
|
||||
if (tag.location == 'Main Space') {
|
||||
removeTagFromSubspace(
|
||||
tag, previousTagSubspace);
|
||||
} else if (tag.location !=
|
||||
previousTagSubspace?.subspaceName) {
|
||||
removeTagFromSubspace(
|
||||
tag, previousTagSubspace);
|
||||
moveToNewSubspace(tag, subspaces ?? []);
|
||||
state.tags.removeWhere((t) =>
|
||||
t.internalId == tag.internalId);
|
||||
} else {
|
||||
updateTagInSubspace(
|
||||
tag, previousTagSubspace);
|
||||
state.tags.removeWhere((t) =>
|
||||
t.internalId == tag.internalId);
|
||||
}
|
||||
}
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTags,
|
||||
pageContext: pageContext,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
spaceModel: SpaceTemplateModel(
|
||||
modelName: spaceName,
|
||||
tags: state.tags,
|
||||
uuid: spaceModel?.uuid,
|
||||
internalId:
|
||||
spaceModel?.internalId,
|
||||
subspaceModels: subspaces),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is AssignTagModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else {
|
||||
return const Center(child: Text('Something went wrong.'));
|
||||
}
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
List<String> getAvailableTags(
|
||||
List<String> allTags, List<TagModel> currentTags, TagModel currentTag) {
|
||||
return allTags
|
||||
.where((tagValue) => !currentTags
|
||||
.where((e) => e != currentTag) // Exclude the current row
|
||||
.map((e) => e.tag)
|
||||
.contains(tagValue))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void removeTagFromSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
|
||||
subspace?.tags?.removeWhere((t) => t.internalId == tag.internalId);
|
||||
}
|
||||
|
||||
SubspaceTemplateModel? checkTagExistInSubspace(
|
||||
TagModel tag, List<SubspaceTemplateModel>? subspaces) {
|
||||
if (subspaces == null) return null;
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags == null) return null;
|
||||
for (var t in subspace.tags!) {
|
||||
if (tag.internalId == t.internalId) return subspace;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void moveToNewSubspace(TagModel tag, List<SubspaceTemplateModel> subspaces) {
|
||||
final targetSubspace = subspaces
|
||||
.firstWhere((subspace) => subspace.subspaceName == tag.location);
|
||||
|
||||
targetSubspace.tags ??= [];
|
||||
if (targetSubspace.tags?.any((t) => t.internalId == tag.internalId) !=
|
||||
true) {
|
||||
targetSubspace.tags?.add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
void updateTagInSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
|
||||
final currentTag = subspace?.tags?.firstWhere(
|
||||
(t) => t.internalId == tag.internalId,
|
||||
);
|
||||
if (currentTag != null) {
|
||||
currentTag.tag = tag.tag;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
import 'subspace_event.dart';
|
||||
import 'subspace_state.dart';
|
||||
|
||||
class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
|
||||
SubSpaceBloc() : super(SubSpaceState([], [], '')) {
|
||||
on<AddSubSpace>((event, emit) {
|
||||
final existingNames =
|
||||
state.subSpaces.map((e) => e.subspaceName).toSet();
|
||||
|
||||
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
|
||||
emit(SubSpaceState(
|
||||
state.subSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'Subspace name already exists.',
|
||||
));
|
||||
} else {
|
||||
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'',
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle RemoveSubSpace Event
|
||||
on<RemoveSubSpace>((event, emit) {
|
||||
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
|
||||
..remove(event.subSpace);
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
if (event.subSpace.uuid?.isNotEmpty ?? false) {
|
||||
updatedSubspaceModels.add(UpdateSubspaceModel(
|
||||
action: Action.delete,
|
||||
uuid: event.subSpace.uuid!,
|
||||
));
|
||||
}
|
||||
|
||||
emit(SubSpaceState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
'', // Clear error message
|
||||
));
|
||||
});
|
||||
|
||||
// Handle UpdateSubSpace Event
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
|
||||
abstract class SubSpaceEvent {}
|
||||
|
||||
class AddSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel subSpace;
|
||||
AddSubSpace(this.subSpace);
|
||||
}
|
||||
|
||||
class RemoveSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel subSpace;
|
||||
RemoveSubSpace(this.subSpace);
|
||||
}
|
||||
|
||||
class UpdateSubSpace extends SubSpaceEvent {
|
||||
final SubspaceModel updatedSubSpace;
|
||||
UpdateSubSpace(this.updatedSubSpace);
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
|
||||
class SubSpaceState {
|
||||
final List<SubspaceModel> subSpaces;
|
||||
final List<UpdateSubspaceModel> updatedSubSpaceModels;
|
||||
final String errorMessage;
|
||||
|
||||
SubSpaceState(
|
||||
this.subSpaces,
|
||||
this.updatedSubSpaceModels,
|
||||
this.errorMessage,
|
||||
);
|
||||
|
||||
|
||||
SubSpaceState copyWith({
|
||||
List<SubspaceModel>? subSpaces,
|
||||
List<UpdateSubspaceModel>? updatedSubSpaceModels,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return SubSpaceState(
|
||||
subSpaces ?? this.subSpaces,
|
||||
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
|
||||
errorMessage ?? this.errorMessage,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSubSpaceDialog extends StatelessWidget {
|
||||
final bool isEdit;
|
||||
final String dialogTitle;
|
||||
final List<SubspaceModel>? existingSubSpaces;
|
||||
final String? spaceName;
|
||||
final List<Tag>? spaceTags;
|
||||
final List<ProductModel>? products;
|
||||
final Function(List<SubspaceModel>?)? onSave;
|
||||
|
||||
const CreateSubSpaceDialog(
|
||||
{Key? key,
|
||||
required this.isEdit,
|
||||
required this.dialogTitle,
|
||||
this.existingSubSpaces,
|
||||
required this.spaceName,
|
||||
required this.spaceTags,
|
||||
required this.products,
|
||||
required this.onSave})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final textController = TextEditingController();
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = SubSpaceBloc();
|
||||
if (existingSubSpaces != null) {
|
||||
for (var subSpace in existingSubSpaces!) {
|
||||
bloc.add(AddSubSpace(subSpace));
|
||||
}
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<SubSpaceBloc, SubSpaceState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: screenWidth * 0.35,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dialogTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...state.subSpaces.map(
|
||||
(subSpace) => Chip(
|
||||
label: Text(
|
||||
subSpace.subspaceName,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => context
|
||||
.read<SubSpaceBloc>()
|
||||
.add(RemoveSubSpace(subSpace)),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: state.subSpaces.isEmpty
|
||||
? 'Please enter the name'
|
||||
: null,
|
||||
hintStyle: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context.read<SubSpaceBloc>().add(
|
||||
AddSubSpace(SubspaceModel(
|
||||
subspaceName: value.trim(),
|
||||
disabled: false)));
|
||||
textController.clear();
|
||||
}
|
||||
},
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor),
|
||||
),
|
||||
),
|
||||
if (state.errorMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
state.errorMessage,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.warningRed,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: () async {
|
||||
final subSpaces = context
|
||||
.read<SubSpaceBloc>()
|
||||
.state
|
||||
.subSpaces;
|
||||
onSave!(subSpaces);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
|
||||
SubSpaceModelBloc() : super(SubSpaceModelState([], [], '', {})) {
|
||||
// Handle AddSubSpaceModel Event
|
||||
on<AddSubSpaceModel>((event, emit) {
|
||||
final existingNames =
|
||||
state.subSpaces.map((e) => e.subspaceName.toLowerCase()).toSet();
|
||||
|
||||
if (existingNames.contains(event.subSpace.subspaceName.toLowerCase())) {
|
||||
final updatedDuplicates = Set<String>.from(state.duplicates)
|
||||
..add(event.subSpace.subspaceName.toLowerCase());
|
||||
final updatedSubSpaces =
|
||||
List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'*Duplicated sub-space name',
|
||||
updatedDuplicates,
|
||||
));
|
||||
} else {
|
||||
// Add subspace if no duplicate exists
|
||||
final updatedSubSpaces =
|
||||
List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..add(event.subSpace);
|
||||
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
state.updatedSubSpaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
// Clear error message
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
// Handle RemoveSubSpaceModel Event
|
||||
on<RemoveSubSpaceModel>((event, emit) {
|
||||
final updatedSubSpaces = List<SubspaceTemplateModel>.from(state.subSpaces)
|
||||
..remove(event.subSpace);
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
final nameOccurrences = <String, int>{};
|
||||
for (final subSpace in updatedSubSpaces) {
|
||||
final lowerName = subSpace.subspaceName.toLowerCase();
|
||||
nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1;
|
||||
}
|
||||
|
||||
final updatedDuplicates = nameOccurrences.entries
|
||||
.where((entry) => entry.value > 1)
|
||||
.map((entry) => entry.key)
|
||||
.toSet();
|
||||
|
||||
if (event.subSpace.uuid?.isNotEmpty ?? false) {
|
||||
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete,
|
||||
uuid: event.subSpace.uuid!,
|
||||
));
|
||||
}
|
||||
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
'',
|
||||
updatedDuplicates,
|
||||
// Clear error message
|
||||
));
|
||||
});
|
||||
|
||||
// Handle UpdateSubSpaceModel Event
|
||||
on<UpdateSubSpaceModel>((event, emit) {
|
||||
final updatedSubSpaces = state.subSpaces.map((subSpace) {
|
||||
if (subSpace.uuid == event.updatedSubSpace.uuid) {
|
||||
return event.updatedSubSpace;
|
||||
}
|
||||
return subSpace;
|
||||
}).toList();
|
||||
|
||||
final updatedSubspaceModels = List<UpdateSubspaceTemplateModel>.from(
|
||||
state.updatedSubSpaceModels,
|
||||
);
|
||||
|
||||
updatedSubspaceModels.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.update,
|
||||
uuid: event.updatedSubSpace.uuid!,
|
||||
));
|
||||
|
||||
emit(SubSpaceModelState(
|
||||
updatedSubSpaces,
|
||||
updatedSubspaceModels,
|
||||
'',
|
||||
state.duplicates,
|
||||
// Clear error message
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
|
||||
abstract class SubSpaceModelEvent {}
|
||||
|
||||
class AddSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel subSpace;
|
||||
AddSubSpaceModel(this.subSpace);
|
||||
}
|
||||
|
||||
class RemoveSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel subSpace;
|
||||
RemoveSubSpaceModel(this.subSpace);
|
||||
}
|
||||
|
||||
class UpdateSubSpaceModel extends SubSpaceModelEvent {
|
||||
final SubspaceTemplateModel updatedSubSpace;
|
||||
UpdateSubSpaceModel(this.updatedSubSpace);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
|
||||
class SubSpaceModelState {
|
||||
final List<SubspaceTemplateModel> subSpaces;
|
||||
final List<UpdateSubspaceTemplateModel> updatedSubSpaceModels;
|
||||
final String errorMessage;
|
||||
final Set<String> duplicates;
|
||||
|
||||
SubSpaceModelState(
|
||||
this.subSpaces,
|
||||
this.updatedSubSpaceModels,
|
||||
this.errorMessage,
|
||||
this.duplicates,
|
||||
);
|
||||
|
||||
SubSpaceModelState copyWith({
|
||||
List<SubspaceTemplateModel>? subSpaces,
|
||||
List<UpdateSubspaceTemplateModel>? updatedSubSpaceModels,
|
||||
String? errorMessage,
|
||||
Set<String>? duplicates,
|
||||
}) {
|
||||
return SubSpaceModelState(
|
||||
subSpaces ?? this.subSpaces,
|
||||
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
|
||||
errorMessage ?? this.errorMessage,
|
||||
duplicates ?? this.duplicates,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/create_subspace_model/bloc/subspace_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/bloc/subspace_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSubSpaceModelDialog extends StatelessWidget {
|
||||
final bool isEdit;
|
||||
final String dialogTitle;
|
||||
final List<SubspaceTemplateModel>? existingSubSpaces;
|
||||
final void Function(List<SubspaceTemplateModel> newSubspaces)? onUpdate;
|
||||
|
||||
const CreateSubSpaceModelDialog(
|
||||
{Key? key,
|
||||
required this.isEdit,
|
||||
required this.dialogTitle,
|
||||
this.existingSubSpaces,
|
||||
this.onUpdate})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final textController = TextEditingController();
|
||||
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = SubSpaceModelBloc();
|
||||
if (existingSubSpaces != null) {
|
||||
for (var subSpace in existingSubSpaces!) {
|
||||
bloc.add(AddSubSpaceModel(subSpace));
|
||||
}
|
||||
}
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<SubSpaceModelBloc, SubSpaceModelState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: SizedBox(
|
||||
width: screenWidth * 0.3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
dialogTitle,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10.0, horizontal: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...state.subSpaces.asMap().entries.map(
|
||||
(entry) {
|
||||
final index = entry.key;
|
||||
final subSpace = entry.value;
|
||||
|
||||
final lowerName =
|
||||
subSpace.subspaceName.toLowerCase();
|
||||
|
||||
final duplicateIndices = state.subSpaces
|
||||
.asMap()
|
||||
.entries
|
||||
.where((e) =>
|
||||
e.value.subspaceName.toLowerCase() ==
|
||||
lowerName)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
final isDuplicate =
|
||||
duplicateIndices.length > 1 &&
|
||||
duplicateIndices.indexOf(index) != 0;
|
||||
|
||||
return Chip(
|
||||
label: Text(
|
||||
subSpace.subspaceName,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(
|
||||
color: isDuplicate
|
||||
? ColorsManager.red
|
||||
: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 16,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onDeleted: () => context
|
||||
.read<SubSpaceModelBloc>()
|
||||
.add(RemoveSubSpaceModel(subSpace)),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: state.subSpaces.isEmpty
|
||||
? 'Please enter the name'
|
||||
: null,
|
||||
hintStyle: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty) {
|
||||
context.read<SubSpaceModelBloc>().add(
|
||||
AddSubSpaceModel(
|
||||
SubspaceTemplateModel(
|
||||
subspaceName: value.trim(),
|
||||
disabled: false)));
|
||||
textController.clear();
|
||||
}
|
||||
},
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (state.errorMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: Text(
|
||||
state.errorMessage,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.red,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: (state.errorMessage.isNotEmpty)
|
||||
? null
|
||||
: () async {
|
||||
final subSpaces = context
|
||||
.read<SubSpaceModelBloc>()
|
||||
.state
|
||||
.subSpaces;
|
||||
Navigator.of(context).pop();
|
||||
if (onUpdate != null) {
|
||||
onUpdate!(subSpaces);
|
||||
}
|
||||
},
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: state.errorMessage.isNotEmpty
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
81
lib/pages/spaces_management/helper/tag_helper.dart
Normal file
81
lib/pages/spaces_management/helper/tag_helper.dart
Normal file
@ -0,0 +1,81 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
class TagHelper {
|
||||
static List<TagModel> generateInitialTags({
|
||||
List<TagModel>? spaceTagModels,
|
||||
List<SubspaceTemplateModel>? subspaces,
|
||||
}) {
|
||||
final List<TagModel> initialTags = [];
|
||||
|
||||
if (spaceTagModels != null) {
|
||||
initialTags.addAll(spaceTagModels);
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
for (var existingTag in subspace.tags!) {
|
||||
initialTags.addAll(
|
||||
subspace.tags!.map(
|
||||
(tag) => tag.copyWith(
|
||||
location: subspace.subspaceName,
|
||||
internalId: existingTag.internalId,
|
||||
tag: existingTag.tag),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return initialTags;
|
||||
}
|
||||
|
||||
static Map<ProductModel, int> groupTags(List<TagModel> tags) {
|
||||
final Map<ProductModel, int> groupedTags = {};
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
final product = tag.product!;
|
||||
groupedTags[product] = (groupedTags[product] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return groupedTags;
|
||||
}
|
||||
|
||||
static List<SelectedProduct> createInitialSelectedProducts(
|
||||
List<TagModel>? tags, List<SubspaceTemplateModel>? subspaces) {
|
||||
final Map<ProductModel, int> productCounts = {};
|
||||
|
||||
if (tags != null) {
|
||||
for (var tag in tags) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] = (productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
if (tag.product != null) {
|
||||
productCounts[tag.product!] =
|
||||
(productCounts[tag.product!] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return productCounts.entries
|
||||
.map((entry) => SelectedProduct(
|
||||
productId: entry.key.uuid,
|
||||
count: entry.value,
|
||||
productName: entry.key.name ?? 'Unnamed',
|
||||
product: entry.key,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
|
||||
|
||||
|
||||
class SpaceModelBloc extends Bloc<SpaceModelEvent, SpaceModelState> {
|
||||
SpaceModelBloc() : super(SpaceModelInitial()) {
|
||||
on<SpaceModelSelectedEvent>((event, emit) {
|
||||
emit(SpaceModelSelectedState(event.selectedIndex));
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
abstract class SpaceModelEvent {}
|
||||
|
||||
class SpaceModelSelectedEvent extends SpaceModelEvent {
|
||||
final int selectedIndex;
|
||||
|
||||
SpaceModelSelectedEvent(this.selectedIndex);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
abstract class SpaceModelState {}
|
||||
|
||||
class SpaceModelInitial extends SpaceModelState {}
|
||||
|
||||
class SpaceModelSelectedState extends SpaceModelState {
|
||||
final int selectedIndex;
|
||||
|
||||
SpaceModelSelectedState(this.selectedIndex);
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/link_space_model/bloc/link_space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/link_space_model/bloc/link_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class LinkSpaceModelDialog extends StatelessWidget {
|
||||
final Function(SpaceTemplateModel?)? onSave;
|
||||
final int? initialSelectedIndex;
|
||||
final List<SpaceTemplateModel> spaceModels;
|
||||
|
||||
const LinkSpaceModelDialog({
|
||||
Key? key,
|
||||
this.onSave,
|
||||
this.initialSelectedIndex,
|
||||
required this.spaceModels,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceModelBloc()
|
||||
..add(
|
||||
SpaceModelSelectedEvent(initialSelectedIndex ?? -1),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final bloc = context.read<SpaceModelBloc>();
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
title: const Text('Link a space model'),
|
||||
content: spaceModels.isNotEmpty
|
||||
? Container(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
child: BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
int selectedIndex = -1;
|
||||
if (state is SpaceModelSelectedState) {
|
||||
selectedIndex = state.selectedIndex;
|
||||
}
|
||||
return GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: 3,
|
||||
),
|
||||
itemCount: spaceModels.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = spaceModels[index];
|
||||
final isSelected = selectedIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
bloc.add(SpaceModelSelectedEvent(index));
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? ColorsManager.spaceColor
|
||||
: Colors.transparent,
|
||||
width: 2.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: SpaceModelCardWidget(model: model,),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Text('No space models available.'),
|
||||
actions: [
|
||||
Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CancelButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
label: 'Cancel',
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
final isEnabled = state is SpaceModelSelectedState &&
|
||||
state.selectedIndex >= 0;
|
||||
return SizedBox(
|
||||
width: 140,
|
||||
child: DefaultButton(
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
onPressed: isEnabled
|
||||
? () {
|
||||
if (onSave != null) {
|
||||
final selectedModel =
|
||||
spaceModels[state.selectedIndex];
|
||||
|
||||
onSave!(selectedModel);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,353 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class CreateSpaceModelBloc
|
||||
extends Bloc<CreateSpaceModelEvent, CreateSpaceModelState> {
|
||||
SpaceTemplateModel? _space;
|
||||
|
||||
final SpaceModelManagementApi _api;
|
||||
|
||||
CreateSpaceModelBloc(this._api) : super(CreateSpaceModelInitial()) {
|
||||
on<CreateSpaceTemplate>((event, emit) async {
|
||||
try {
|
||||
late SpaceTemplateModel spaceTemplate = event.spaceTemplate;
|
||||
|
||||
final tagBodyModels =
|
||||
spaceTemplate.tags?.map((tag) => tag.toTagBodyModel()).toList() ??
|
||||
[];
|
||||
|
||||
final subspaceTemplateBodyModels =
|
||||
spaceTemplate.subspaceModels?.map((subspaceModel) {
|
||||
final tagsubspaceBodyModels = subspaceModel.tags
|
||||
?.map((tag) => tag.toTagBodyModel())
|
||||
.toList() ??
|
||||
[];
|
||||
return CreateSubspaceTemplateModel()
|
||||
..subspaceName = subspaceModel.subspaceName
|
||||
..tags = tagsubspaceBodyModels;
|
||||
}).toList() ??
|
||||
[];
|
||||
final spaceModelBody = CreateSpaceTemplateBodyModel(
|
||||
modelName: spaceTemplate.modelName,
|
||||
tags: tagBodyModels,
|
||||
subspaceModels: subspaceTemplateBodyModels);
|
||||
|
||||
final newSpaceTemplate = await _api.createSpaceModel(spaceModelBody);
|
||||
spaceTemplate.uuid = newSpaceTemplate?.uuid ?? '';
|
||||
|
||||
if (newSpaceTemplate != null) {
|
||||
emit(CreateSpaceModelLoaded(spaceTemplate));
|
||||
|
||||
if (event.onCreate != null) {
|
||||
event.onCreate!(spaceTemplate);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(CreateSpaceModelError('Error creating space model'));
|
||||
}
|
||||
});
|
||||
|
||||
on<LoadSpaceTemplate>((event, emit) {
|
||||
emit(CreateSpaceModelLoading());
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (_space != null) {
|
||||
emit(CreateSpaceModelLoaded(_space!));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("No space template found"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
on<UpdateSpaceTemplate>((event, emit) {
|
||||
_space = event.spaceTemplate;
|
||||
emit(CreateSpaceModelLoaded(_space!));
|
||||
});
|
||||
on<AddSubspacesToSpaceTemplate>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
final eventSubspaceIds =
|
||||
event.subspaces.map((e) => e.internalId).toSet();
|
||||
|
||||
// Update or retain subspaces
|
||||
final updatedSubspaces = currentState.space.subspaceModels
|
||||
?.where((subspace) =>
|
||||
eventSubspaceIds.contains(subspace.internalId))
|
||||
.map((subspace) {
|
||||
final matchingEventSubspace = event.subspaces.firstWhere(
|
||||
(e) => e.internalId == subspace.internalId,
|
||||
orElse: () => subspace,
|
||||
);
|
||||
|
||||
// Update the subspace's tags
|
||||
final eventTagIds = matchingEventSubspace.tags
|
||||
?.map((e) => e.internalId)
|
||||
.toSet() ??
|
||||
{};
|
||||
|
||||
final updatedTags = [
|
||||
...?subspace.tags?.map<TagModel>((tag) {
|
||||
final matchingTag =
|
||||
matchingEventSubspace.tags?.firstWhere(
|
||||
(e) => e.internalId == tag.internalId,
|
||||
orElse: () => tag,
|
||||
);
|
||||
final isUpdated = matchingTag != tag;
|
||||
return isUpdated
|
||||
? tag.copyWith(tag: matchingTag?.tag)
|
||||
: tag;
|
||||
}) ??
|
||||
<TagModel>[],
|
||||
...?matchingEventSubspace.tags?.where(
|
||||
(e) =>
|
||||
subspace.tags
|
||||
?.every((t) => t.internalId != e.internalId) ??
|
||||
true,
|
||||
) ??
|
||||
<TagModel>[],
|
||||
];
|
||||
return subspace.copyWith(
|
||||
subspaceName: matchingEventSubspace.subspaceName,
|
||||
tags: updatedTags,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
// Add new subspaces
|
||||
event.subspaces
|
||||
.where((e) =>
|
||||
updatedSubspaces.every((s) => s.internalId != e.internalId))
|
||||
.forEach((newSubspace) {
|
||||
updatedSubspaces.add(newSubspace);
|
||||
});
|
||||
|
||||
final updatedSpace =
|
||||
currentState.space.copyWith(subspaceModels: updatedSubspaces);
|
||||
|
||||
emit(CreateSpaceModelLoaded(updatedSpace));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<AddTagsToSpaceTemplate>((event, emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
final eventTagIds = event.tags.map((e) => e.internalId).toSet();
|
||||
|
||||
final updatedTags = currentState.space.tags
|
||||
?.where((tag) => eventTagIds.contains(tag.internalId))
|
||||
.map((tag) {
|
||||
final matchingEventTag = event.tags.firstWhere(
|
||||
(e) => e.internalId == tag.internalId,
|
||||
orElse: () => tag,
|
||||
);
|
||||
return matchingEventTag != tag
|
||||
? tag.copyWith(tag: matchingEventTag.tag)
|
||||
: tag;
|
||||
}).toList() ??
|
||||
[];
|
||||
|
||||
event.tags
|
||||
.where(
|
||||
(e) => updatedTags.every((t) => t.internalId != e.internalId))
|
||||
.forEach((e) {
|
||||
updatedTags.add(e);
|
||||
});
|
||||
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space.copyWith(tags: updatedTags)));
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<UpdateSpaceTemplateName>((event, emit) {
|
||||
final currentState = state;
|
||||
if (currentState is CreateSpaceModelLoaded) {
|
||||
if (event.allModels.contains(event.name) == true) {
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space,
|
||||
errorMessage: "Duplicate Model name",
|
||||
));
|
||||
} else if (event.name.trim().isEmpty) {
|
||||
emit(CreateSpaceModelLoaded(
|
||||
currentState.space,
|
||||
errorMessage: "Model name cannot be empty",
|
||||
));
|
||||
} else {
|
||||
final updatedSpaceModel =
|
||||
currentState.space.copyWith(modelName: event.name);
|
||||
|
||||
emit(CreateSpaceModelLoaded(updatedSpaceModel));
|
||||
}
|
||||
} else {
|
||||
emit(CreateSpaceModelError("Space template not initialized"));
|
||||
}
|
||||
});
|
||||
|
||||
on<ModifySpaceTemplate>((event, emit) async {
|
||||
try {
|
||||
final prevSpaceModel = event.spaceTemplate;
|
||||
final newSpaceModel = event.updatedSpaceTemplate;
|
||||
String? spaceModelName;
|
||||
if (prevSpaceModel.modelName != newSpaceModel.modelName) {
|
||||
spaceModelName = newSpaceModel.modelName;
|
||||
}
|
||||
List<TagModelUpdate> tagUpdates = [];
|
||||
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
|
||||
final List<SubspaceTemplateModel>? prevSubspaces =
|
||||
prevSpaceModel.subspaceModels;
|
||||
final List<SubspaceTemplateModel>? newSubspaces =
|
||||
newSpaceModel.subspaceModels;
|
||||
|
||||
tagUpdates = processTagUpdates(prevSpaceModel.tags, newSpaceModel.tags);
|
||||
|
||||
if (prevSubspaces != null || newSubspaces != null) {
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
for (var prevSubspace in prevSubspaces!) {
|
||||
final existsInNew = newSubspaces!
|
||||
.any((newTag) => newTag.uuid == prevSubspace.uuid);
|
||||
if (!existsInNew) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevSubspaces != null && newSubspaces == null) {
|
||||
for (var prevSubspace in prevSubspaces) {
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.delete, uuid: prevSubspace.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
if (newSubspaces != null) {
|
||||
for (var newSubspace in newSubspaces!) {
|
||||
// Tag without UUID
|
||||
if ((newSubspace.uuid == null || newSubspace.uuid!.isEmpty)) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
|
||||
if (newSubspace.tags != null) {
|
||||
for (var tag in newSubspace.tags!) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: tag.tag,
|
||||
productUuid: tag.product?.uuid));
|
||||
}
|
||||
}
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.add,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagUpdates));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSubspaces != null && newSubspaces != null) {
|
||||
final newSubspaceMap = {
|
||||
for (var subspace in newSubspaces!) subspace.uuid: subspace
|
||||
};
|
||||
|
||||
for (var prevSubspace in prevSubspaces!) {
|
||||
final newSubspace = newSubspaceMap[prevSubspace.uuid];
|
||||
if (newSubspace != null) {
|
||||
final List<TagModelUpdate> tagSubspaceUpdates =
|
||||
processTagUpdates(prevSubspace.tags, newSubspace.tags);
|
||||
subspaceUpdates.add(UpdateSubspaceTemplateModel(
|
||||
action: Action.update,
|
||||
uuid: newSubspace.uuid,
|
||||
subspaceName: newSubspace.subspaceName,
|
||||
tags: tagSubspaceUpdates));
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final spaceModelBody = CreateSpaceTemplateBodyModel(
|
||||
modelName: spaceModelName,
|
||||
tags: tagUpdates,
|
||||
subspaceModels: subspaceUpdates);
|
||||
|
||||
final res = await _api.updateSpaceModel(
|
||||
spaceModelBody, prevSpaceModel.uuid ?? '');
|
||||
|
||||
if (res != null) {
|
||||
emit(CreateSpaceModelLoaded(newSpaceModel));
|
||||
if (event.onUpdate != null) {
|
||||
event.onUpdate!(event.updatedSpaceTemplate);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(CreateSpaceModelError('Error creating space model'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
List<TagModelUpdate> processTagUpdates(
|
||||
List<TagModel>? prevTags,
|
||||
List<TagModel>? newTags,
|
||||
) {
|
||||
final List<TagModelUpdate> tagUpdates = [];
|
||||
final processedTags = <String?>{};
|
||||
|
||||
if (newTags != null || prevTags != null) {
|
||||
// Case 1: Tags deleted
|
||||
if (prevTags != null && newTags != null) {
|
||||
for (var prevTag in prevTags!) {
|
||||
final existsInNew =
|
||||
newTags!.any((newTag) => newTag.uuid == prevTag.uuid);
|
||||
if (!existsInNew) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
} else if (prevTags != null && newTags == null) {
|
||||
for (var prevTag in prevTags) {
|
||||
tagUpdates
|
||||
.add(TagModelUpdate(action: Action.delete, uuid: prevTag.uuid));
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Tags added
|
||||
if (newTags != null) {
|
||||
for (var newTag in newTags!) {
|
||||
// Tag without UUID
|
||||
if ((newTag.uuid == null || newTag.uuid!.isEmpty) &&
|
||||
!processedTags.contains(newTag.tag)) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.add,
|
||||
tag: newTag.tag,
|
||||
productUuid: newTag.product?.uuid));
|
||||
processedTags.add(newTag.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tags updated
|
||||
if (prevTags != null && newTags != null) {
|
||||
final newTagMap = {for (var tag in newTags!) tag.uuid: tag};
|
||||
|
||||
for (var prevTag in prevTags!) {
|
||||
final newTag = newTagMap[prevTag.uuid];
|
||||
if (newTag != null) {
|
||||
tagUpdates.add(TagModelUpdate(
|
||||
action: Action.update,
|
||||
uuid: newTag.uuid,
|
||||
tag: newTag.tag,
|
||||
));
|
||||
} else {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagUpdates;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class CreateSpaceModelEvent extends Equatable {
|
||||
const CreateSpaceModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadSpaceTemplate extends CreateSpaceModelEvent {}
|
||||
|
||||
class UpdateSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
|
||||
UpdateSpaceTemplate(this.spaceTemplate);
|
||||
}
|
||||
|
||||
class CreateSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
final Function(SpaceTemplateModel)? onCreate;
|
||||
|
||||
const CreateSpaceTemplate({
|
||||
required this.spaceTemplate,
|
||||
this.onCreate,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [spaceTemplate];
|
||||
}
|
||||
|
||||
class UpdateSpaceTemplateName extends CreateSpaceModelEvent {
|
||||
final String name;
|
||||
final List<String> allModels;
|
||||
|
||||
UpdateSpaceTemplateName({required this.name, required this.allModels});
|
||||
|
||||
@override
|
||||
List<Object> get props => [name, allModels];
|
||||
}
|
||||
|
||||
class AddSubspacesToSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final List<SubspaceTemplateModel> subspaces;
|
||||
|
||||
AddSubspacesToSpaceTemplate(this.subspaces);
|
||||
}
|
||||
|
||||
class AddTagsToSpaceTemplate extends CreateSpaceModelEvent {
|
||||
final List<TagModel> tags;
|
||||
|
||||
AddTagsToSpaceTemplate(this.tags);
|
||||
}
|
||||
|
||||
class ValidateSpaceTemplateName extends CreateSpaceModelEvent {
|
||||
final String name;
|
||||
|
||||
ValidateSpaceTemplateName({required this.name});
|
||||
}
|
||||
|
||||
class ModifySpaceTemplate extends CreateSpaceModelEvent {
|
||||
final SpaceTemplateModel spaceTemplate;
|
||||
final SpaceTemplateModel updatedSpaceTemplate;
|
||||
final Function(SpaceTemplateModel)? onUpdate;
|
||||
|
||||
ModifySpaceTemplate(
|
||||
{required this.spaceTemplate,
|
||||
required this.updatedSpaceTemplate,
|
||||
this.onUpdate});
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class CreateSpaceModelState extends Equatable {
|
||||
const CreateSpaceModelState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class CreateSpaceModelInitial extends CreateSpaceModelState {}
|
||||
|
||||
class CreateSpaceModelLoading extends CreateSpaceModelState {}
|
||||
|
||||
class CreateSpaceModelLoaded extends CreateSpaceModelState {
|
||||
final SpaceTemplateModel space;
|
||||
final String? errorMessage;
|
||||
|
||||
CreateSpaceModelLoaded(this.space, {this.errorMessage});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [space, errorMessage];
|
||||
}
|
||||
|
||||
class CreateSpaceModelError extends CreateSpaceModelState {
|
||||
final String message;
|
||||
|
||||
CreateSpaceModelError(this.message);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
|
||||
class SpaceModelBloc extends Bloc<SpaceModelEvent, SpaceModelState> {
|
||||
final SpaceModelManagementApi api;
|
||||
|
||||
SpaceModelBloc({
|
||||
required this.api,
|
||||
required List<SpaceTemplateModel> initialSpaceModels,
|
||||
}) : super(SpaceModelLoaded(spaceModels: initialSpaceModels)) {
|
||||
on<CreateSpaceModel>(_onCreateSpaceModel);
|
||||
on<UpdateSpaceModel>(_onUpdateSpaceModel);
|
||||
}
|
||||
|
||||
Future<void> _onCreateSpaceModel(
|
||||
CreateSpaceModel event, Emitter<SpaceModelState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is SpaceModelLoaded) {
|
||||
try {
|
||||
final newSpaceModel =
|
||||
await api.getSpaceModel(event.newSpaceModel.uuid ?? '');
|
||||
|
||||
if (newSpaceModel != null) {
|
||||
final updatedSpaceModels =
|
||||
List<SpaceTemplateModel>.from(currentState.spaceModels)
|
||||
..add(newSpaceModel);
|
||||
emit(SpaceModelLoaded(spaceModels: updatedSpaceModels));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceModelError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onUpdateSpaceModel(
|
||||
UpdateSpaceModel event, Emitter<SpaceModelState> emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is SpaceModelLoaded) {
|
||||
try {
|
||||
final newSpaceModel =
|
||||
await api.getSpaceModel(event.spaceModelUuid ?? '');
|
||||
if (newSpaceModel != null) {
|
||||
final updatedSpaceModels = currentState.spaceModels.map((model) {
|
||||
return model.uuid == event.spaceModelUuid ? newSpaceModel : model;
|
||||
}).toList();
|
||||
emit(SpaceModelLoaded(spaceModels: updatedSpaceModels));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SpaceModelError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceModelEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadSpaceModels extends SpaceModelEvent {}
|
||||
|
||||
class CreateSpaceModel extends SpaceModelEvent {
|
||||
final SpaceTemplateModel newSpaceModel;
|
||||
|
||||
CreateSpaceModel({required this.newSpaceModel});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [newSpaceModel];
|
||||
}
|
||||
|
||||
class GetSpaceModel extends SpaceModelEvent {
|
||||
final String spaceModelUuid;
|
||||
|
||||
GetSpaceModel({required this.spaceModelUuid});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModelUuid];
|
||||
}
|
||||
|
||||
class UpdateSpaceModel extends SpaceModelEvent {
|
||||
final String spaceModelUuid;
|
||||
|
||||
UpdateSpaceModel({required this.spaceModelUuid});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModelUuid];
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
|
||||
abstract class SpaceModelState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class SpaceModelInitial extends SpaceModelState {}
|
||||
|
||||
class SpaceModelLoading extends SpaceModelState {}
|
||||
|
||||
class SpaceModelLoaded extends SpaceModelState {
|
||||
final List<SpaceTemplateModel> spaceModels;
|
||||
|
||||
SpaceModelLoaded({required this.spaceModels});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [spaceModels];
|
||||
}
|
||||
|
||||
class SpaceModelError extends SpaceModelState {
|
||||
final String message;
|
||||
|
||||
SpaceModelError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
class TagBodyModel {
|
||||
late String uuid;
|
||||
late String tag;
|
||||
late final String? productUuid;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
||||
|
||||
class CreateSubspaceTemplateModel {
|
||||
late String subspaceName;
|
||||
late List<TagBodyModel>? tags;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((tag) => tag.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CreateSpaceTemplateBodyModel {
|
||||
final String? modelName;
|
||||
final List<dynamic>? tags;
|
||||
final List<dynamic>? subspaceModels;
|
||||
|
||||
CreateSpaceTemplateBodyModel({
|
||||
this.modelName,
|
||||
this.tags,
|
||||
this.subspaceModels,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'modelName': modelName,
|
||||
'tags': tags,
|
||||
'subspaceModels': subspaceModels,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceTemplateModel extends Equatable {
|
||||
String? uuid;
|
||||
String modelName;
|
||||
List<SubspaceTemplateModel>? subspaceModels;
|
||||
final List<TagModel>? tags;
|
||||
String internalId;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [modelName, subspaceModels, tags];
|
||||
|
||||
SpaceTemplateModel({
|
||||
this.uuid,
|
||||
String? internalId,
|
||||
required this.modelName,
|
||||
this.subspaceModels,
|
||||
this.tags,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SpaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return SpaceTemplateModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
internalId: internalId,
|
||||
modelName: json['modelName'] ?? '',
|
||||
subspaceModels: (json['subspaceModels'] as List<dynamic>?)
|
||||
?.where((e) => e is Map<String, dynamic>) // Validate type
|
||||
.map((e) =>
|
||||
SubspaceTemplateModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.where((item) => item is Map<String, dynamic>) // Validate type
|
||||
.map((item) => TagModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
SpaceTemplateModel copyWith({
|
||||
String? uuid,
|
||||
String? modelName,
|
||||
List<SubspaceTemplateModel>? subspaceModels,
|
||||
List<TagModel>? tags,
|
||||
String? internalId,
|
||||
}) {
|
||||
return SpaceTemplateModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
modelName: modelName ?? this.modelName,
|
||||
subspaceModels: subspaceModels ?? this.subspaceModels,
|
||||
tags: tags ?? this.tags,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'modelName': modelName,
|
||||
'subspaceModels': subspaceModels?.map((e) => e.toJson()).toList(),
|
||||
'tags': tags?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateSubspaceTemplateModel {
|
||||
final String? uuid;
|
||||
final Action action;
|
||||
final String? subspaceName;
|
||||
final List<TagModelUpdate>? tags;
|
||||
|
||||
UpdateSubspaceTemplateModel({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
this.subspaceName,
|
||||
this.tags,
|
||||
});
|
||||
|
||||
factory UpdateSubspaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
return UpdateSubspaceTemplateModel(
|
||||
action: ActionExtension.fromValue(json['action']),
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
tags: (json['tags'] as List)
|
||||
.map((item) => TagModelUpdate.fromJson(item))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension SpaceTemplateExtensions on SpaceTemplateModel {
|
||||
List<String> listAllTagValues() {
|
||||
final List<String> tagValues = [];
|
||||
|
||||
if (tags != null) {
|
||||
tagValues.addAll(
|
||||
tags!.map((tag) => tag.tag ?? '').where((tag) => tag.isNotEmpty));
|
||||
}
|
||||
|
||||
if (subspaceModels != null) {
|
||||
for (final subspace in subspaceModels!) {
|
||||
if (subspace.tags != null) {
|
||||
tagValues.addAll(
|
||||
subspace.tags!
|
||||
.map((tag) => tag.tag ?? '')
|
||||
.where((tag) => tag.isNotEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tagValues;
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SubspaceTemplateModel {
|
||||
final String? uuid;
|
||||
String subspaceName;
|
||||
final bool disabled;
|
||||
List<TagModel>? tags;
|
||||
String internalId;
|
||||
|
||||
SubspaceTemplateModel({
|
||||
this.uuid,
|
||||
required this.subspaceName,
|
||||
required this.disabled,
|
||||
this.tags,
|
||||
String? internalId,
|
||||
}) : internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory SubspaceTemplateModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return SubspaceTemplateModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
subspaceName: json['subspaceName'] ?? '',
|
||||
internalId: internalId,
|
||||
disabled: json['disabled'] ?? false,
|
||||
tags: (json['tags'] as List<dynamic>?)
|
||||
?.map((item) => TagModel.fromJson(item))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'subspaceName': subspaceName,
|
||||
'disabled': disabled,
|
||||
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
SubspaceTemplateModel copyWith({
|
||||
String? uuid,
|
||||
String? subspaceName,
|
||||
bool? disabled,
|
||||
List<TagModel>? tags,
|
||||
String? internalId,
|
||||
}) {
|
||||
return SubspaceTemplateModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
subspaceName: subspaceName ?? this.subspaceName,
|
||||
disabled: disabled ?? this.disabled,
|
||||
tags: tags ?? this.tags,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
class CreateTagBodyModel {
|
||||
late String tag;
|
||||
late final String? productUuid;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return toJson().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class TagModel {
|
||||
String? uuid;
|
||||
String? tag;
|
||||
final ProductModel? product;
|
||||
String internalId;
|
||||
String? location;
|
||||
|
||||
TagModel(
|
||||
{this.uuid,
|
||||
required this.tag,
|
||||
this.product,
|
||||
String? internalId,
|
||||
this.location})
|
||||
: internalId = internalId ?? const Uuid().v4();
|
||||
|
||||
factory TagModel.fromJson(Map<String, dynamic> json) {
|
||||
final String internalId = json['internalId'] ?? const Uuid().v4();
|
||||
|
||||
return TagModel(
|
||||
uuid: json['uuid'] ?? '',
|
||||
internalId: internalId,
|
||||
tag: json['tag'] ?? '',
|
||||
product: json['product'] != null
|
||||
? ProductModel.fromMap(json['product'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
TagModel copyWith(
|
||||
{String? tag,
|
||||
ProductModel? product,
|
||||
String? location,
|
||||
String? internalId}) {
|
||||
return TagModel(
|
||||
tag: tag ?? this.tag,
|
||||
product: product ?? this.product,
|
||||
location: location ?? this.location,
|
||||
internalId: internalId ?? this.internalId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'tag': tag,
|
||||
'product': product?.toMap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension TagModelExtensions on TagModel {
|
||||
TagBodyModel toTagBodyModel() {
|
||||
return TagBodyModel()
|
||||
..uuid = uuid ?? ''
|
||||
..tag = tag ?? ''
|
||||
..productUuid = product?.uuid;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import 'package:syncrow_web/utils/constants/action_enum.dart';
|
||||
|
||||
class TagModelUpdate {
|
||||
final Action action;
|
||||
final String? uuid;
|
||||
final String? tag;
|
||||
final String? productUuid;
|
||||
|
||||
TagModelUpdate({
|
||||
required this.action,
|
||||
this.uuid,
|
||||
this.tag,
|
||||
this.productUuid,
|
||||
});
|
||||
|
||||
factory TagModelUpdate.fromJson(Map<String, dynamic> json) {
|
||||
return TagModelUpdate(
|
||||
action: json['action'],
|
||||
uuid: json['uuid'],
|
||||
tag: json['tag'],
|
||||
productUuid: json['productUuid'],
|
||||
);
|
||||
}
|
||||
|
||||
// Method to convert an instance to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'action': action.value,
|
||||
'uuid': uuid, // Nullable field
|
||||
'tag': tag,
|
||||
'productUuid': productUuid,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/add_space_model_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/space_model_card_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceModelPage extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
|
||||
const SpaceModelPage({Key? key, this.products}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SpaceModelBloc, SpaceModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is SpaceModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SpaceModelLoaded) {
|
||||
final spaceModels = state.spaceModels;
|
||||
final allTagValues = _getAllTagValues(spaceModels);
|
||||
final allSpaceModelNames = _getAllSpaceModelName(spaceModels);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20.0, 16.0, 16.0, 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
childAspectRatio: _calculateChildAspectRatio(context),
|
||||
),
|
||||
itemCount: spaceModels.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == spaceModels.length) {
|
||||
// Add Button
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTagValues,
|
||||
pageContext: context,
|
||||
otherSpaceModels: allSpaceModelNames,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const AddSpaceModelWidget(),
|
||||
);
|
||||
}
|
||||
// Render existing space model
|
||||
final model = spaceModels[index];
|
||||
final otherModel = List<String>.from(allSpaceModelNames);
|
||||
otherModel.remove(model.modelName);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTagValues,
|
||||
spaceModel: model,
|
||||
otherSpaceModels: otherModel,
|
||||
pageContext: context,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: SpaceModelCardWidget(model: model),
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is SpaceModelError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Error: ${state.message}',
|
||||
style: const TextStyle(color: ColorsManager.warningRed),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: Text('Initializing...'));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _calculateChildAspectRatio(BuildContext context) {
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth > 1600) {
|
||||
return 2;
|
||||
}
|
||||
if (screenWidth > 1200) {
|
||||
return 3;
|
||||
} else if (screenWidth > 800) {
|
||||
return 3.5;
|
||||
} else {
|
||||
return 4.0;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _getAllTagValues(List<SpaceTemplateModel> spaceModels) {
|
||||
final List<String> allTags = [];
|
||||
for (final spaceModel in spaceModels) {
|
||||
if (spaceModel.tags != null) {
|
||||
allTags.addAll(spaceModel.listAllTagValues());
|
||||
}
|
||||
}
|
||||
return allTags;
|
||||
}
|
||||
|
||||
List<String> _getAllSpaceModelName(List<SpaceTemplateModel> spaceModels) {
|
||||
final List<String> names = [];
|
||||
for (final spaceModel in spaceModels) {
|
||||
names.add(spaceModel.modelName);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AddSpaceModelWidget extends StatelessWidget {
|
||||
const AddSpaceModelWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.semiTransparentBlackColor,
|
||||
blurRadius: 15,
|
||||
offset: Offset(0, 4),
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: ColorsManager.semiTransparentBlackColor,
|
||||
blurRadius: 25,
|
||||
offset: Offset(0, 15),
|
||||
spreadRadius: -5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 60,
|
||||
height: 60, // Set a proper height here
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: ColorsManager.neutralGray,
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
size: 40,
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonContentWidget extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const ButtonContentWidget({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 3.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/create_space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/tag_chips_display_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/bloc/space_model_event.dart';
|
||||
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_model_create_widget.dart';
|
||||
import 'package:syncrow_web/services/space_model_mang_api.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceModelDialog extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<String>? allTags;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
|
||||
const CreateSpaceModelDialog(
|
||||
{Key? key,
|
||||
this.products,
|
||||
this.allTags,
|
||||
this.spaceModel,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final SpaceModelManagementApi _spaceModelApi = SpaceModelManagementApi();
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final TextEditingController spaceNameController = TextEditingController(
|
||||
text: spaceModel?.modelName ?? '',
|
||||
);
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
width: screenWidth * 0.3,
|
||||
child: BlocProvider(
|
||||
create: (_) {
|
||||
final bloc = CreateSpaceModelBloc(_spaceModelApi);
|
||||
if (spaceModel != null) {
|
||||
bloc.add(UpdateSpaceTemplate(spaceModel!));
|
||||
} else {
|
||||
bloc.add(UpdateSpaceTemplate(SpaceTemplateModel(
|
||||
modelName: '',
|
||||
subspaceModels: const [],
|
||||
)));
|
||||
}
|
||||
|
||||
spaceNameController.addListener(() {
|
||||
bloc.add(UpdateSpaceTemplateName(
|
||||
name: spaceNameController.text,
|
||||
allModels: otherSpaceModels ?? []));
|
||||
});
|
||||
|
||||
return bloc;
|
||||
},
|
||||
child: BlocBuilder<CreateSpaceModelBloc, CreateSpaceModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is CreateSpaceModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is CreateSpaceModelLoaded) {
|
||||
final updatedSpaceModel = state.space;
|
||||
final subspaces = updatedSpaceModel.subspaceModels ?? [];
|
||||
final isNameValid = spaceNameController.text.trim().isNotEmpty;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
spaceModel?.uuid == null
|
||||
? 'Create New Space Model'
|
||||
: 'Edit Space Model',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: TextField(
|
||||
controller: spaceNameController,
|
||||
onChanged: (value) {
|
||||
context.read<CreateSpaceModelBloc>().add(
|
||||
UpdateSpaceTemplateName(
|
||||
name: value,
|
||||
allModels: otherSpaceModels ?? []));
|
||||
},
|
||||
style: const TextStyle(color: ColorsManager.blackColor),
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: ColorsManager.textFieldGreyColor,
|
||||
hintText: 'Please enter the name',
|
||||
errorText: state.errorMessage,
|
||||
hintStyle: const TextStyle(
|
||||
color: ColorsManager.lightGrayColor),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0,
|
||||
horizontal: 16.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SubspaceModelCreate(
|
||||
context,
|
||||
subspaces: state.space.subspaceModels ?? [],
|
||||
onSpaceModelUpdate: (updatedSubspaces) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(AddSubspacesToSpaceTemplate(updatedSubspaces));
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TagChipDisplay(
|
||||
context,
|
||||
screenWidth: screenWidth,
|
||||
spaceModel: updatedSpaceModel,
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
allTags: allTags,
|
||||
spaceNameController: spaceNameController,
|
||||
pageContext: pageContext,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DefaultButton(
|
||||
onPressed: state.errorMessage == null ||
|
||||
isNameValid
|
||||
? () {
|
||||
final updatedSpaceTemplate =
|
||||
updatedSpaceModel.copyWith(
|
||||
modelName:
|
||||
spaceNameController.text.trim(),
|
||||
);
|
||||
if (updatedSpaceModel.uuid == null) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(
|
||||
CreateSpaceTemplate(
|
||||
spaceTemplate:
|
||||
updatedSpaceTemplate,
|
||||
onCreate: (newModel) {
|
||||
if (pageContext != null) {
|
||||
pageContext!
|
||||
.read<SpaceModelBloc>()
|
||||
.add(CreateSpaceModel(
|
||||
newSpaceModel:
|
||||
newModel));
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop(); // Close the dialog
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (pageContext != null) {
|
||||
final currentState = pageContext!
|
||||
.read<SpaceModelBloc>()
|
||||
.state;
|
||||
if (currentState
|
||||
is SpaceModelLoaded) {
|
||||
final spaceModels =
|
||||
List<SpaceTemplateModel>.from(
|
||||
currentState.spaceModels);
|
||||
|
||||
final SpaceTemplateModel?
|
||||
currentSpaceModel = spaceModels
|
||||
.cast<SpaceTemplateModel?>()
|
||||
.firstWhere(
|
||||
(sm) =>
|
||||
sm?.uuid ==
|
||||
updatedSpaceModel
|
||||
.uuid,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (currentSpaceModel != null) {
|
||||
context
|
||||
.read<CreateSpaceModelBloc>()
|
||||
.add(ModifySpaceTemplate(
|
||||
spaceTemplate:
|
||||
currentSpaceModel,
|
||||
updatedSpaceTemplate:
|
||||
updatedSpaceTemplate,
|
||||
onUpdate: (newModel) {
|
||||
if (pageContext !=
|
||||
null) {
|
||||
pageContext!
|
||||
.read<
|
||||
SpaceModelBloc>()
|
||||
.add(UpdateSpaceModel(
|
||||
spaceModelUuid:
|
||||
newModel.uuid ??
|
||||
''));
|
||||
}
|
||||
Navigator.of(context)
|
||||
.pop();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
borderRadius: 10,
|
||||
foregroundColor: isNameValid
|
||||
? ColorsManager.whiteColors
|
||||
: ColorsManager.whiteColorsWithOpacity,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is CreateSpaceModelError) {
|
||||
return Text(
|
||||
'Error: ${state.message}',
|
||||
style: const TextStyle(color: ColorsManager.warningRed),
|
||||
);
|
||||
}
|
||||
|
||||
return const Center(child: Text('Initializing...'));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/ellipsis_item_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/flexible_item_widget.dart';
|
||||
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DynamicProductWidget extends StatelessWidget {
|
||||
final Map<String, int> productTagCount;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
|
||||
const DynamicProductWidget({
|
||||
Key? key,
|
||||
required this.productTagCount,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double itemSpacing = 8.0;
|
||||
const double lineSpacing = 8.0;
|
||||
const double textPadding = 16.0;
|
||||
const double itemHeight = 40.0;
|
||||
|
||||
List<Widget> productWidgets = [];
|
||||
double currentLineWidth = 0.0;
|
||||
double currentHeight = itemHeight;
|
||||
|
||||
for (final product in productTagCount.entries) {
|
||||
final String prodType = product.key;
|
||||
final int count = product.value;
|
||||
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: 'x$count',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final double itemWidth = textPainter.width + textPadding + 20;
|
||||
|
||||
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
|
||||
currentHeight += itemHeight + lineSpacing;
|
||||
if (currentHeight > maxHeight) {
|
||||
productWidgets.add(const EllipsisItemWidget());
|
||||
break;
|
||||
}
|
||||
currentLineWidth = 0.0;
|
||||
}
|
||||
|
||||
productWidgets.add(FlexibleItemWidget(prodType: prodType, count: count));
|
||||
currentLineWidth += itemWidth + itemSpacing;
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: itemSpacing,
|
||||
runSpacing: lineSpacing,
|
||||
children: productWidgets,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/room_name_widget.dart';
|
||||
|
||||
class DynamicRoomWidget extends StatelessWidget {
|
||||
final List<SubspaceTemplateModel>? subspaceModels;
|
||||
final double maxWidth;
|
||||
final double maxHeight;
|
||||
|
||||
const DynamicRoomWidget({
|
||||
Key? key,
|
||||
required this.subspaceModels,
|
||||
required this.maxWidth,
|
||||
required this.maxHeight,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double itemSpacing = 8.0;
|
||||
const double lineSpacing = 8.0;
|
||||
const double textPadding = 16.0;
|
||||
const double itemHeight = 30.0;
|
||||
|
||||
List<Widget> roomWidgets = [];
|
||||
double currentLineWidth = 0.0;
|
||||
double currentHeight = itemHeight;
|
||||
|
||||
for (final subspace in subspaceModels!) {
|
||||
final TextPainter textPainter = TextPainter(
|
||||
text: TextSpan(
|
||||
text: subspace.subspaceName,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
final double itemWidth = textPainter.width + textPadding;
|
||||
|
||||
if (currentLineWidth + itemWidth + itemSpacing > maxWidth) {
|
||||
currentHeight += itemHeight + lineSpacing;
|
||||
if (currentHeight > maxHeight) {
|
||||
roomWidgets.add(const RoomNameWidget(name: "..."));
|
||||
break;
|
||||
}
|
||||
currentLineWidth = 0.0;
|
||||
}
|
||||
|
||||
roomWidgets.add(RoomNameWidget(name: subspace.subspaceName));
|
||||
currentLineWidth += itemWidth + itemSpacing;
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: itemSpacing,
|
||||
runSpacing: lineSpacing,
|
||||
children: roomWidgets,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EllipsisItemWidget extends StatelessWidget {
|
||||
const EllipsisItemWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Text(
|
||||
"...",
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class FlexibleItemWidget extends StatelessWidget {
|
||||
final String prodType;
|
||||
final int count;
|
||||
|
||||
const FlexibleItemWidget({
|
||||
Key? key,
|
||||
required this.prodType,
|
||||
required this.count,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
prodType,
|
||||
width: 15,
|
||||
height: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'x$count',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RoomNameWidget extends StatelessWidget {
|
||||
final String name;
|
||||
|
||||
const RoomNameWidget({Key? key, required this.name}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Text(
|
||||
name,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_product_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dynamic_room_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceModelCardWidget extends StatelessWidget {
|
||||
final SpaceTemplateModel model;
|
||||
|
||||
const SpaceModelCardWidget({Key? key, required this.model}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, int> productTagCount = {};
|
||||
|
||||
if (model.tags != null) {
|
||||
for (var tag in model.tags!) {
|
||||
final prodIcon = tag.product?.icon ?? 'Unknown';
|
||||
productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (model.subspaceModels != null) {
|
||||
for (var subspace in model.subspaceModels!) {
|
||||
if (subspace.tags != null) {
|
||||
for (var tag in subspace.tags!) {
|
||||
final prodIcon = tag.product?.icon ?? 'Unknown';
|
||||
productTagCount[prodIcon] = (productTagCount[prodIcon] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
model.modelName,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// Left Container
|
||||
Expanded(
|
||||
flex: 1, // Distribute space proportionally
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: DynamicRoomWidget(
|
||||
subspaceModels: model.subspaceModels,
|
||||
maxWidth: constraints.maxWidth,
|
||||
maxHeight: constraints.maxHeight,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (productTagCount.isNotEmpty && model.subspaceModels != null)
|
||||
Container(
|
||||
width: 1.0,
|
||||
color: ColorsManager.softGray,
|
||||
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1, // Distribute space proportionally
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: DynamicProductWidget(
|
||||
productTagCount: productTagCount,
|
||||
maxWidth: constraints.maxWidth,
|
||||
maxHeight: constraints.maxHeight));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SubspaceChipWidget extends StatelessWidget {
|
||||
final String subspace;
|
||||
|
||||
const SubspaceChipWidget({
|
||||
Key? key,
|
||||
required this.subspace,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
subspace,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/views/create_subspace_model_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SubspaceModelCreate extends StatelessWidget {
|
||||
final List<SubspaceTemplateModel> subspaces;
|
||||
final void Function(List<SubspaceTemplateModel> newSubspaces)?
|
||||
onSpaceModelUpdate;
|
||||
|
||||
const SubspaceModelCreate(BuildContext context,
|
||||
{Key? key, required this.subspaces, this.onSpaceModelUpdate})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
child: subspaces.isEmpty
|
||||
? TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
overlayColor: ColorsManager.transparentColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
await _openDialog(context, 'Create Sub-space');
|
||||
},
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Create Sub Space',
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...subspaces.map((subspace) => Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: ColorsManager.transparentColor),
|
||||
),
|
||||
child: Text(
|
||||
subspace.subspaceName,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.spaceColor),
|
||||
),
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
await _openDialog(context, 'Edit Sub-space');
|
||||
},
|
||||
child: Chip(
|
||||
label: const Text(
|
||||
'Edit',
|
||||
style: TextStyle(color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side:
|
||||
const BorderSide(color: ColorsManager.spaceColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openDialog(BuildContext context, String dialogTitle) async {
|
||||
await showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CreateSubSpaceModelDialog(
|
||||
isEdit: true,
|
||||
dialogTitle: dialogTitle,
|
||||
existingSubSpaces: subspaces,
|
||||
onUpdate: (subspaceModels) {
|
||||
onSpaceModelUpdate!(subspaceModels);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TagChipDisplay extends StatelessWidget {
|
||||
final double screenWidth;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
final List<ProductModel>? products;
|
||||
final List<SubspaceTemplateModel>? subspaces;
|
||||
final List<String>? allTags;
|
||||
final TextEditingController spaceNameController;
|
||||
final BuildContext? pageContext;
|
||||
final List<String>? otherSpaceModels;
|
||||
|
||||
const TagChipDisplay(BuildContext context,
|
||||
{Key? key,
|
||||
required this.screenWidth,
|
||||
required this.spaceModel,
|
||||
required this.products,
|
||||
required this.subspaces,
|
||||
required this.allTags,
|
||||
required this.spaceNameController,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (spaceModel?.tags?.isNotEmpty == true ||
|
||||
spaceModel?.subspaceModels
|
||||
?.any((subspace) => subspace.tags?.isNotEmpty == true) ==
|
||||
true)
|
||||
? SizedBox(
|
||||
width: screenWidth * 0.25,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0, // Border width
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
...TagHelper.groupTags([
|
||||
...?spaceModel?.tags,
|
||||
...?spaceModel?.subspaceModels
|
||||
?.expand((subspace) => subspace.tags ?? [])
|
||||
]).entries.map(
|
||||
(entry) => Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
entry.key.icon ?? 'assets/icons/gateway.svg',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x${entry.value}', // Show count
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// Use the Navigator's context for showDialog
|
||||
final navigatorContext =
|
||||
Navigator.of(context).overlay?.context;
|
||||
|
||||
if (navigatorContext != null) {
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: navigatorContext,
|
||||
builder: (context) => AssignTagModelsDialog(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
pageContext: pageContext,
|
||||
allTags: allTags,
|
||||
spaceModel: spaceModel,
|
||||
initialTags: TagHelper.generateInitialTags(
|
||||
subspaces: subspaces,
|
||||
spaceTagModels: spaceModel?.tags ?? []),
|
||||
title: 'Edit Device',
|
||||
addedProducts:
|
||||
TagHelper.createInitialSelectedProducts(
|
||||
spaceModel?.tags ?? [], subspaces),
|
||||
spaceName: spaceModel?.modelName ?? '',
|
||||
));
|
||||
}
|
||||
},
|
||||
child: Chip(
|
||||
label: const Text(
|
||||
'Edit',
|
||||
style: TextStyle(color: ColorsManager.spaceColor),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(color: ColorsManager.spaceColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
await showDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AddDeviceTypeModelWidget(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
allTags: allTags,
|
||||
spaceName: spaceNameController.text,
|
||||
pageContext: pageContext,
|
||||
isCreate: true,
|
||||
spaceModel: spaceModel,
|
||||
),
|
||||
);
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
child: const ButtonContentWidget(
|
||||
icon: Icons.add,
|
||||
label: 'Add Devices',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart';
|
||||
|
||||
// Bloc Implementation
|
||||
class CenterBodyBloc extends Bloc<CenterBodyEvent, CenterBodyState> {
|
||||
CenterBodyBloc() : super(InitialState()) {
|
||||
on<CommunityStructureSelectedEvent>((event, emit) {
|
||||
emit(CommunityStructureState());
|
||||
});
|
||||
|
||||
on<SpaceModelSelectedEvent>((event, emit) {
|
||||
emit(SpaceModelState());
|
||||
});
|
||||
|
||||
on<CommunitySelectedEvent>((event, emit) {
|
||||
emit(CommunitySelectedState());
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
// Define Events
|
||||
abstract class CenterBodyEvent {}
|
||||
|
||||
class CommunityStructureSelectedEvent extends CenterBodyEvent {}
|
||||
|
||||
class SpaceModelSelectedEvent extends CenterBodyEvent {}
|
||||
|
||||
class CommunitySelectedEvent extends CenterBodyEvent {}
|
@ -0,0 +1,9 @@
|
||||
abstract class CenterBodyState {}
|
||||
|
||||
class InitialState extends CenterBodyState {}
|
||||
|
||||
class CommunityStructureState extends CenterBodyState {}
|
||||
|
||||
class SpaceModelState extends CenterBodyState {}
|
||||
|
||||
class CommunitySelectedState extends CenterBodyState {}
|
@ -0,0 +1,81 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/bloc/space_management_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/structure_selector/bloc/center_body_state.dart';
|
||||
import '../bloc/center_body_bloc.dart';
|
||||
|
||||
class CenterBodyWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CenterBodyBloc, CenterBodyState>(
|
||||
builder: (context, state) {
|
||||
if (state is InitialState) {
|
||||
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
|
||||
}
|
||||
if (state is CommunityStructureState) {
|
||||
context.read<SpaceManagementBloc>().add(BlankStateEvent());
|
||||
}
|
||||
|
||||
if (state is SpaceModelState) {
|
||||
context.read<SpaceManagementBloc>().add(SpaceModelLoadEvent());
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
|
||||
},
|
||||
child: Text(
|
||||
'Community Structure',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontWeight: state is CommunityStructureState || state is CommunitySelectedState
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: state is CommunityStructureState || state is CommunitySelectedState
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
|
||||
},
|
||||
child: Text(
|
||||
'Space Model',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontWeight: state is SpaceModelState
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: state is SpaceModelState
|
||||
? Theme.of(context).textTheme.bodyLarge!.color
|
||||
: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge!
|
||||
.color!
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
|
||||
|
||||
class AddDeviceTypeModelBloc
|
||||
extends Bloc<AddDeviceTypeModelEvent, AddDeviceModelState> {
|
||||
AddDeviceTypeModelBloc() : super(AddDeviceModelInitial()) {
|
||||
on<InitializeDeviceTypeModel>(_onInitializeTagModels);
|
||||
on<UpdateProductCountEvent>(_onUpdateProductCount);
|
||||
}
|
||||
|
||||
void _onInitializeTagModels(
|
||||
InitializeDeviceTypeModel event, Emitter<AddDeviceModelState> emit) {
|
||||
emit(AddDeviceModelLoaded(
|
||||
selectedProducts: event.addedProducts,
|
||||
initialTag: event.initialTags,
|
||||
));
|
||||
}
|
||||
|
||||
void _onUpdateProductCount(
|
||||
UpdateProductCountEvent event, Emitter<AddDeviceModelState> emit) {
|
||||
final currentState = state;
|
||||
|
||||
if (currentState is AddDeviceModelLoaded) {
|
||||
final existingProduct = currentState.selectedProducts.firstWhere(
|
||||
(p) => p.productId == event.productId,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: 0,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
);
|
||||
|
||||
List<SelectedProduct> updatedProducts;
|
||||
|
||||
if (event.count > 0) {
|
||||
if (!currentState.selectedProducts.contains(existingProduct)) {
|
||||
updatedProducts = [
|
||||
...currentState.selectedProducts,
|
||||
SelectedProduct(
|
||||
productId: event.productId,
|
||||
count: event.count,
|
||||
productName: event.productName,
|
||||
product: event.product,
|
||||
),
|
||||
];
|
||||
} else {
|
||||
updatedProducts = currentState.selectedProducts.map((p) {
|
||||
if (p.productId == event.productId) {
|
||||
return SelectedProduct(
|
||||
productId: p.productId,
|
||||
count: event.count,
|
||||
productName: p.productName,
|
||||
product: p.product,
|
||||
);
|
||||
}
|
||||
return p;
|
||||
}).toList();
|
||||
}
|
||||
} else {
|
||||
// Remove the product if the count is 0
|
||||
updatedProducts = currentState.selectedProducts
|
||||
.where((p) => p.productId != event.productId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Emit the updated state
|
||||
emit(AddDeviceModelLoaded(
|
||||
selectedProducts: updatedProducts,
|
||||
initialTag: currentState.initialTag));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AddDeviceModelState extends Equatable {
|
||||
const AddDeviceModelState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AddDeviceModelInitial extends AddDeviceModelState {}
|
||||
|
||||
class AddDeviceModelLoading extends AddDeviceModelState {}
|
||||
|
||||
class AddDeviceModelLoaded extends AddDeviceModelState {
|
||||
final List<SelectedProduct> selectedProducts;
|
||||
final List<TagModel> initialTag;
|
||||
|
||||
const AddDeviceModelLoaded({
|
||||
required this.selectedProducts,
|
||||
required this.initialTag,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [selectedProducts, initialTag];
|
||||
}
|
||||
|
||||
class AddDeviceModelError extends AddDeviceModelState {
|
||||
final String errorMessage;
|
||||
|
||||
const AddDeviceModelError(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
|
||||
abstract class AddDeviceTypeModelEvent extends Equatable {
|
||||
const AddDeviceTypeModelEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
|
||||
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
|
||||
final String productId;
|
||||
final int count;
|
||||
final String productName;
|
||||
final ProductModel product;
|
||||
|
||||
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
|
||||
|
||||
@override
|
||||
List<Object> get props => [productId, count];
|
||||
}
|
||||
|
||||
|
||||
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
|
||||
final List<TagModel> initialTags;
|
||||
final List<SelectedProduct> addedProducts;
|
||||
|
||||
const InitializeDeviceTypeModel({
|
||||
this.initialTags = const [],
|
||||
required this.addedProducts,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [initialTags, addedProducts];
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/scrollable_grid_view_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AddDeviceTypeModelWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final List<SelectedProduct>? initialSelectedProducts;
|
||||
final List<SubspaceTemplateModel>? subspaces;
|
||||
final List<TagModel>? spaceTagModels;
|
||||
final List<String>? allTags;
|
||||
final String spaceName;
|
||||
final bool isCreate;
|
||||
final List<String>? otherSpaceModels;
|
||||
final BuildContext? pageContext;
|
||||
final SpaceTemplateModel? spaceModel;
|
||||
|
||||
const AddDeviceTypeModelWidget(
|
||||
{super.key,
|
||||
this.products,
|
||||
this.initialSelectedProducts,
|
||||
this.subspaces,
|
||||
this.allTags,
|
||||
this.spaceTagModels,
|
||||
required this.spaceName,
|
||||
required this.isCreate,
|
||||
this.pageContext,
|
||||
this.otherSpaceModels,
|
||||
this.spaceModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final crossAxisCount = size.width > 1200
|
||||
? 8
|
||||
: size.width > 800
|
||||
? 5
|
||||
: 3;
|
||||
|
||||
return BlocProvider(
|
||||
create: (_) => AddDeviceTypeModelBloc()
|
||||
..add(InitializeDeviceTypeModel(
|
||||
initialTags: spaceTagModels ?? [],
|
||||
addedProducts: initialSelectedProducts ?? [],
|
||||
)),
|
||||
child: Builder(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Add Devices'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
|
||||
builder: (context, state) {
|
||||
if (state is AddDeviceModelLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state is AddDeviceModelLoaded) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.height * 0.65,
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: ScrollableGridViewWidget(
|
||||
isCreate: isCreate,
|
||||
products: products,
|
||||
crossAxisCount: crossAxisCount,
|
||||
initialProductCounts: state.selectedProducts,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CancelButton(
|
||||
label: 'Cancel',
|
||||
onPressed: () async {
|
||||
if (isCreate) {
|
||||
Navigator.of(context).pop();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return CreateSpaceModelDialog(
|
||||
products: products,
|
||||
allTags: allTags,
|
||||
pageContext: pageContext,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
spaceModel: SpaceTemplateModel(
|
||||
modelName: spaceName,
|
||||
tags: spaceModel?.tags ?? [],
|
||||
uuid: spaceModel?.uuid,
|
||||
internalId: spaceModel?.internalId,
|
||||
subspaceModels: subspaces),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final initialTags = generateInitialTags(
|
||||
spaceTagModels: spaceTagModels,
|
||||
subspaces: subspaces,
|
||||
);
|
||||
|
||||
Navigator.of(context).pop();
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AssignTagModelsDialog(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
addedProducts: initialSelectedProducts ?? [],
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
initialTags: initialTags,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
title: 'Edit Device',
|
||||
spaceModel: spaceModel,
|
||||
pageContext: pageContext,
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child:
|
||||
BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
|
||||
builder: (context, state) {
|
||||
final isDisabled = state is AddDeviceModelLoaded &&
|
||||
state.selectedProducts.isEmpty;
|
||||
|
||||
return DefaultButton(
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: isDisabled
|
||||
? ColorsManager.whiteColorsWithOpacity
|
||||
: ColorsManager.whiteColors,
|
||||
borderRadius: 10,
|
||||
onPressed: isDisabled
|
||||
? null // Disable the button
|
||||
: () async {
|
||||
if (state is AddDeviceModelLoaded &&
|
||||
state.selectedProducts.isNotEmpty) {
|
||||
final initialTags = generateInitialTags(
|
||||
spaceTagModels: spaceTagModels,
|
||||
subspaces: subspaces,
|
||||
);
|
||||
|
||||
final dialogTitle = initialTags.isNotEmpty
|
||||
? 'Edit Device'
|
||||
: 'Assign Tags';
|
||||
Navigator.of(context).pop();
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AssignTagModelsDialog(
|
||||
products: products,
|
||||
subspaces: subspaces,
|
||||
addedProducts: state.selectedProducts,
|
||||
allTags: allTags,
|
||||
spaceName: spaceName,
|
||||
initialTags: state.initialTag,
|
||||
otherSpaceModels: otherSpaceModels,
|
||||
title: dialogTitle,
|
||||
spaceModel: spaceModel,
|
||||
pageContext: pageContext,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('Next'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<TagModel> generateInitialTags({
|
||||
List<TagModel>? spaceTagModels,
|
||||
List<SubspaceTemplateModel>? subspaces,
|
||||
}) {
|
||||
final List<TagModel> initialTags = [];
|
||||
|
||||
if (spaceTagModels != null) {
|
||||
initialTags.addAll(spaceTagModels);
|
||||
}
|
||||
|
||||
if (subspaces != null) {
|
||||
for (var subspace in subspaces) {
|
||||
if (subspace.tags != null) {
|
||||
initialTags.addAll(
|
||||
subspace.tags!.map(
|
||||
(tag) => tag.copyWith(location: subspace.subspaceName),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialTags;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
|
||||
class ActionButton extends StatelessWidget {
|
||||
final String label;
|
||||
final Color backgroundColor;
|
||||
final Color foregroundColor;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const ActionButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.backgroundColor,
|
||||
required this.foregroundColor,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 120,
|
||||
child: DefaultButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: foregroundColor,
|
||||
child: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class DeviceIconWidget extends StatelessWidget {
|
||||
final String? icon;
|
||||
|
||||
const DeviceIconWidget({
|
||||
super.key,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 50,
|
||||
width: 50,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
icon ?? Assets.sensors,
|
||||
width: 30,
|
||||
height: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class DeviceNameWidget extends StatelessWidget {
|
||||
final String? name;
|
||||
|
||||
const DeviceNameWidget({
|
||||
super.key,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 35,
|
||||
child: Text(
|
||||
name ?? '',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/counter_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_name_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class DeviceTypeTileWidget extends StatelessWidget {
|
||||
final ProductModel product;
|
||||
final List<SelectedProduct> productCounts;
|
||||
final bool isCreate;
|
||||
|
||||
const DeviceTypeTileWidget(
|
||||
{super.key,
|
||||
required this.product,
|
||||
required this.productCounts,
|
||||
required this.isCreate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedProduct = productCounts.firstWhere(
|
||||
(p) => p.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: product.uuid,
|
||||
count: 0,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
DeviceIconWidget(icon: product.icon ?? Assets.doorSensor),
|
||||
const SizedBox(height: 4),
|
||||
DeviceNameWidget(name: product.name),
|
||||
const SizedBox(height: 4),
|
||||
CounterWidget(
|
||||
isCreate: isCreate,
|
||||
initialCount: selectedProduct.count,
|
||||
onCountChanged: (newCount) {
|
||||
context.read<AddDeviceTypeModelBloc>().add(
|
||||
UpdateProductCountEvent(
|
||||
productId: product.uuid,
|
||||
count: newCount,
|
||||
productName: product.catName,
|
||||
product: product),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/bloc/add_device_model_state.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart';
|
||||
|
||||
class ScrollableGridViewWidget extends StatelessWidget {
|
||||
final List<ProductModel>? products;
|
||||
final int crossAxisCount;
|
||||
final List<SelectedProduct>? initialProductCounts;
|
||||
final bool isCreate;
|
||||
|
||||
const ScrollableGridViewWidget({
|
||||
super.key,
|
||||
required this.products,
|
||||
required this.crossAxisCount,
|
||||
this.initialProductCounts,
|
||||
required this.isCreate
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scrollController = ScrollController();
|
||||
|
||||
return Scrollbar(
|
||||
controller: scrollController,
|
||||
thumbVisibility: true,
|
||||
child: BlocBuilder<AddDeviceTypeModelBloc, AddDeviceModelState>(
|
||||
builder: (context, state) {
|
||||
final productCounts = state is AddDeviceModelLoaded
|
||||
? state.selectedProducts
|
||||
: <SelectedProduct>[];
|
||||
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 6,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: .8,
|
||||
),
|
||||
itemCount: products?.length ?? 0,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products![index];
|
||||
final initialProductCount = _findInitialProductCount(product);
|
||||
|
||||
return DeviceTypeTileWidget(
|
||||
product: product,
|
||||
isCreate: isCreate,
|
||||
productCounts: initialProductCount != null
|
||||
? [...productCounts, initialProductCount]
|
||||
: productCounts,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SelectedProduct? _findInitialProductCount(ProductModel product) {
|
||||
// Check if the product exists in initialProductCounts
|
||||
if (initialProductCounts == null) return null;
|
||||
final matchingProduct = initialProductCounts!.firstWhere(
|
||||
(selectedProduct) => selectedProduct.productId == product.uuid,
|
||||
orElse: () => SelectedProduct(
|
||||
productId: '',
|
||||
count: 0,
|
||||
productName: '',
|
||||
product: null,
|
||||
),
|
||||
);
|
||||
|
||||
// Check if the product was actually found
|
||||
return matchingProduct.productId.isNotEmpty ? matchingProduct : null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user