Merged with dev

This commit is contained in:
Abdullah Alassaf
2025-01-30 12:17:06 +03:00
54 changed files with 2572 additions and 1301 deletions

View File

@ -19,12 +19,14 @@ class DefaultButton extends StatelessWidget {
this.padding,
this.borderColor,
this.elevation,
this.borderWidth = 1.0,
});
final void Function()? onPressed;
final Widget child;
final double? height;
final bool isSecondary;
final double? borderRadius;
final double borderWidth;
final bool enabled;
final double? padding;
final bool isDone;
@ -66,13 +68,16 @@ class DefaultButton extends StatelessWidget {
}),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
side: BorderSide(color: borderColor ?? Colors.transparent),
side: BorderSide(
color: borderColor ?? Colors.transparent,
width: borderWidth,
),
borderRadius: BorderRadius.circular(borderRadius ?? 20),
),
),
fixedSize: height != null
? WidgetStateProperty.all(Size.fromHeight(height!))
: null,
? WidgetStateProperty.all(Size.fromHeight(height!))
: null,
padding: WidgetStateProperty.all(
EdgeInsets.all(padding ?? 10),
),

View File

@ -0,0 +1,47 @@
class DeviceSubspace {
final String uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String subspaceName;
final bool disabled;
DeviceSubspace({
required this.uuid,
this.createdAt,
this.updatedAt,
required this.subspaceName,
required this.disabled,
});
factory DeviceSubspace.fromJson(Map<String, dynamic> json) {
return DeviceSubspace(
uuid: json['uuid'] as String,
createdAt: json['createdAt'] != null
? DateTime.tryParse(json['createdAt'].toString())
: null,
updatedAt: json['updatedAt'] != null
? DateTime.tryParse(json['updatedAt'].toString())
: null,
subspaceName: json['subspaceName'] as String,
disabled: json['disabled'] as bool,
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt?.toIso8601String(),
'updatedAt': updatedAt?.toIso8601String(),
'subspaceName': subspaceName,
'disabled': disabled,
};
}
static List<DeviceSubspace> listFromJson(List<dynamic> jsonList) {
return jsonList.map((json) => DeviceSubspace.fromJson(json)).toList();
}
static List<Map<String, dynamic>> listToJson(List<DeviceSubspace> subspaces) {
return subspaces.map((subspace) => subspace.toJson()).toList();
}
}

View File

@ -1,5 +1,6 @@
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_community.model.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_space_model.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_subspace.model.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart';
@ -47,6 +48,7 @@ class AllDevicesModel {
*/
DevicesModelRoom? room;
DeviceSubspace? subspace;
DevicesModelUnit? unit;
DeviceCommunityModel? community;
String? productUuid;
@ -77,6 +79,7 @@ class AllDevicesModel {
AllDevicesModel({
this.room,
this.subspace,
this.unit,
this.community,
this.productUuid,
@ -110,6 +113,9 @@ class AllDevicesModel {
room = (json['room'] != null && (json['room'] is Map))
? DevicesModelRoom.fromJson(json['room'])
: null;
subspace = (json['subspace'] != null && (json['subspace'] is Map))
? DeviceSubspace.fromJson(json['subspace'])
: null;
unit = (json['unit'] != null && (json['unit'] is Map))
? DevicesModelUnit.fromJson(json['unit'])
: null;
@ -276,6 +282,9 @@ SOS
if (room != null) {
data['room'] = room!.toJson();
}
if (subspace != null) {
data['subspace'] = subspace!.toJson();
}
if (unit != null) {
data['unit'] = unit!.toJson();
}
@ -318,6 +327,7 @@ SOS
return other is AllDevicesModel &&
other.room == room &&
other.subspace == subspace &&
other.unit == unit &&
other.productUuid == productUuid &&
other.productType == productType &&
@ -348,6 +358,7 @@ SOS
@override
int get hashCode {
return room.hashCode ^
subspace.hashCode ^
unit.hashCode ^
productUuid.hashCode ^
productType.hashCode ^

View File

@ -95,8 +95,9 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
]),
TableRow(
children: [
_buildInfoRow('Space Name:', device.unit?.name ?? 'N/A'),
_buildInfoRow('Room:', device.room?.name ?? 'N/A'),
_buildInfoRow('Space Name:',
device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'),
],
),
TableRow(
@ -111,9 +112,13 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
),
_buildInfoRow(
'Battery Level:',
device.batteryLevel != null ? '${device.batteryLevel ?? 0}%' : "-",
device.batteryLevel != null
? '${device.batteryLevel ?? 0}%'
: "-",
statusColor: device.batteryLevel != null
? (device.batteryLevel! < 20 ? ColorsManager.red : ColorsManager.green)
? (device.batteryLevel! < 20
? ColorsManager.red
: ColorsManager.green)
: null,
),
],

View File

@ -1,38 +1,74 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.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) {
class AddDeviceTypeBloc extends Bloc<AddDeviceTypeEvent, AddDeviceState> {
AddDeviceTypeBloc() : super(AddDeviceInitial()) {
on<InitializeDevice>(_onInitializeTagModels);
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 ),
);
void _onInitializeTagModels(
InitializeDevice event, Emitter<AddDeviceState> emit) {
emit(AddDeviceLoaded(
selectedProducts: event.addedProducts,
initialTag: event.initialTags,
));
}
if (event.count > 0) {
if (!state.contains(existingProduct)) {
emit([
...state,
SelectedProduct(productId: event.productId, count: event.count, productName: event.productName, product: event.product)
]);
void _onUpdateProductCount(
UpdateProductCountEvent event, Emitter<AddDeviceState> emit) {
final currentState = state;
if (currentState is AddDeviceLoaded) {
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 {
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);
// Remove the product if the count is 0
updatedProducts = currentState.selectedProducts
.where((p) => p.productId != event.productId)
.toList();
}
} else {
emit(state.where((p) => p.productId != event.productId).toList());
// Emit the updated state
emit(AddDeviceLoaded(
selectedProducts: updatedProducts,
initialTag: currentState.initialTag));
}
}
}

View File

@ -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/all_spaces/model/tag.dart';
abstract class AddDeviceState extends Equatable {
const AddDeviceState();
@override
List<Object> get props => [];
}
class AddDeviceInitial extends AddDeviceState {}
class AddDeviceLoading extends AddDeviceState {}
class AddDeviceLoaded extends AddDeviceState {
final List<SelectedProduct> selectedProducts;
final List<Tag> initialTag;
const AddDeviceLoaded({
required this.selectedProducts,
required this.initialTag,
});
@override
List<Object> get props => [selectedProducts, initialTag];
}
class AddDeviceError extends AddDeviceState {
final String errorMessage;
const AddDeviceError(this.errorMessage);
@override
List<Object> get props => [errorMessage];
}

View File

@ -1,7 +1,11 @@
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/all_spaces/model/tag.dart';
abstract class AddDeviceTypeEvent extends Equatable {
const AddDeviceTypeEvent();
@override
List<Object> get props => [];
}
@ -12,8 +16,26 @@ class UpdateProductCountEvent extends AddDeviceTypeEvent {
final String productName;
final ProductModel product;
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
UpdateProductCountEvent(
{required this.productId,
required this.count,
required this.productName,
required this.product});
@override
List<Object> get props => [productId, count];
}
class InitializeDevice extends AddDeviceTypeEvent {
final List<Tag> initialTags;
final List<SelectedProduct> addedProducts;
const InitializeDevice({
this.initialTags = const [],
required this.addedProducts,
});
@override
List<Object> get props => [initialTags, addedProducts];
}

View File

@ -1,17 +1,19 @@
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/add_device_type/bloc/add_device_model_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/bloc/add_device_state.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/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/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AddDeviceTypeWidget extends StatelessWidget {
final List<ProductModel>? products;
final ValueChanged<List<SelectedProduct>>? onProductsSelected;
@ -20,11 +22,12 @@ class AddDeviceTypeWidget extends StatelessWidget {
final List<Tag>? spaceTags;
final List<String>? allTags;
final String spaceName;
final Function(List<Tag>,List<SubspaceModel>?)? onSave;
final bool isCreate;
final Function(List<Tag>, List<SubspaceModel>?)? onSave;
const AddDeviceTypeWidget(
{super.key,
required this.isCreate,
this.products,
this.initialSelectedProducts,
this.onProductsSelected,
@ -44,30 +47,47 @@ class AddDeviceTypeWidget extends StatelessWidget {
: 3;
return BlocProvider(
create: (_) => AddDeviceTypeBloc(initialSelectedProducts ?? []),
create: (_) => AddDeviceTypeBloc()
..add(InitializeDevice(
initialTags: spaceTags ?? [],
addedProducts: 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),
),
content: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
builder: (context, state) {
if (state is AddDeviceLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is AddDeviceLoaded) {
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(
initialProductCounts: state.selectedProducts,
products: products,
isCreate: isCreate,
crossAxisCount: crossAxisCount),
),
),
],
),
],
),
),
),
),
);
}
return const SizedBox();
}),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -78,72 +98,54 @@ class AddDeviceTypeWidget extends StatelessWidget {
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();
SizedBox(
width: 140,
child: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
builder: (context, state) {
final isDisabled = state is AddDeviceLoaded &&
state.selectedProducts.isEmpty;
return DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: isDisabled
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
onPressed: () async {
if (state is AddDeviceLoaded &&
state.selectedProducts.isNotEmpty) {
final initialTags =
TagHelper.generateInitialForTags(
spaceTags: spaceTags,
subspaces: subspaces,
);
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);
final dialogTitle = initialTags.isNotEmpty
? 'Edit Device'
: 'Assign Tags';
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AssignTagDialog(
products: products,
subspaces: subspaces,
addedProducts: state.selectedProducts,
allTags: allTags,
spaceName: spaceName,
initialTags: initialTags,
title: dialogTitle,
onSave: onSave),
);
}
},
),
);
}
},
),
child: const Text('Next'),
);
},
)),
],
),
],
),
));
}
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;
}
}

View File

@ -13,12 +13,13 @@ 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,
});
const DeviceTypeTileWidget(
{super.key,
required this.product,
required this.productCounts,
required this.isCreate});
@override
Widget build(BuildContext context) {
@ -48,7 +49,7 @@ class DeviceTypeTileWidget extends StatelessWidget {
DeviceNameWidget(name: product.name),
const SizedBox(height: 4),
CounterWidget(
isCreate: false,
isCreate: isCreate,
initialCount: selectedProduct.count,
onCountChanged: (newCount) {
context.read<AddDeviceTypeBloc>().add(

View File

@ -1,6 +1,7 @@
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_state.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';
@ -9,13 +10,14 @@ 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,
});
const ScrollableGridViewWidget(
{super.key,
required this.products,
required this.crossAxisCount,
this.initialProductCounts,
required this.isCreate});
@override
Widget build(BuildContext context) {
@ -24,8 +26,11 @@ class ScrollableGridViewWidget extends StatelessWidget {
return Scrollbar(
controller: scrollController,
thumbVisibility: true,
child: BlocBuilder<AddDeviceTypeBloc, List<SelectedProduct>>(
builder: (context, productCounts) {
child: BlocBuilder<AddDeviceTypeBloc, AddDeviceState>(
builder: (context, state) {
final productCounts = state is AddDeviceLoaded
? state.selectedProducts
: <SelectedProduct>[];
return GridView.builder(
controller: scrollController,
shrinkWrap: true,
@ -42,6 +47,7 @@ class ScrollableGridViewWidget extends StatelessWidget {
return DeviceTypeTileWidget(
product: product,
isCreate: isCreate,
productCounts: initialProductCount != null
? [...productCounts, initialProductCount]
: productCounts,

View File

@ -5,12 +5,15 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_mod
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/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/pages/spaces_management/space_model/models/tag_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_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';
import 'package:syncrow_web/utils/constants/action_enum.dart';
class SpaceManagementBloc
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
@ -341,7 +344,7 @@ class SpaceManagementBloc
products: _cachedProducts ?? [],
selectedCommunity: selectedCommunity,
selectedSpace: selectedSpace,
spaceModels: spaceModels ?? []));
spaceModels: spaceModels));
}
} catch (e) {
emit(SpaceManagementError('Error updating state: $e'));
@ -428,6 +431,76 @@ class SpaceManagementBloc
for (var space in orderedSpaces) {
try {
if (space.uuid != null && space.uuid!.isNotEmpty) {
List<TagModelUpdate> tagUpdates = [];
final prevSpace = await _api.getSpace(communityUuid, space.uuid!);
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
final List<SubspaceModel>? prevSubspaces = prevSpace?.subspaces;
final List<SubspaceModel>? newSubspaces = space.subspaces;
tagUpdates = processTagUpdates(prevSpace?.tags, space.tags);
if (prevSubspaces != null || newSubspaces != null) {
if (prevSubspaces != null && newSubspaces != null) {
for (var prevSubspace in prevSubspaces) {
final existsInNew = newSubspaces
.any((subspace) => subspace.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,
uuid: tag.uuid == '' ? null : tag.uuid,
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));
}
}
}
}
final response = await _api.updateSpace(
communityId: communityUuid,
spaceId: space.uuid!,
@ -436,6 +509,8 @@ class SpaceManagementBloc
isPrivate: space.isPrivate,
position: space.position,
icon: space.icon,
subspaces: subspaceUpdates,
tags: tagUpdates,
direction: space.incomingConnection?.direction,
);
} else {
@ -535,4 +610,79 @@ class SpaceManagementBloc
emit(SpaceManagementError('Error loading communities and spaces: $e'));
}
}
List<TagModelUpdate> processTagUpdates(
List<Tag>? prevTags,
List<Tag>? newTags,
) {
final List<TagModelUpdate> tagUpdates = [];
final processedTags = <String?>{};
if (prevTags == null && newTags != null) {
for (var newTag in newTags) {
tagUpdates.add(TagModelUpdate(
action: Action.add,
tag: newTag.tag,
uuid: newTag.uuid,
productUuid: newTag.product?.uuid,
));
}
return tagUpdates;
}
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) {
final prevTagUuids = prevTags?.map((t) => t.uuid).toSet() ?? {};
for (var newTag in newTags) {
// Tag without UUID
if ((newTag.uuid == null || !prevTagUuids.contains(newTag.uuid)) &&
!processedTags.contains(newTag.tag)) {
tagUpdates.add(TagModelUpdate(
action: Action.add,
tag: newTag.tag,
uuid: newTag.uuid == '' ? null : newTag.uuid,
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;
}
}

View File

@ -0,0 +1,26 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:uuid/uuid.dart';
abstract class BaseTag {
String? uuid;
String? tag;
final ProductModel? product;
String internalId;
String? location;
BaseTag({
this.uuid,
required this.tag,
this.product,
String? internalId,
this.location,
}) : internalId = internalId ?? const Uuid().v4();
Map<String, dynamic> toJson();
BaseTag copyWith({
String? tag,
ProductModel? product,
String? location,
String? internalId,
});
}

View File

@ -66,7 +66,6 @@ class SpaceModel {
final instance = SpaceModel(
internalId: internalId,
uuid: json['uuid'] ?? '',
spaceTuyaUuid: json['spaceTuyaUuid'],
name: json['spaceName'],
isPrivate: json['isPrivate'] ?? false,
invitationCode: json['invitationCode'],

View File

@ -1,5 +1,6 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/utils/constants/action_enum.dart';
import 'package:uuid/uuid.dart';
import 'tag.dart';
@ -8,19 +9,24 @@ class SubspaceModel {
String subspaceName;
final bool disabled;
List<Tag>? tags;
String internalId;
SubspaceModel({
this.uuid,
required this.subspaceName,
required this.disabled,
this.tags,
});
String? internalId,
}) : internalId = internalId ?? const Uuid().v4();
factory SubspaceModel.fromJson(Map<String, dynamic> json) {
final String internalId = json['internalId'] ?? const Uuid().v4();
return SubspaceModel(
uuid: json['uuid'] ?? '',
subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false,
internalId: internalId,
tags: (json['tags'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item))
.toList() ??
@ -43,7 +49,7 @@ class UpdateSubspaceModel {
final Action action;
final String? subspaceName;
final List<UpdateTag>? tags;
UpdateSubspaceModel({
UpdateSubspaceModel({
required this.action,
required this.uuid,
this.subspaceName,
@ -107,4 +113,4 @@ class UpdateTag {
'product': product?.toMap(),
};
}
}
}

View File

@ -1,22 +1,23 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart';
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();
class Tag extends BaseTag {
Tag({
String? uuid,
required String? tag,
ProductModel? product,
String? internalId,
String? location,
}) : super(
uuid: uuid,
tag: tag,
product: product,
internalId: internalId,
location: location,
);
factory Tag.fromJson(Map<String, dynamic> json) {
final String internalId = json['internalId'] ?? const Uuid().v4();
@ -31,15 +32,19 @@ class Tag {
);
}
@override
Tag copyWith({
String? tag,
ProductModel? product,
String? location,
String? internalId,
}) {
return Tag(
uuid: uuid,
tag: tag ?? this.tag,
product: product ?? this.product,
location: location ?? this.location,
internalId: internalId ?? this.internalId,
);
}
@ -60,7 +65,7 @@ extension TagModelExtensions on Tag {
..productUuid = product?.uuid;
}
CreateTagBodyModel toCreateTagBodyModel() {
CreateTagBodyModel toCreateTagBodyModel() {
return CreateTagBodyModel()
..tag = tag ?? ''
..productUuid = product?.uuid;

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.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_header_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeaderActionButtons extends StatelessWidget {
const CommunityStructureHeaderActionButtons(
{super.key,
required this.theme,
required this.isSave,
required this.onSave,
required this.onDelete,
required this.selectedSpace,
required this.onDuplicate,
required this.onEdit});
final ThemeData theme;
final bool isSave;
final VoidCallback onSave;
final VoidCallback onDelete;
final VoidCallback onDuplicate;
final VoidCallback onEdit;
final SpaceModel? selectedSpace;
@override
Widget build(BuildContext context) {
final canShowActions = selectedSpace != null &&
selectedSpace?.status != SpaceStatus.deleted &&
selectedSpace?.status != SpaceStatus.parentDeleted;
return Wrap(
alignment: WrapAlignment.end,
spacing: 10,
children: [
if (isSave)
CommunityStructureHeaderButton(
label: "Save",
icon: const Icon(Icons.save,
size: 18, color: ColorsManager.spaceColor),
onPressed: onSave,
theme: theme,
),
if (canShowActions) ...[
CommunityStructureHeaderButton(
label: "Edit",
svgAsset: Assets.editSpace,
onPressed: onEdit,
theme: theme,
),
CommunityStructureHeaderButton(
label: "Duplicate",
svgAsset: Assets.duplicate,
onPressed: onDuplicate,
theme: theme,
),
CommunityStructureHeaderButton(
label: "Delete",
svgAsset: Assets.spaceDelete,
onPressed: onDelete,
theme: theme,
),
],
],
);
}
}

View File

@ -0,0 +1,65 @@
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/utils/color_manager.dart';
class CommunityStructureHeaderButton extends StatelessWidget {
const CommunityStructureHeaderButton({
super.key,
required this.label,
this.icon,
required this.onPressed,
this.svgAsset,
required this.theme,
});
final String label;
final Widget? icon;
final VoidCallback onPressed;
final String? svgAsset;
final ThemeData theme;
@override
Widget build(BuildContext context) {
const double buttonHeight = 40;
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 130,
minHeight: buttonHeight,
),
child: DefaultButton(
onPressed: onPressed,
borderWidth: 2,
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderRadius: 12.0,
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: ColorsManager.lightGrayColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) icon!,
if (svgAsset != null)
SvgPicture.asset(
svgAsset!,
width: 20,
height: 20,
),
const SizedBox(width: 10),
Flexible(
child: Text(
label,
style: theme.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}

View File

@ -1,8 +1,8 @@
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/all_spaces/widgets/community_structure_header_action_button.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';
@ -14,6 +14,9 @@ class CommunityStructureHeader extends StatefulWidget {
final TextEditingController nameController;
final VoidCallback onSave;
final VoidCallback onDelete;
final VoidCallback onEdit;
final VoidCallback onDuplicate;
final VoidCallback onEditName;
final ValueChanged<String> onNameSubmitted;
final List<CommunityModel> communities;
@ -32,7 +35,9 @@ class CommunityStructureHeader extends StatefulWidget {
required this.onNameSubmitted,
this.community,
required this.communities,
this.selectedSpace});
this.selectedSpace,
required this.onDuplicate,
required this.onEdit});
@override
State<CommunityStructureHeader> createState() =>
@ -141,70 +146,18 @@ class _CommunityStructureHeaderState extends State<CommunityStructureHeader> {
),
),
const SizedBox(width: 8),
_buildActionButtons(theme),
CommunityStructureHeaderActionButtons(
theme: theme,
isSave: widget.isSave,
onSave: widget.onSave,
onDelete: widget.onDelete,
onDuplicate: widget.onDuplicate,
onEdit: widget.onEdit,
selectedSpace: widget.selectedSpace,
),
],
),
],
);
}
Widget _buildActionButtons(ThemeData theme) {
return Wrap(
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: "Delete",
icon: const Icon(Icons.delete,
size: 18, color: ColorsManager.warningRed),
onPressed: widget.onDelete,
theme: theme),
],
);
}
Widget _buildButton(
{required String label,
required Widget icon,
required VoidCallback onPressed,
required ThemeData theme}) {
const double buttonHeight = 30;
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: 80, minHeight: buttonHeight),
child: DefaultButton(
onPressed: onPressed,
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderRadius: 8.0,
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: ColorsManager.lightGrayColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
icon,
const SizedBox(width: 5),
Flexible(
child: Text(
label,
style: theme.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}

View File

@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
// Syncrow project imports
import 'package:syncrow_web/pages/common/buttons/add_space_button.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/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/model/product_model.dart';
@ -17,6 +19,7 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/blank_com
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/dialogs/duplicate_process_dialog.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';
@ -133,6 +136,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
onSave: _saveSpaces,
selectedSpace: widget.selectedSpace,
onDelete: _onDelete,
onDuplicate: () => {_onDuplicate(context)},
onEdit: () => {_showEditSpaceDialog()},
onEditName: () {
setState(() {
isEditingName = !isEditingName;
@ -190,6 +195,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
screenSize,
position:
spaces[index].position + newPosition,
parentIndex: index,
direction: direction,
);
@ -209,9 +215,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
opacity: isHighlighted ? 1.0 : 0.3,
child: SpaceContainerWidget(
index: index,
onDoubleTap: () {
_showEditSpaceDialog(spaces[index]);
},
onTap: () {
_selectSpace(context, spaces[index]);
},
@ -292,6 +295,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
allTags: _getAllTagValues(spaces),
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
onCreateSpace: (String name,
String icon,
@ -328,7 +332,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
parentSpace.addOutgoingConnection(newConnection);
parentSpace.children.add(newSpace);
}
spaces.add(newSpace);
_updateNodePosition(newSpace, newSpace.position);
});
@ -338,39 +341,46 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
);
}
void _showEditSpaceDialog(SpaceModel space) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSpaceDialog(
products: widget.products,
name: space.name,
icon: space.icon,
editSpace: space,
isEdit: true,
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.spaceModel = spaceModel;
space.subspaces = subspaces;
space.tags = tags;
void _showEditSpaceDialog() {
if (widget.selectedSpace != null) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
name: widget.selectedSpace!.name,
icon: widget.selectedSpace!.icon,
editSpace: widget.selectedSpace,
tags: widget.selectedSpace?.tags,
subspaces: widget.selectedSpace?.subspaces,
isEdit: true,
allTags: _getAllTagValues(spaces),
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Update the space's properties
widget.selectedSpace!.name = name;
widget.selectedSpace!.icon = icon;
widget.selectedSpace!.spaceModel = spaceModel;
widget.selectedSpace!.subspaces = subspaces;
widget.selectedSpace!.tags = tags;
if (space.status != SpaceStatus.newSpace) {
space.status = SpaceStatus.modified; // Mark as modified
}
});
},
key: Key(space.name),
);
},
);
if (widget.selectedSpace!.status != SpaceStatus.newSpace) {
widget.selectedSpace!.status =
SpaceStatus.modified; // Mark as modified
}
});
},
key: Key(widget.selectedSpace!.name),
);
},
);
}
}
void _handleHoverChanged(int index, bool isHovered) {
@ -463,7 +473,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_markChildrenAsDeleted(space);
}
}
_removeConnectionsForDeletedSpaces();
});
}
@ -545,4 +555,206 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
space.status == SpaceStatus.modified ||
space.status == SpaceStatus.deleted);
}
void _onDuplicate(BuildContext parentContext) {
final screenWidth = MediaQuery.of(context).size.width;
if (widget.selectedSpace != null) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
title: Text(
"Duplicate Space",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
content: SizedBox(
width: screenWidth * 0.4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Are you sure you want to duplicate?",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 15),
Text("All the child spaces will be duplicated.",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: ColorsManager.lightGrayColor)),
const SizedBox(width: 15),
],
),
),
actions: [
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
SizedBox(
width: 200,
child: CancelButton(
onPressed: () {
Navigator.of(context).pop();
},
label: "Cancel",
),
),
const SizedBox(width: 10),
SizedBox(
width: 200,
child: DefaultButton(
onPressed: () {
Navigator.of(context).pop();
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return DuplicateProcessDialog(
onDuplicate: () {
_duplicateSpace(widget.selectedSpace!);
_deselectSpace(parentContext);
},
);
},
);
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
])
],
);
},
);
}
}
void _duplicateSpace(SpaceModel space) {
final Map<SpaceModel, SpaceModel> originalToDuplicate = {};
const double horizontalGap = 200.0;
const double verticalGap = 100.0;
final Map<String, int> nameCounters = {};
String _generateCopyName(String originalName) {
final baseName = originalName.replaceAll(RegExp(r'\(\d+\)$'), '').trim();
nameCounters[baseName] = (nameCounters[baseName] ?? 0) + 1;
return "$baseName(${nameCounters[baseName]})";
}
SpaceModel duplicateRecursive(SpaceModel original, Offset parentPosition,
SpaceModel? duplicatedParent) {
Offset newPosition = parentPosition + Offset(horizontalGap, 0);
while (spaces.any((s) =>
(s.position - newPosition).distance < horizontalGap &&
s.status != SpaceStatus.deleted)) {
newPosition += Offset(horizontalGap, 0);
}
final duplicatedName = _generateCopyName(original.name);
final duplicated = SpaceModel(
name: duplicatedName,
icon: original.icon,
position: newPosition,
isPrivate: original.isPrivate,
children: [],
status: SpaceStatus.newSpace,
parent: duplicatedParent,
spaceModel: original.spaceModel,
subspaces: original.subspaces,
tags: original.tags,
);
originalToDuplicate[original] = duplicated;
setState(() {
spaces.add(duplicated);
_updateNodePosition(duplicated, duplicated.position);
if (duplicatedParent != null) {
final newConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection);
}
if (original.parent != null && duplicatedParent == null) {
final originalParent = original.parent!;
final duplicatedParent =
originalToDuplicate[originalParent] ?? originalParent;
final parentConnection = Connection(
startSpace: duplicatedParent,
endSpace: duplicated,
direction: original.incomingConnection?.direction ?? "down",
);
connections.add(parentConnection);
duplicated.incomingConnection = parentConnection;
duplicatedParent.addOutgoingConnection(parentConnection);
}
});
final childrenWithDownDirection = original.children
.where((child) =>
child.incomingConnection?.direction == "down" &&
child.status != SpaceStatus.deleted)
.toList();
Offset childStartPosition = childrenWithDownDirection.length == 1
? duplicated.position
: newPosition + Offset(0, verticalGap);
for (final child in original.children) {
final isDownDirection =
child.incomingConnection?.direction == "down" ?? false;
if (isDownDirection && childrenWithDownDirection.length == 1) {
// Place the only "down" child vertically aligned with the parent
childStartPosition = duplicated.position + Offset(0, verticalGap);
} else if (!isDownDirection) {
// Position children with other directions horizontally
childStartPosition = duplicated.position + Offset(horizontalGap, 0);
}
final duplicatedChild =
duplicateRecursive(child, childStartPosition, duplicated);
duplicated.children.add(duplicatedChild);
childStartPosition += Offset(0, verticalGap);
}
return duplicated;
}
if (space.parent == null) {
duplicateRecursive(space, space.position, null);
} else {
final duplicatedParent =
originalToDuplicate[space.parent!] ?? space.parent!;
duplicateRecursive(space, space.position, duplicatedParent);
}
}
List<String> _getAllTagValues(List<SpaceModel> spaces) {
final List<String> allTags = [];
for (final space in spaces) {
if (space.tags != null) {
allTags.addAll(space.listAllTagValues());
}
}
return allTags;
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/common/edit_chip.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';
@ -9,16 +10,25 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model
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/assign_tag/views/assign_tag_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.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/pages/spaces_management/space_model/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.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,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) 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;
@ -29,6 +39,7 @@ class CreateSpaceDialog extends StatefulWidget {
final List<SpaceTemplateModel>? spaceModels;
final List<SubspaceModel>? subspaces;
final List<Tag>? tags;
final List<String>? allTags;
const CreateSpaceDialog(
{super.key,
@ -39,6 +50,7 @@ class CreateSpaceDialog extends StatefulWidget {
this.icon,
this.isEdit = false,
this.editSpace,
this.allTags,
this.selectedProducts = const [],
this.spaceModels,
this.subspaces,
@ -69,6 +81,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
tags = widget.tags ?? [];
subspaces = widget.subspaces ?? [];
}
@override
@ -139,50 +153,52 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameController,
onChanged: (value) {
enteredName = value.trim();
setState(() {
isNameFieldExist = false;
isOkButtonEnabled = false;
isNameFieldInvalid = value.isEmpty;
SizedBox(
width: screenWidth * 0.25,
child: 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;
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,
});
},
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.lightGrayColor),
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,
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
),
),
),
),
@ -211,46 +227,20 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
),
const SizedBox(height: 10),
selectedSpaceModel == null
? DefaultButton(
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
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,
),
),
],
),
child: const ButtonContentWidget(
svgAssets: Assets.link,
label: 'Link a space model',
),
)
: Container(
width: screenWidth * 0.35,
width: screenWidth * 0.25,
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
@ -264,8 +254,11 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
Chip(
label: Text(
selectedSpaceModel?.modelName ?? '',
style: const TextStyle(
color: ColorsManager.spaceColor),
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
@ -298,25 +291,25 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
),
),
const SizedBox(height: 25),
const Row(
Row(
children: [
Expanded(
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Text(
'OR',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
Expanded(
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
@ -325,48 +318,23 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
],
),
const SizedBox(height: 25),
subspaces == null
? DefaultButton(
onPressed: () {
subspaces == null || subspaces!.isEmpty
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () async {
_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,
),
),
],
),
child: const ButtonContentWidget(
icon: Icons.add,
label: 'Create Sub Space',
),
)
: SizedBox(
width: screenWidth * 0.35,
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
@ -382,50 +350,46 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
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(
...subspaces!.map((subspace) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SubspaceNameDisplayWidget(
text: subspace.subspaceName,
validateName: (updatedName) {
bool nameExists =
subspaces!.any((s) {
bool isSameId = s.internalId ==
subspace.internalId;
bool isSameName = s.subspaceName
.trim()
.toLowerCase() ==
updatedName
.trim()
.toLowerCase();
return !isSameId && isSameName;
});
return !nameExists;
},
onNameChanged: (updatedName) {
setState(() {
subspace.subspaceName =
updatedName;
});
},
),
],
);
}),
EditChip(
onTap: () async {
_showSubSpaceDialog(
context,
enteredName,
[],
false,
widget.products,
subspaces);
_showSubSpaceDialog(context, enteredName,
[], true, 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),
),
),
),
)
],
),
),
@ -452,7 +416,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
..._groupTags([
...TagHelper.groupTags([
...?tags,
...?subspaces?.expand(
(subspace) => subspace.tags ?? [])
@ -469,9 +433,12 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
),
label: Text(
'x${entry.value}', // Show count
style: const TextStyle(
color: ColorsManager.spaceColor,
),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: ColorsManager
.spaceColor),
),
backgroundColor:
ColorsManager.whiteColors,
@ -484,70 +451,53 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
),
),
),
GestureDetector(
onTap: () async {
_showTagCreateDialog(context, enteredName,
widget.products);
// Edit action
},
child: Chip(
label: const Text(
'Edit',
style: TextStyle(
color: ColorsManager.spaceColor),
EditChip(onTap: () async {
final result = await showDialog(
context: context,
builder: (context) => AssignTagDialog(
products: widget.products,
subspaces: widget.subspaces,
addedProducts: TagHelper
.createInitialSelectedProductsForTags(
tags ?? [], subspaces),
title: 'Edit Device',
initialTags:
TagHelper.generateInitialForTags(
spaceTags: tags,
subspaces: subspaces),
spaceName: widget.name ?? '',
onSave:
(updatedTags, updatedSubspaces) {
setState(() {
tags = updatedTags;
subspaces = updatedSubspaces;
});
},
),
backgroundColor:
ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor),
),
),
),
);
})
],
),
),
)
: DefaultButton(
: TextButton(
onPressed: () {
_showTagCreateDialog(
context, enteredName, widget.products);
context,
enteredName,
widget.isEdit,
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(
'Add devices',
overflow: TextOverflow
.ellipsis, // Prevent overflow
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
],
),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
)
child: const ButtonContentWidget(
icon: Icons.add,
label: 'Add Devices',
))
],
),
),
@ -579,8 +529,13 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) {
widget.onCreateSpace(newName, selectedIcon,
selectedProducts, selectedSpaceModel,subspaces,tags);
widget.onCreateSpace(
newName,
selectedIcon,
selectedProducts,
selectedSpaceModel,
subspaces,
tags);
Navigator.of(context).pop();
}
}
@ -635,6 +590,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
setState(() {
selectedSpaceModel = selectedModel;
subspaces = null;
tags = null;
});
}
},
@ -655,7 +611,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
builder: (BuildContext context) {
return CreateSubSpaceDialog(
spaceName: name,
dialogTitle: 'Create Sub-space',
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space',
spaceTags: spaceTags,
isEdit: isEdit,
products: products,
@ -672,85 +628,76 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
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;
void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
List<ProductModel>? products, List<SubspaceModel>? subspaces) {
isEdit
? showDialog(
context: context,
builder: (BuildContext context) {
return AssignTagDialog(
title: 'Edit Device',
addedProducts: TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
spaceName: name,
products: products,
subspaces: subspaces,
allTags: widget.allTags,
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;
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;
}
}
}
}
}
}
}
}
});
},
);
},
);
}
});
},
);
},
)
: showDialog(
context: context,
builder: (BuildContext context) {
return AddDeviceTypeWidget(
spaceName: name,
products: products,
subspaces: subspaces,
spaceTags: tags,
isCreate: true,
allTags: widget.allTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() {
tags = selectedSpaceTags;
selectedSpaceModel = null;
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;
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;
}
}
}
}
}
});
},
);
},
);
}
}

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class DuplicateProcessDialog extends StatefulWidget {
final VoidCallback onDuplicate;
const DuplicateProcessDialog({required this.onDuplicate, Key? key})
: super(key: key);
@override
_DuplicateProcessDialogState createState() => _DuplicateProcessDialogState();
}
class _DuplicateProcessDialogState extends State<DuplicateProcessDialog> {
bool isDuplicating = true;
@override
void initState() {
super.initState();
_startDuplication();
}
void _startDuplication() async {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onDuplicate();
});
await Future.delayed(const Duration(seconds: 2));
setState(() {
isDuplicating = false;
});
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
content: SizedBox(
width: screenWidth * 0.4,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isDuplicating) ...[
const CircularProgressIndicator(
color: ColorsManager.vividBlue,
),
const SizedBox(height: 15),
Text(
"Duplicating in progress",
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: ColorsManager.primaryColor),
textAlign: TextAlign.center,
),
] else ...[
const Icon(
Icons.check_circle,
color: ColorsManager.vividBlue,
size: 50,
),
const SizedBox(height: 15),
Text(
"Duplicating successful",
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(color: ColorsManager.primaryColor),
),
],
],
),
),
);
}
}

View File

@ -196,8 +196,12 @@ class _SidebarWidgetState extends State<SidebarWidget> {
_handleExpansionChange(community.uuid, expanded);
},
children: hasChildren
? community.spaces.map((space) => _buildSpaceTile(space, community)).toList()
: null, // Render spaces within the community
? community.spaces
.where((space) => (space.status != SpaceStatus.deleted ||
space.status != SpaceStatus.parentDeleted))
.map((space) => _buildSpaceTile(space, community))
.toList()
: null,
);
}

View File

@ -20,8 +20,7 @@ class SpaceWidget extends StatelessWidget {
top: position.dy,
child: GestureDetector(
onTap: onTap,
child:
Container(
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
@ -39,11 +38,10 @@ class SpaceWidget extends StatelessWidget {
children: [
const Icon(Icons.location_on, color: ColorsManager.spaceColor),
const SizedBox(width: 8),
Text(name, style: const TextStyle(fontSize: 16)),
Text(name, style: Theme.of(context).textTheme.bodyMedium),
],
),
),
),
);
}

View File

@ -46,9 +46,9 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
}
emit(AssignTagLoaded(
tags: allTags,
isSaveEnabled: _validateTags(allTags),
));
tags: allTags,
isSaveEnabled: _validateTags(allTags),
errorMessage: ''));
});
on<UpdateTagEvent>((event, emit) {
@ -117,12 +117,10 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
}
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;
final isValid = uniqueTags.length == tags.length && !hasEmptyTag;
return isValid;
}
String? _getValidationError(List<Tag> tags) {

View File

@ -1,7 +1,10 @@
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/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/subspace_model.dart';
@ -9,6 +12,7 @@ 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/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AssignTagDialog extends StatelessWidget {
@ -37,8 +41,11 @@ class AssignTagDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList();
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider(
create: (_) => AssignTagBloc()
..add(InitializeTags(
@ -79,6 +86,7 @@ class AssignTagDialog extends StatelessWidget {
style:
Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label: Text('Tag',
style:
Theme.of(context).textTheme.bodyMedium)),
@ -89,30 +97,32 @@ class AssignTagDialog extends StatelessWidget {
],
rows: state.tags.isEmpty
? [
const DataRow(cells: [
DataRow(cells: [
DataCell(
Center(
child: Text(
'No Data Available',
style: TextStyle(
fontSize: 14,
color: ColorsManager.lightGrayColor,
),
),
child: Text('No Data Available',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager
.lightGrayColor,
)),
),
),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const 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.toString())),
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment:
@ -123,147 +133,80 @@ class AssignTagDialog extends StatelessWidget {
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,
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,
),
),
),
SizedBox(
width: MediaQuery.of(context)
.size
.width *
0.15,
child: PopupMenuButton<String>(
color: ColorsManager.whiteColors,
child: IconButton(
icon: const Icon(
Icons.arrow_drop_down,
color:
ColorsManager.blackColor),
onSelected: (value) {
controller.text = value;
Icons.close,
color: ColorsManager
.lightGreyColor,
size: 16,
),
onPressed: () {
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();
.add(DeleteTag(
tagToDelete: tag,
tags: state.tags));
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(),
),
),
],
),
),
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) {
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<AssignTagBloc>()
.add(UpdateTagEvent(
index: index,
tag: value.trim(),
));
},
),
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue:
tag.location ?? 'Main Space',
onSelected: (value) {
context
.read<AssignTagBloc>()
.add(UpdateLocation(
index: index,
location: value,
));
}
},
),
),
},
)),
),
],
);
@ -271,10 +214,11 @@ class AssignTagDialog extends StatelessWidget {
),
),
if (state.errorMessage != null)
Text(
state.errorMessage!,
style: const TextStyle(color: ColorsManager.warningRed),
),
Text(state.errorMessage!,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.warningRed)),
],
),
),
@ -284,11 +228,35 @@ class AssignTagDialog extends StatelessWidget {
children: [
const SizedBox(width: 10),
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(state.tags);
final result = processTags(updatedTags, subspaces);
final processedTags =
result['updatedTags'] as List<Tag>;
final processedSubspaces = result['subspaces'];
Navigator.of(context).pop();
await showDialog(
context: context,
builder: (context) => AddDeviceTypeWidget(
products: products,
subspaces: processedSubspaces,
initialSelectedProducts: TagHelper
.createInitialSelectedProductsForTags(
processedTags, processedSubspaces),
spaceName: spaceName,
spaceTags: processedTags,
isCreate: false,
onSave: onSave,
),
);
},
),
),
),
const SizedBox(width: 10),
@ -301,23 +269,16 @@ class AssignTagDialog extends StatelessWidget {
foregroundColor: ColorsManager.whiteColors,
onPressed: state.isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(state.tags);
final result =
processTags(updatedTags, subspaces);
final processedTags =
result['updatedTags'] as List<Tag>;
final processedSubspaces =
result['subspaces'] as List<SubspaceModel>;
onSave?.call(processedTags, processedSubspaces);
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'),
@ -337,4 +298,118 @@ class AssignTagDialog extends StatelessWidget {
),
);
}
List<String> getAvailableTags(
List<String> allTags, List<Tag> currentTags, Tag currentTag) {
return allTags
.where((tagValue) => !currentTags
.where((e) => e != currentTag) // Exclude the current row
.map((e) => e.tag)
.contains(tagValue))
.toList();
}
Map<String, dynamic> processTags(
List<Tag> updatedTags, List<SubspaceModel>? subspaces) {
final modifiedTags = List<Tag>.from(updatedTags);
final modifiedSubspaces = List<SubspaceModel>.from(subspaces ?? []);
if (subspaces != null) {
for (var subspace in subspaces) {
subspace.tags?.removeWhere(
(tag) => !modifiedTags
.any((updatedTag) => updatedTag.internalId == tag.internalId),
);
}
}
for (var tag in modifiedTags.toList()) {
if (modifiedSubspaces.isEmpty) continue;
final prevIndice = checkTagExistInSubspace(tag, modifiedSubspaces);
if ((tag.location == 'Main Space' || tag.location == null) &&
(prevIndice == null ||
modifiedSubspaces[prevIndice].subspaceName == 'Main Space')) {
continue;
}
if ((tag.location == 'Main Space' || tag.location == null) &&
prevIndice != null) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
prevIndice == null) {
final newIndex = modifiedSubspaces
.indexWhere((subspace) => subspace.subspaceName == tag.location);
if (newIndex != -1) {
if (modifiedSubspaces[newIndex]
.tags
?.any((t) => t.internalId == tag.internalId) !=
true) {
tag.location = modifiedSubspaces[newIndex].subspaceName;
modifiedSubspaces[newIndex].tags?.add(tag);
}
}
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
tag.location != modifiedSubspaces[prevIndice!].subspaceName) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
final newIndex = modifiedSubspaces
.indexWhere((subspace) => subspace.subspaceName == tag.location);
if (newIndex != -1) {
if (modifiedSubspaces[newIndex]
.tags
?.any((t) => t.internalId == tag.internalId) !=
true) {
tag.location = modifiedSubspaces[newIndex].subspaceName;
modifiedSubspaces[newIndex].tags?.add(tag);
}
}
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
tag.location == modifiedSubspaces[prevIndice!].subspaceName) {
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location == 'Main Space' || tag.location == null) &&
prevIndice != null) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
}
}
return {
'updatedTags': modifiedTags,
'subspaces': modifiedSubspaces,
};
}
int? checkTagExistInSubspace(Tag tag, List<SubspaceModel>? subspaces) {
if (subspaces == null) return null;
for (int i = 0; i < subspaces.length; i++) {
final subspace = subspaces[i];
if (subspace.tags == null) continue;
for (var t in subspace.tags!) {
if (tag.internalId == t.internalId) {
return i;
}
}
}
return null;
}
}

View File

@ -15,6 +15,7 @@ import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model
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';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
class AssignTagModelsDialog extends StatelessWidget {
final List<ProductModel>? products;
@ -29,6 +30,7 @@ class AssignTagModelsDialog extends StatelessWidget {
final String title;
final BuildContext? pageContext;
final List<String>? otherSpaceModels;
final List<SpaceTemplateModel>? allSpaceModels;
const AssignTagModelsDialog(
{Key? key,
@ -42,7 +44,8 @@ class AssignTagModelsDialog extends StatelessWidget {
required this.title,
this.pageContext,
this.otherSpaceModels,
this.spaceModel})
this.spaceModel,
this.allSpaceModels})
: super(key: key);
@override
@ -109,22 +112,22 @@ class AssignTagModelsDialog extends StatelessWidget {
],
rows: state.tags.isEmpty
? [
const DataRow(cells: [
DataRow(cells: [
DataCell(
Center(
child: Text(
'No Data Available',
style: TextStyle(
fontSize: 14,
color:
ColorsManager.lightGrayColor,
),
),
child: Text('No Devices Available',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager
.lightGrayColor,
)),
),
),
DataCell(SizedBox()),
DataCell(SizedBox()),
DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
@ -211,8 +214,8 @@ class AssignTagModelsDialog extends StatelessWidget {
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue:
tag.location ?? 'Main Space',
selectedValue: tag.location ??
'Main Space',
onSelected: (value) {
context
.read<
@ -230,11 +233,11 @@ class AssignTagModelsDialog extends StatelessWidget {
),
),
if (state.errorMessage != null)
Text(
state.errorMessage!,
style: const TextStyle(
color: ColorsManager.warningRed),
),
Text(state.errorMessage!,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.warningRed)),
],
),
),
@ -248,33 +251,14 @@ class AssignTagModelsDialog extends StatelessWidget {
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
for (var tag in state.tags) {
if (tag.location == null ||
subspaces == null) {
continue;
}
final updatedTags =
List<TagModel>.from(state.tags);
final result =
processTags(updatedTags, subspaces);
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);
}
}
final processedTags =
result['updatedTags'] as List<TagModel>;
final processedSubspaces = result['subspaces'];
if (context.mounted) {
Navigator.of(context).pop();
@ -284,22 +268,25 @@ class AssignTagModelsDialog extends StatelessWidget {
builder: (dialogContext) =>
AddDeviceTypeModelWidget(
products: products,
subspaces: subspaces,
subspaces: processedSubspaces,
isCreate: false,
initialSelectedProducts:
addedProducts,
initialSelectedProducts: TagHelper
.createInitialSelectedProducts(
processedTags,
processedSubspaces),
allTags: allTags,
spaceName: spaceName,
otherSpaceModels: otherSpaceModels,
spaceTagModels: state.tags,
spaceTagModels: processedTags,
pageContext: pageContext,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: state.tags,
tags: updatedTags,
uuid: spaceModel?.uuid,
internalId:
spaceModel?.internalId,
subspaceModels: subspaces)),
subspaceModels:
processedSubspaces)),
);
}
},
@ -316,33 +303,17 @@ class AssignTagModelsDialog extends StatelessWidget {
foregroundColor: ColorsManager.whiteColors,
onPressed: state.isSaveEnabled
? () async {
for (var tag in state.tags) {
if (tag.location == null ||
subspaces == null) {
continue;
}
final updatedTags =
List<TagModel>.from(state.tags);
final result =
processTags(updatedTags, subspaces);
final previousTagSubspace =
checkTagExistInSubspace(
tag, subspaces ?? []);
final processedTags =
result['updatedTags'] as List<TagModel>;
final processedSubspaces =
result['subspaces']
as List<SubspaceTemplateModel>;
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);
@ -351,16 +322,18 @@ class AssignTagModelsDialog extends StatelessWidget {
builder: (BuildContext dialogContext) {
return CreateSpaceModelDialog(
products: products,
allSpaceModels: allSpaceModels,
allTags: allTags,
pageContext: pageContext,
otherSpaceModels: otherSpaceModels,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: state.tags,
tags: processedTags,
uuid: spaceModel?.uuid,
internalId:
spaceModel?.internalId,
subspaceModels: subspaces),
subspaceModels:
processedSubspaces),
);
},
);
@ -394,39 +367,109 @@ class AssignTagModelsDialog extends StatelessWidget {
.toList();
}
void removeTagFromSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
subspace?.tags?.removeWhere((t) => t.internalId == tag.internalId);
}
SubspaceTemplateModel? checkTagExistInSubspace(
int? checkTagExistInSubspace(
TagModel tag, List<SubspaceTemplateModel>? subspaces) {
if (subspaces == null) return null;
for (var subspace in subspaces) {
if (subspace.tags == null) return null;
for (int i = 0; i < subspaces.length; i++) {
final subspace = subspaces[i];
if (subspace.tags == null) continue;
for (var t in subspace.tags!) {
if (tag.internalId == t.internalId) return subspace;
if (tag.internalId == t.internalId) {
return i;
}
}
}
return null;
}
void moveToNewSubspace(TagModel tag, List<SubspaceTemplateModel> subspaces) {
final targetSubspace = subspaces
.firstWhere((subspace) => subspace.subspaceName == tag.location);
Map<String, dynamic> processTags(
List<TagModel> updatedTags, List<SubspaceTemplateModel>? subspaces) {
final modifiedTags = List<TagModel>.from(updatedTags);
final modifiedSubspaces = List<SubspaceTemplateModel>.from(subspaces ?? []);
targetSubspace.tags ??= [];
if (targetSubspace.tags?.any((t) => t.internalId == tag.internalId) !=
true) {
targetSubspace.tags?.add(tag);
if (subspaces != null) {
for (var subspace in subspaces) {
subspace.tags?.removeWhere(
(tag) => !modifiedTags
.any((updatedTag) => updatedTag.internalId == tag.internalId),
);
}
}
}
void updateTagInSubspace(TagModel tag, SubspaceTemplateModel? subspace) {
final currentTag = subspace?.tags?.firstWhere(
(t) => t.internalId == tag.internalId,
);
if (currentTag != null) {
currentTag.tag = tag.tag;
for (var tag in modifiedTags.toList()) {
if (modifiedSubspaces.isEmpty) continue;
final prevIndice = checkTagExistInSubspace(tag, modifiedSubspaces);
if ((tag.location == 'Main Space' || tag.location == null) &&
(prevIndice == null ||
modifiedSubspaces[prevIndice].subspaceName == 'Main Space')) {
continue;
}
if ((tag.location == 'Main Space' || tag.location == null) &&
prevIndice != null) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
prevIndice == null) {
final newIndex = modifiedSubspaces
.indexWhere((subspace) => subspace.subspaceName == tag.location);
if (newIndex != -1) {
if (modifiedSubspaces[newIndex]
.tags
?.any((t) => t.internalId == tag.internalId) !=
true) {
tag.location = modifiedSubspaces[newIndex].subspaceName;
modifiedSubspaces[newIndex].tags?.add(tag);
}
}
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
tag.location != modifiedSubspaces[prevIndice!].subspaceName) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
final newIndex = modifiedSubspaces
.indexWhere((subspace) => subspace.subspaceName == tag.location);
if (newIndex != -1) {
if (modifiedSubspaces[newIndex]
.tags
?.any((t) => t.internalId == tag.internalId) !=
true) {
tag.location = modifiedSubspaces[newIndex].subspaceName;
modifiedSubspaces[newIndex].tags?.add(tag);
}
}
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location != 'Main Space' && tag.location != null) &&
tag.location == modifiedSubspaces[prevIndice!].subspaceName) {
modifiedTags.removeWhere((t) => t.internalId == tag.internalId);
continue;
}
if ((tag.location == 'Main Space' || tag.location == null) &&
prevIndice != null) {
modifiedSubspaces[prevIndice]
.tags
?.removeWhere((t) => t.internalId == tag.internalId);
}
}
return {
'updatedTags': modifiedTags,
'subspaces': modifiedSubspaces,
};
}
}

View File

@ -77,9 +77,7 @@ class CreateCommunityDialog extends StatelessWidget {
.read<CommunityDialogBloc>()
.add(ValidateCommunityNameEvent(value));
},
style: const TextStyle(
color: ColorsManager.blackColor,
),
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the community name',
filled: true,

View File

@ -6,26 +6,42 @@ import 'subspace_event.dart';
import 'subspace_state.dart';
class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
SubSpaceBloc() : super(SubSpaceState([], [], '')) {
SubSpaceBloc() : super(SubSpaceState([], [], '', {})) {
on<AddSubSpace>((event, emit) {
final existingNames =
state.subSpaces.map((e) => e.subspaceName).toSet();
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 updatedDuplicates = Set<String>.from(state.duplicates)
..add(event.subSpace.subspaceName.toLowerCase());
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
..add(event.subSpace);
emit(SubSpaceState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
'*Duplicated sub-space name',
updatedDuplicates,
));
} else {
// Add subspace if no duplicate exists
final updatedSubSpaces = List<SubspaceModel>.from(state.subSpaces)
..add(event.subSpace);
if (state.duplicates.isNotEmpty) {
emit(SubSpaceState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'*Duplicated sub-space name',
state.duplicates,
));
} else {
emit(SubSpaceState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
state.duplicates,
// Clear error message
));
}
}
});
@ -45,13 +61,50 @@ class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
));
}
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();
final errorMessage =
updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : '';
emit(SubSpaceState(
updatedSubSpaces,
updatedSubspaceModels,
'', // Clear error message
errorMessage,
updatedDuplicates,
));
});
// Handle UpdateSubSpace Event
on<UpdateSubSpace>((event, emit) {
final updatedSubSpaces = state.subSpaces.map((subSpace) {
if (subSpace.uuid == event.updatedSubSpace.uuid) {
return event.updatedSubSpace;
}
return subSpace;
}).toList();
final updatedSubspaceModels = List<UpdateSubspaceModel>.from(
state.updatedSubSpaceModels,
);
updatedSubspaceModels.add(UpdateSubspaceModel(
action: Action.update,
uuid: event.updatedSubSpace.uuid!,
));
emit(SubSpaceState(
updatedSubSpaces,
updatedSubspaceModels,
'',
state.duplicates,
));
});
}
}

View File

@ -4,23 +4,26 @@ class SubSpaceState {
final List<SubspaceModel> subSpaces;
final List<UpdateSubspaceModel> updatedSubSpaceModels;
final String errorMessage;
final Set<String> duplicates;
SubSpaceState(
this.subSpaces,
this.updatedSubSpaceModels,
this.errorMessage,
this.duplicates,
);
SubSpaceState copyWith({
List<SubspaceModel>? subSpaces,
List<UpdateSubspaceModel>? updatedSubSpaceModels,
String? errorMessage,
Set<String>? duplicates,
}) {
return SubSpaceState(
subSpaces ?? this.subSpaces,
updatedSubSpaceModels ?? this.updatedSubSpaceModels,
errorMessage ?? this.errorMessage,
duplicates ?? this.duplicates,
);
}
}

View File

@ -81,104 +81,136 @@ class CreateSubSpaceDialog extends StatelessWidget {
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,
...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: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color:
ColorsManager.spaceColor)),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDuplicate
? ColorsManager.red
: ColorsManager.transparentColor,
width: 0,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
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)),
),
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),
),
controller: textController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: state.subSpaces.isEmpty
? 'Please enter the name'
: null,
hintStyle: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: ColorsManager
.lightGrayColor)),
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
context.read<SubSpaceBloc>().add(
AddSubSpace(SubspaceModel(
subspaceName: value.trim(),
disabled: false)));
textController.clear();
}
},
style:
Theme.of(context).textTheme.bodyMedium),
),
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
state.errorMessage,
style: const TextStyle(
color: ColorsManager.warningRed,
fontSize: 12,
),
),
),
],
),
),
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(state.errorMessage,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: ColorsManager.warningRed,
)),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {},
onPressed: () async {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: () async {
final subSpaces = context
.read<SubSpaceBloc>()
.state
.subSpaces;
onSave!(subSpaces);
Navigator.of(context).pop();
},
onPressed: (state.errorMessage.isNotEmpty)
? null
: () async {
final subSpaces = context
.read<SubSpaceBloc>()
.state
.subSpaces;
onSave!(subSpaces);
Navigator.of(context).pop();
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
foregroundColor: state.errorMessage.isNotEmpty
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
child: const Text('OK'),
),
),

View File

@ -25,22 +25,30 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
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
));
if (state.duplicates.isNotEmpty) {
emit(SubSpaceModelState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'*Duplicated sub-space name',
state.duplicates,
));
} else {
emit(SubSpaceModelState(
updatedSubSpaces,
state.updatedSubSpaceModels,
'',
state.duplicates,
));
}
}
});
// Handle RemoveSubSpaceModel Event
on<RemoveSubSpaceModel>((event, emit) {
final updatedSubSpaces = List<SubspaceTemplateModel>.from(state.subSpaces)
..remove(event.subSpace);
@ -48,16 +56,6 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
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(
@ -66,12 +64,28 @@ class SubSpaceModelBloc extends Bloc<SubSpaceModelEvent, SubSpaceModelState> {
));
}
// Count occurrences of sub-space names to identify duplicates
final nameOccurrences = <String, int>{};
for (final subSpace in updatedSubSpaces) {
final lowerName = subSpace.subspaceName.toLowerCase();
nameOccurrences[lowerName] = (nameOccurrences[lowerName] ?? 0) + 1;
}
// Identify duplicate names
final updatedDuplicates = nameOccurrences.entries
.where((entry) => entry.value > 1)
.map((entry) => entry.key)
.toSet();
// Determine the error message
final errorMessage =
updatedDuplicates.isNotEmpty ? '*Duplicated sub-space name' : '';
emit(SubSpaceModelState(
updatedSubSpaces,
updatedSubspaceModels,
'',
errorMessage,
updatedDuplicates,
// Clear error message
));
});

View File

@ -94,12 +94,13 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
duplicateIndices.indexOf(index) != 0;
return Chip(
label: Text(
subSpace.subspaceName,
style: const TextStyle(
color: ColorsManager.spaceColor,
),
),
label: Text(subSpace.subspaceName,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: ColorsManager.spaceColor,
)),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
@ -135,28 +136,33 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
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),
),
controller: textController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: state.subSpaces.isEmpty
? 'Please enter the name'
: null,
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
color: ColorsManager
.lightGrayColor)),
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
context.read<SubSpaceModelBloc>().add(
AddSubSpaceModel(
SubspaceTemplateModel(
subspaceName: value.trim(),
disabled: false)));
textController.clear();
}
},
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.blackColor)),
),
],
),
@ -164,13 +170,13 @@ class CreateSubSpaceModelDialog extends StatelessWidget {
if (state.errorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Text(
state.errorMessage,
style: const TextStyle(
color: ColorsManager.red,
fontSize: 12,
),
),
child: Text(state.errorMessage,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: ColorsManager.red,
)),
),
const SizedBox(height: 16),
Row(

View File

@ -1,5 +1,8 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_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/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/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_model.dart';
@ -17,23 +20,52 @@ class TagHelper {
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),
initialTags.addAll(
subspace.tags!.map(
(tag) => tag.copyWith(
location: subspace.subspaceName,
internalId: tag.internalId,
tag: tag.tag,
),
);
}
),
);
}
}
}
return initialTags;
}
static Map<ProductModel, int> groupTags(List<TagModel> tags) {
static List<Tag> generateInitialForTags({
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,
internalId: tag.internalId,
tag: tag.tag,
),
),
);
}
}
}
return initialTags;
}
static Map<ProductModel, int> groupTags(List<BaseTag> tags) {
final Map<ProductModel, int> groupedTags = {};
for (var tag in tags) {
if (tag.product != null) {
@ -78,4 +110,39 @@ class TagHelper {
))
.toList();
}
static List<SelectedProduct> createInitialSelectedProductsForTags(
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();
}
}

View File

@ -68,8 +68,11 @@ class CreateSpaceModelBloc
on<UpdateSpaceTemplate>((event, emit) {
_space = event.spaceTemplate;
emit(CreateSpaceModelLoaded(_space!));
final String? errorMessage = _checkDuplicateModelName(
event.allModels ?? [], event.spaceTemplate.modelName);
emit(CreateSpaceModelLoaded(_space!, errorMessage: errorMessage));
});
on<AddSubspacesToSpaceTemplate>((event, emit) {
final currentState = state;
@ -132,7 +135,8 @@ class CreateSpaceModelBloc
final updatedSpace =
currentState.space.copyWith(subspaceModels: updatedSubspaces);
emit(CreateSpaceModelLoaded(updatedSpace));
emit(CreateSpaceModelLoaded(updatedSpace,
errorMessage: currentState.errorMessage));
} else {
emit(CreateSpaceModelError("Space template not initialized"));
}
@ -197,94 +201,101 @@ class CreateSpaceModelBloc
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;
if (event.spaceTemplate.uuid != null) {
final prevSpaceModel =
await _api.getSpaceModel(event.spaceTemplate.uuid ?? '');
tagUpdates = processTagUpdates(prevSpaceModel.tags, newSpaceModel.tags);
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;
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) {
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((subspace) => subspace.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));
}
}
} 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 (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));
if (newSubspace.tags != null) {
for (var tag in newSubspace.tags!) {
tagUpdates.add(TagModelUpdate(
action: Action.add,
uuid: tag.uuid == '' ? null : tag.uuid,
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));
}
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
};
final spaceModelBody = CreateSpaceTemplateBodyModel(
modelName: spaceModelName,
tags: tagUpdates,
subspaceModels: subspaceUpdates);
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 res = await _api.updateSpaceModel(
spaceModelBody, prevSpaceModel?.uuid ?? '');
if (res != null) {
emit(CreateSpaceModelLoaded(newSpaceModel));
if (event.onUpdate != null) {
event.onUpdate!(event.updatedSpaceTemplate);
}
}
}
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'));
}
@ -298,10 +309,22 @@ class CreateSpaceModelBloc
final List<TagModelUpdate> tagUpdates = [];
final processedTags = <String?>{};
if (prevTags == null && newTags != null) {
for (var newTag in newTags) {
tagUpdates.add(TagModelUpdate(
action: Action.add,
tag: newTag.tag,
uuid: newTag.uuid,
productUuid: newTag.product?.uuid,
));
}
return tagUpdates;
}
if (newTags != null || prevTags != null) {
// Case 1: Tags deleted
if (prevTags != null && newTags != null) {
for (var prevTag in prevTags!) {
for (var prevTag in prevTags) {
final existsInNew =
newTags!.any((newTag) => newTag.uuid == prevTag.uuid);
if (!existsInNew) {
@ -318,13 +341,16 @@ class CreateSpaceModelBloc
// Case 2: Tags added
if (newTags != null) {
final prevTagUuids = prevTags?.map((t) => t.uuid).toSet() ?? {};
for (var newTag in newTags!) {
// Tag without UUID
if ((newTag.uuid == null || newTag.uuid!.isEmpty) &&
if ((newTag.uuid == null || !prevTagUuids.contains(newTag.uuid)) &&
!processedTags.contains(newTag.tag)) {
tagUpdates.add(TagModelUpdate(
action: Action.add,
tag: newTag.tag,
uuid: newTag.uuid == '' ? null : newTag.uuid,
productUuid: newTag.product?.uuid));
processedTags.add(newTag.tag);
}
@ -350,4 +376,11 @@ class CreateSpaceModelBloc
return tagUpdates;
}
String? _checkDuplicateModelName(List<String> allModels, String name) {
if (allModels.contains(name)) {
return "Duplicate Model name";
}
return null;
}
}

View File

@ -14,8 +14,9 @@ class LoadSpaceTemplate extends CreateSpaceModelEvent {}
class UpdateSpaceTemplate extends CreateSpaceModelEvent {
final SpaceTemplateModel spaceTemplate;
List<String>? allModels;
UpdateSpaceTemplate(this.spaceTemplate);
UpdateSpaceTemplate(this.spaceTemplate,this.allModels);
}
class CreateSpaceTemplate extends CreateSpaceModelEvent {

View File

@ -1,5 +1,5 @@
class TagBodyModel {
late String uuid;
late String? uuid;
late String tag;
late final String? productUuid;

View File

@ -20,7 +20,7 @@ class SubspaceTemplateModel {
final String internalId = json['internalId'] ?? const Uuid().v4();
return SubspaceTemplateModel(
uuid: json['uuid'] ?? '',
uuid: json['uuid'],
subspaceName: json['subspaceName'] ?? '',
internalId: internalId,
disabled: json['disabled'] ?? false,

View File

@ -1,27 +1,27 @@
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/base_tag.dart';
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();
class TagModel extends BaseTag {
TagModel({
String? uuid,
required String? tag,
ProductModel? product,
String? internalId,
String? location,
}) : super(
uuid: uuid,
tag: tag,
product: product,
internalId: internalId,
location: location,
);
factory TagModel.fromJson(Map<String, dynamic> json) {
final String internalId = json['internalId'] ?? const Uuid().v4();
return TagModel(
uuid: json['uuid'] ?? '',
uuid: json['uuid'] ,
internalId: internalId,
tag: json['tag'] ?? '',
product: json['product'] != null
@ -30,16 +30,19 @@ class TagModel {
);
}
@override
TagModel copyWith(
{String? tag,
ProductModel? product,
String? uuid,
String? location,
String? internalId}) {
String? internalId}) {
return TagModel(
tag: tag ?? this.tag,
product: product ?? this.product,
location: location ?? this.location,
internalId: internalId ?? this.internalId,
uuid:uuid?? this.uuid
);
}
@ -55,7 +58,7 @@ class TagModel {
extension TagModelExtensions on TagModel {
TagBodyModel toTagBodyModel() {
return TagBodyModel()
..uuid = uuid ?? ''
..uuid = uuid
..tag = tag ?? ''
..productUuid = product?.uuid;
}

View File

@ -63,7 +63,8 @@ class SpaceModelPage extends StatelessWidget {
}
// Render existing space model
final model = spaceModels[index];
final otherModel = List<String>.from(allSpaceModelNames);
final otherModel =
List<String>.from(allSpaceModelNames);
otherModel.remove(model.modelName);
return GestureDetector(
onTap: () {
@ -76,6 +77,7 @@ class SpaceModelPage extends StatelessWidget {
spaceModel: model,
otherSpaceModels: otherModel,
pageContext: context,
allSpaceModels: spaceModels,
);
},
);
@ -95,7 +97,10 @@ class SpaceModelPage extends StatelessWidget {
return Center(
child: Text(
'Error: ${state.message}',
style: const TextStyle(color: ColorsManager.warningRed),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.warningRed),
),
);
}
@ -107,14 +112,14 @@ class SpaceModelPage extends StatelessWidget {
double _calculateChildAspectRatio(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
if (screenWidth > 1600) {
return 2;
return 1.5; // Decrease to make cards taller
}
if (screenWidth > 1200) {
return 3;
return 2.0;
} else if (screenWidth > 800) {
return 3.5;
return 2.5;
} else {
return 4.0;
return 3.0;
}
}

View File

@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ButtonContentWidget extends StatelessWidget {
final IconData icon;
final IconData? icon;
final String label;
final String? svgAssets;
const ButtonContentWidget({
Key? key,
required this.icon,
required this.label,
}) : super(key: key);
const ButtonContentWidget(
{Key? key, this.icon, required this.label, this.svgAssets})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -30,10 +30,20 @@ class ButtonContentWidget extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
child: Row(
children: [
Icon(
icon,
color: ColorsManager.spaceColor,
),
if (icon != null)
Icon(
icon,
color: ColorsManager.spaceColor,
),
if (svgAssets != null)
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
svgAssets!,
width: screenWidth * 0.015, // Adjust icon size
height: screenWidth * 0.015,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(

View File

@ -22,6 +22,7 @@ class CreateSpaceModelDialog extends StatelessWidget {
final SpaceTemplateModel? spaceModel;
final BuildContext? pageContext;
final List<String>? otherSpaceModels;
final List<SpaceTemplateModel>? allSpaceModels;
const CreateSpaceModelDialog(
{Key? key,
@ -29,7 +30,8 @@ class CreateSpaceModelDialog extends StatelessWidget {
this.allTags,
this.spaceModel,
this.pageContext,
this.otherSpaceModels})
this.otherSpaceModels,
this.allSpaceModels})
: super(key: key);
@override
@ -50,12 +52,14 @@ class CreateSpaceModelDialog extends StatelessWidget {
create: (_) {
final bloc = CreateSpaceModelBloc(_spaceModelApi);
if (spaceModel != null) {
bloc.add(UpdateSpaceTemplate(spaceModel!));
bloc.add(UpdateSpaceTemplate(spaceModel!, otherSpaceModels));
} else {
bloc.add(UpdateSpaceTemplate(SpaceTemplateModel(
modelName: '',
subspaceModels: const [],
)));
bloc.add(UpdateSpaceTemplate(
SpaceTemplateModel(
modelName: '',
subspaceModels: const [],
),
otherSpaceModels));
}
spaceNameController.addListener(() {
@ -98,14 +102,19 @@ class CreateSpaceModelDialog extends StatelessWidget {
name: value,
allModels: otherSpaceModels ?? []));
},
style: const TextStyle(color: ColorsManager.blackColor),
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.blackColor),
decoration: InputDecoration(
filled: true,
fillColor: ColorsManager.textFieldGreyColor,
hintText: 'Please enter the name',
errorText: state.errorMessage,
hintStyle: const TextStyle(
color: ColorsManager.lightGrayColor),
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.lightGrayColor),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
@ -119,7 +128,6 @@ class CreateSpaceModelDialog extends StatelessWidget {
),
const SizedBox(height: 16),
SubspaceModelCreate(
context,
subspaces: state.space.subspaceModels ?? [],
onSpaceModelUpdate: (updatedSubspaces) {
context
@ -138,6 +146,7 @@ class CreateSpaceModelDialog extends StatelessWidget {
spaceNameController: spaceNameController,
pageContext: pageContext,
otherSpaceModels: otherSpaceModels,
allSpaceModels: allSpaceModels,
),
const SizedBox(height: 20),
SizedBox(
@ -147,15 +156,19 @@ class CreateSpaceModelDialog extends StatelessWidget {
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
onPressed: () {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: state.errorMessage == null ||
isNameValid
? () {
onPressed: ((state.errorMessage != null &&
state.errorMessage != '') ||
!isNameValid)
? null
: () {
final updatedSpaceTemplate =
updatedSpaceModel.copyWith(
modelName:
@ -228,13 +241,14 @@ class CreateSpaceModelDialog extends StatelessWidget {
}
}
}
}
: null,
},
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: isNameValid
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
foregroundColor: ((state.errorMessage != null &&
state.errorMessage != '') ||
!isNameValid)
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
@ -246,7 +260,10 @@ class CreateSpaceModelDialog extends StatelessWidget {
} else if (state is CreateSpaceModelError) {
return Text(
'Error: ${state.message}',
style: const TextStyle(color: ColorsManager.warningRed),
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.warningRed),
);
}

View File

@ -29,7 +29,7 @@ class DynamicRoomWidget extends StatelessWidget {
final TextPainter textPainter = TextPainter(
text: TextSpan(
text: subspace.subspaceName,
style: const TextStyle(fontSize: 16),
style: Theme.of(context).textTheme.bodyMedium
),
textDirection: TextDirection.ltr,
)..layout();

View File

@ -31,82 +31,90 @@ class SpaceModelCardWidget extends StatelessWidget {
}
}
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),
return LayoutBuilder(
builder: (context, constraints) {
bool showOnlyName = constraints.maxWidth < 250;
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
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,
),
if (!showOnlyName) ...[
const SizedBox(height: 10),
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,
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));
},
),
),
),
],
),
),
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));
},
),
),
),
],
),
]
],
),
],
),
);
},
);
}
}

View File

@ -1,23 +1,41 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/edit_chip.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/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SubspaceModelCreate extends StatelessWidget {
class SubspaceModelCreate extends StatefulWidget {
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);
const SubspaceModelCreate({
Key? key,
required this.subspaces,
this.onSpaceModelUpdate,
}) : super(key: key);
@override
_SubspaceModelCreateState createState() => _SubspaceModelCreateState();
}
class _SubspaceModelCreateState extends State<SubspaceModelCreate> {
late List<SubspaceTemplateModel> _subspaces;
String? errorSubspaceId;
@override
void initState() {
super.initState();
_subspaces = List.from(widget.subspaces);
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Container(
child: subspaces.isEmpty
child: _subspaces.isEmpty
? TextButton(
style: TextButton.styleFrom(
overlayColor: ColorsManager.transparentColor,
@ -39,46 +57,37 @@ class SubspaceModelCreate extends StatelessWidget {
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
width: 3.0,
),
),
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),
..._subspaces.map((subspace) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SubspaceNameDisplayWidget(
text: subspace.subspaceName,
validateName: (updatedName) {
return !_subspaces.any((s) =>
s != subspace &&
s.subspaceName == updatedName);
},
onNameChanged: (updatedName) {
setState(() {
subspace.subspaceName = updatedName;
});
},
),
child: Text(
subspace.subspaceName,
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
)),
GestureDetector(
],
);
}),
EditChip(
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),
),
),
),
],
),
@ -95,9 +104,16 @@ class SubspaceModelCreate extends StatelessWidget {
return CreateSubSpaceModelDialog(
isEdit: true,
dialogTitle: dialogTitle,
existingSubSpaces: subspaces,
existingSubSpaces: _subspaces,
onUpdate: (subspaceModels) {
onSpaceModelUpdate!(subspaceModels);
setState(() {
_subspaces = subspaceModels;
errorSubspaceId = null;
});
if (widget.onSpaceModelUpdate != null) {
widget.onSpaceModelUpdate!(subspaceModels);
}
},
);
},

View File

@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SubspaceNameDisplayWidget extends StatefulWidget {
final String text;
final TextStyle? textStyle;
final Color backgroundColor;
final Color borderColor;
final EdgeInsetsGeometry padding;
final BorderRadiusGeometry borderRadius;
final void Function(String updatedName) onNameChanged;
final bool Function(String updatedName) validateName;
const SubspaceNameDisplayWidget({
Key? key,
required this.text,
this.textStyle,
this.backgroundColor = ColorsManager.whiteColors,
this.borderColor = ColorsManager.transparentColor,
this.padding = const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
this.borderRadius = const BorderRadius.all(Radius.circular(10)),
required this.onNameChanged,
required this.validateName,
}) : super(key: key);
@override
_SubspaceNameDisplayWidgetState createState() =>
_SubspaceNameDisplayWidgetState();
}
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
bool isEditing = false;
late TextEditingController _controller;
late FocusNode _focusNode;
late String previousName;
String? errorText;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.text);
_focusNode = FocusNode();
previousName = widget.text;
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
void _handleValidationAndSave() {
final updatedName = _controller.text;
if (widget.validateName(updatedName)) {
setState(() {
errorText = null;
isEditing = false;
previousName = updatedName;
widget.onNameChanged(updatedName);
});
} else {
setState(() {
errorText = 'Subspace name already exists.';
});
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
isEditing = true;
_focusNode.requestFocus();
});
},
child: Container(
padding: widget.padding,
decoration: BoxDecoration(
color: widget.backgroundColor,
borderRadius: widget.borderRadius,
border: Border.all(color: widget.borderColor),
),
child: isEditing
? TextField(
controller: _controller,
focusNode: _focusNode,
style: widget.textStyle ??
Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
autofocus: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(horizontal: 8.0),
),
onSubmitted: (value) {
_handleValidationAndSave();
},
)
: Text(
widget.text,
style: widget.textStyle ??
Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
),
),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
errorText!,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.warningRed),
),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/edit_chip.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';
@ -18,6 +19,7 @@ class TagChipDisplay extends StatelessWidget {
final TextEditingController spaceNameController;
final BuildContext? pageContext;
final List<String>? otherSpaceModels;
final List<SpaceTemplateModel>? allSpaceModels;
const TagChipDisplay(BuildContext context,
{Key? key,
@ -28,7 +30,8 @@ class TagChipDisplay extends StatelessWidget {
required this.allTags,
required this.spaceNameController,
this.pageContext,
this.otherSpaceModels})
this.otherSpaceModels,
this.allSpaceModels})
: super(key: key);
@override
@ -70,9 +73,12 @@ class TagChipDisplay extends StatelessWidget {
),
label: Text(
'x${entry.value}', // Show count
style: const TextStyle(
color: ColorsManager.spaceColor,
),
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
color:
ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
@ -83,45 +89,31 @@ class TagChipDisplay extends StatelessWidget {
),
),
),
GestureDetector(
onTap: () async {
// Use the Navigator's context for showDialog
final navigatorContext =
Navigator.of(context).overlay?.context;
EditChip(onTap: () async {
// Use the Navigator's context for showDialog
Navigator.of(context).pop();
if (navigatorContext != null) {
await showDialog<bool>(
barrierDismissible: false,
context: navigatorContext,
builder: (context) => AssignTagModelsDialog(
products: products,
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AssignTagModelsDialog(
products: products,
allSpaceModels: allSpaceModels,
subspaces: subspaces,
pageContext: pageContext,
allTags: allTags,
spaceModel: spaceModel,
otherSpaceModels: otherSpaceModels,
initialTags: TagHelper.generateInitialTags(
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),
),
),
),
spaceTagModels: spaceModel?.tags ?? []),
title: 'Edit Device',
addedProducts:
TagHelper.createInitialSelectedProducts(
spaceModel?.tags ?? [], subspaces),
spaceName: spaceModel?.modelName ?? '',
));
})
],
),
),
@ -141,6 +133,7 @@ class TagChipDisplay extends StatelessWidget {
pageContext: pageContext,
isCreate: true,
spaceModel: spaceModel,
otherSpaceModels: otherSpaceModels,
),
);
},

View File

@ -5,6 +5,7 @@ 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/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/models/tag_model.dart';
@ -26,6 +27,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
final List<String>? otherSpaceModels;
final BuildContext? pageContext;
final SpaceTemplateModel? spaceModel;
final List<SpaceTemplateModel>? allSpaceModels;
const AddDeviceTypeModelWidget(
{super.key,
@ -38,7 +40,8 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
required this.isCreate,
this.pageContext,
this.otherSpaceModels,
this.spaceModel});
this.spaceModel,
this.allSpaceModels});
@override
Widget build(BuildContext context) {
@ -106,6 +109,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
context: context,
builder: (BuildContext dialogContext) {
return CreateSpaceModelDialog(
allSpaceModels: allSpaceModels,
products: products,
allTags: allTags,
pageContext: pageContext,
@ -120,7 +124,7 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
},
);
} else {
final initialTags = generateInitialTags(
final initialTags = TagHelper.generateInitialTags(
spaceTagModels: spaceTagModels,
subspaces: subspaces,
);
@ -162,7 +166,8 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
: () async {
if (state is AddDeviceModelLoaded &&
state.selectedProducts.isNotEmpty) {
final initialTags = generateInitialTags(
final initialTags =
TagHelper.generateInitialTags(
spaceTagModels: spaceTagModels,
subspaces: subspaces,
);
@ -175,11 +180,12 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
context: context,
builder: (context) => AssignTagModelsDialog(
products: products,
allSpaceModels: allSpaceModels,
subspaces: subspaces,
addedProducts: state.selectedProducts,
allTags: allTags,
spaceName: spaceName,
initialTags: state.initialTag,
initialTags: initialTags,
otherSpaceModels: otherSpaceModels,
title: dialogTitle,
spaceModel: spaceModel,
@ -200,29 +206,4 @@ class AddDeviceTypeModelWidget extends StatelessWidget {
),
);
}
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;
}
}