Compare commits

..
Author SHA1 Message Date
Faris Armoush 12deceb7d3 SP-1513-rework 2025-05-25 11:35:01 +03:00
Faris Armoush 0f9227a6f5 Merge pull request #200 from SyncrowIOT/SP-1591-FE-Implement-Space-Level-Structure-Selection-and-Air-Quality-Device-Dropdown
Sp 1591 fe implement space level structure selection and air quality device dropdown
2025-05-22 15:59:19 +03:00
Faris Armoush 5b13962d41 removed unnecessary * 1 calculation of height. 2025-05-22 15:57:03 +03:00
Faris Armoush 8c53d5322a SP-1591 2025-05-22 15:53:18 +03:00
Faris Armoush af4d37939b Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1591-FE-Implement-Space-Level-Structure-Selection-and-Air-Quality-Device-Dropdown 2025-05-22 15:48:47 +03:00
Faris Armoush d43c1847ff SP-1591 2025-05-22 15:44:19 +03:00
Faris Armoush 4c5b390887 Fixed typos. 2025-05-22 15:42:49 +03:00
Faris Armoush 5eeac01666 cannot select a community in AirQualityDataLoadingStrategy. 2025-05-22 15:35:04 +03:00
Faris Armoush 717d698378 can select child spaces with children in AirQualityDataLoadingStrategy. 2025-05-22 15:23:42 +03:00
Faris Armoush 9adbbb9a2d Integrated and implemented devices dropdown into the newly created widget AirQualityEndSideWidget. 2025-05-22 15:19:50 +03:00
Faris Armoush e792dbd72f SP-1591/ Implement business logic in AirQualityDataLoadingStrategy for community structure loading strategy. 2025-05-22 14:58:42 +03:00
Faris Armoush d2eea33714 Prepared AirQualityView layout and structure with PlaceHolder widgets. 2025-05-22 12:24:13 +03:00
Faris Armoush 5a61647fe4 Prepared and created the necessary component for the air quality loading strategy for the side bar selection, and for loading data in different parts of the UI. 2025-05-21 16:49:30 +03:00
Faris Armoush 568b6be354 Created AirQualityView widget for the new Air Quality analytics module. 2025-05-21 16:46:38 +03:00
50 changed files with 1582 additions and 1993 deletions
+5 -18
View File
@@ -17,8 +17,7 @@ class TagDialogTextfieldDropdown extends StatefulWidget {
}) : super(key: key);
@override
_DialogTextfieldDropdownState createState() =>
_DialogTextfieldDropdownState();
_DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState();
}
class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
@@ -37,12 +36,6 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
// Call onSelected when focus is lost
final selectedTag = _filteredItems.firstWhere(
(tag) => tag.tag == _controller.text,
orElse: () => Tag(tag: _controller.text),
);
widget.onSelected(selectedTag);
_closeDropdown();
}
});
@@ -50,9 +43,7 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
void _filterItems() {
setState(() {
_filteredItems = widget.items;
// .where((tag) => tag.product?.uuid == widget.product)
// .toList();
_filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList();
});
}
@@ -121,9 +112,7 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager
.textPrimaryColor)),
?.copyWith(color: ColorsManager.textPrimaryColor)),
onTap: () {
_controller.text = tag.tag ?? '';
widget.onSelected(tag);
@@ -167,15 +156,13 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: (value) {
final selectedTag = _filteredItems.firstWhere(
(tag) => tag.tag == value,
final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value,
orElse: () => Tag(tag: value));
widget.onSelected(selectedTag);
_closeDropdown();
},
onTapOutside: (event) {
widget.onSelected(_filteredItems.firstWhere(
(tag) => tag.tag == _controller.text,
widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text,
orElse: () => Tag(tag: _controller.text)));
_closeDropdown();
},
-16
View File
@@ -1,16 +0,0 @@
import 'enums.dart';
class GenericException implements Exception {
final ExceptionType type;
final String errorMessage;
const GenericException(
{required this.type, this.errorMessage = "Unknown Error"});
@override
String toString() {
return errorMessage;
}
List<Object?> get props => [type, errorMessage];
}
-36
View File
@@ -1,36 +0,0 @@
import 'package:dio/dio.dart';
import '../../utils/constants/api_const.dart';
class DioInstance {
Dio? _dio;
Dio get dio => _dio ?? _instantiate();
String? baseUrl;
DioInstance({this.baseUrl});
Dio _instantiate() {
Dio dio = Dio(
BaseOptions(
baseUrl: baseUrl ?? '${ApiEndpoints.baseUrl}/',
receiveDataWhenStatusError: true,
headers: {
'content_Type': 'application/json',
},
),
);
dio.interceptors.add(
LogInterceptor(
responseHeader: false,
requestHeader: false,
requestBody: true,
responseBody: true,
),
);
return dio;
}
}
-3
View File
@@ -1,3 +0,0 @@
class EndPoints {
static const String fetchCommunities = 'projects/{projectUuid}/communities';
}
-50
View File
@@ -1,50 +0,0 @@
enum RequestType {
get,
post,
delete,
put,
}
enum ExceptionType {
notAuthenticated,
connectionError,
// related to http status code exceptions
notAuthorized,
notFound,
internalServerException,
serviceUnavailableException,
pageGone,
// related to bad request status code
// related to auth requests
invalidCredentials,
solutionAlreadySunmitted,
invalidValidation,
// other
other,
}
enum ExceptionMessage {
NOT_AUTHENTICATED,
INVALID_CREDENTIALS,
The_password_field_must_be_at_least_8_characters,
SOLUTION_ALREADY_SUBMITTED,
you_are_not_authorized,
page_not_found,
page_gone,
INTERNAL_SERVER_ERROR,
service_unavailable,
}
enum NotificationsType {
payment,
transporation,
product,
zero,
}
enum ImagesType {
assets,
svg,
network,
}
-121
View File
@@ -1,121 +0,0 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../pages/auth/model/token.dart';
import 'custom_exceptions.dart';
import 'dio.dart';
import 'enums.dart';
class Request {
String endPoint;
bool? autherized;
bool? isFormData;
RequestType? method;
Map<String, dynamic>? headers;
final Map<String, dynamic>? queryParams;
Map<String, dynamic>? body;
Duration? receiveTimeout;
Request(
this.endPoint, {
this.autherized,
this.isFormData,
this.method,
this.headers,
this.queryParams,
this.body,
this.receiveTimeout,
}) {
headers = {
'content_Type': 'application/json',
'Accept': 'application/json',
};
}
Future<Map<String, dynamic>> sendRequest() async {
Response? response;
if (autherized != null && autherized!) {
final storage = const FlutterSecureStorage();
final token = await storage.read(key: Token.loginAccessTokenKey);
if (token != null) {
headers!["authorization"] = "Bearer $token";
}
}
try {
response = await DioInstance().dio.request(
endPoint,
queryParameters: queryParams,
data: isFormData != null && isFormData == true
? FormData.fromMap(body!)
: body,
options: Options(
method: method!.name.toUpperCase(),
headers: headers,
contentType: 'application/json',
receiveTimeout: receiveTimeout,
),
);
if (response.statusCode! >= 200 && response.statusCode! < 300) {
return response.data;
}
} on DioException catch (error) {
if (error.type == DioExceptionType.badResponse) {
throw badRequestException[error.response!.data["error"]] ??
const GenericException(
type: ExceptionType.other,
);
}
if (error.type == DioExceptionType.connectionError ||
error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.sendTimeout ||
error.type == DioExceptionType.unknown) {
throw const GenericException(
type: ExceptionType.connectionError,
errorMessage: 'no_internet_connection',
);
}
}
return {};
}
Map<String, dynamic> toJson() => {
'endPoint': endPoint,
'method': method!.name.toUpperCase(),
'body': json.encode(body),
'headers': headers,
'queryParams': queryParams,
'autherized': autherized,
'isFormData': isFormData,
};
@override
String toString() {
return jsonEncode(toJson());
}
List<Object?> get props => [
endPoint,
autherized,
isFormData,
method,
headers,
queryParams,
body,
receiveTimeout,
];
}
Map<String, GenericException> badRequestException = {
ExceptionMessage.INVALID_CREDENTIALS.name: const GenericException(
type: ExceptionType.invalidCredentials,
errorMessage: "Invalid credentials ...",
),
"The password field must be at least 8 characters.": const GenericException(
type: ExceptionType.invalidValidation,
errorMessage: 'password must be 8 or more characters',
),
ExceptionMessage.NOT_AUTHENTICATED.name: const GenericException(
type: ExceptionType.notAuthenticated,
errorMessage: "not authenticated",
),
};
@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
abstract final class FetchAirQualityDataHelper {
const FetchAirQualityDataHelper._();
static void loadAirQualityData(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
}
static void clearAllData(BuildContext context) {
context.read<AnalyticsDevicesBloc>().add(
const ClearAnalyticsDeviceEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
}
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['AQI'],
requestType: AnalyticsDeviceRequestType.energyManagement,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
},
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
class AirQualityView extends StatelessWidget {
const AirQualityView({super.key});
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isMediumOrLess = constraints.maxWidth <= 900;
final height = MediaQuery.sizeOf(context).height;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: _padding,
child: Column(
spacing: 32,
children: [
SizedBox(
height: height * 1.2,
child: const AirQualityEndSideWidget(),
),
SizedBox(height: height * 0.5, child: const Placeholder()),
SizedBox(height: height * 0.5, child: const Placeholder()),
],
),
);
}
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height,
child: const Column(
children: [
Expanded(
child: Row(
spacing: 32,
children: [
Expanded(
flex: 2,
child: Column(
spacing: 20,
children: [
Expanded(child: Placeholder()),
Expanded(child: Placeholder()),
],
),
),
Expanded(child: AirQualityEndSideWidget()),
],
),
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class AirQualityEndSideWidget extends StatelessWidget {
const AirQualityEndSideWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsetsDirectional.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
Text(
'Device ID:',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 6),
SelectableText(
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: SelectableText(
'AQI Sensor',
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
fontSize: 18,
),
),
),
),
const Spacer(),
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDeviceDropdown(
onChanged: (value) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(value),
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
),
),
],
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart';
@@ -10,6 +11,10 @@ enum AnalyticsPageTab {
occupancy(
title: 'Occupancy',
child: AnalyticsOccupancyView(),
),
airQuality(
title: 'Air Quality',
child: AirQualityView(),
);
const AnalyticsPageTab({
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.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';
final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
) {
// Do nothing
}
@override
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (isSpaceSelected) {
clearData(context);
return;
}
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
FetchAirQualityDataHelper.loadAirQualityData(
context,
communityUuid: community.uuid,
spaceUuid: space.uuid ?? '',
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
if (child.children.isNotEmpty) return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchAirQualityDataHelper.clearAllData(context);
}
}
@@ -1,4 +1,5 @@
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart';
@@ -9,6 +10,7 @@ abstract final class AnalyticsDataLoadingStrategyFactory {
return switch (tab) {
AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(),
AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(),
AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(),
};
}
}
@@ -48,7 +48,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel child,
) {
onSpaceSelected(context, community, child);
if (child.children.isNotEmpty) return onSpaceSelected(context, community, child);
}
@override
@@ -125,7 +125,48 @@ class PowerClampPhasesDataWidget extends StatelessWidget {
(e) => e.code == code,
orElse: () => DataPoint(value: '--'),
);
final value = element?.value;
if (code.contains('Current')) {
return _formatCurrentValue(value?.toString());
}
if (code.contains('PowerFactor')) {
return _formatPowerFactor(value?.toString());
}
if (code.contains('Voltage')) {
return _formatVoltage(value?.toString());
}
return value?.toString() ?? '--';
}
return element?.value.toString() ?? '--';
String _formatCurrentValue(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
if (str.length == 1) return '${str[0]}.0';
return '${str[0]}.${str.substring(1)}';
}
String _formatPowerFactor(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
final intValue = int.tryParse(str);
if (intValue == null) return '--';
final doubleValue = intValue / 100;
return doubleValue.toStringAsFixed(2);
}
String _formatVoltage(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
if (str.length == 1) return '0.${str[0]}';
return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}';
}
}
+5 -8
View File
@@ -51,13 +51,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
try {
var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
user = await HomeApi().fetchUserInfo(uuid);
if (user != null && user!.project != null) {
await ProjectManager.setProjectUUID(user!.project!.uuid);
// NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
}
add(FetchTermEvent());
add(FetchPolicyEvent());
@@ -89,12 +88,10 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
Future _confirmUserAgreement(
ConfirmUserAgreementEvent event, Emitter<HomeState> emit) async {
Future _confirmUserAgreement(ConfirmUserAgreementEvent event, Emitter<HomeState> emit) async {
try {
emit(LoadingHome());
var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
policy = await HomeApi().confirmUserAgreements(uuid);
emit(PolicyAgreement());
} catch (e) {
@@ -158,7 +155,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
},
color: ColorsManager.primaryColor,
),
// HomeItemModel(
// title: 'Move in',
// icon: Assets.moveinIcon,
+6 -10
View File
@@ -24,7 +24,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
void initState() {
super.initState();
final homeBloc = BlocProvider.of<HomeBloc>(context);
// homeBloc.add(const FetchUserInfo());
homeBloc.add(const FetchUserInfo());
}
@override
@@ -38,10 +38,8 @@ class _HomeWebPageState extends State<HomeWebPage> {
child: BlocConsumer<HomeBloc, HomeState>(
listener: (BuildContext context, state) {
if (state is HomeInitial) {
if (homeBloc.user!.hasAcceptedWebAgreement == false &&
!_dialogShown) {
_dialogShown =
true; // Set the flag to true to indicate the dialog is showing.
if (homeBloc.user!.hasAcceptedWebAgreement == false && !_dialogShown) {
_dialogShown = true; // Set the flag to true to indicate the dialog is showing.
Future.delayed(const Duration(seconds: 1), () {
showDialog(
context: context,
@@ -56,7 +54,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
_dialogShown = false;
if (v != null) {
homeBloc.add(ConfirmUserAgreementEvent());
// homeBloc.add(const FetchUserInfo());
homeBloc.add(const FetchUserInfo());
}
});
});
@@ -100,8 +98,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
width: size.width * 0.68,
child: GridView.builder(
itemCount: homeBloc.homeItems.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // Adjust as needed.
crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0,
@@ -113,8 +110,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
active: homeBloc.homeItems[index].active!,
name: homeBloc.homeItems[index].title!,
img: homeBloc.homeItems[index].icon!,
onTap: () =>
homeBloc.homeItems[index].onPress(context),
onTap: () => homeBloc.homeItems[index].onPress(context),
);
},
),
@@ -21,8 +21,7 @@ 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' as custom_action;
class SpaceManagementBloc
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementState> {
final CommunitySpaceManagementApi _api;
final ProductApi _productApi;
final SpaceModelManagementApi _spaceModelApi;
@@ -63,8 +62,7 @@ class SpaceManagementBloc
int page = 1;
while (hasNext) {
final spaceModels = await _spaceModelApi.listSpaceModels(
page: page, projectId: projectUuid);
final spaceModels = await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels);
page++;
@@ -77,29 +75,26 @@ class SpaceManagementBloc
await fetchTags();
emit(SpaceModelLoaded(
communities: state is SpaceManagementLoaded
? (state as SpaceManagementLoaded).communities
: [],
communities:
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [],
products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? []));
}
void _deleteSpaceModelFromCache(DeleteSpaceModelFromCache event,
Emitter<SpaceManagementState> emit) async {
void _deleteSpaceModelFromCache(
DeleteSpaceModelFromCache event, Emitter<SpaceManagementState> emit) async {
if (_cachedSpaceModels != null) {
_cachedSpaceModels = _cachedSpaceModels!
.where((model) => model.uuid != event.deletedUuid)
.toList();
_cachedSpaceModels =
_cachedSpaceModels!.where((model) => model.uuid != event.deletedUuid).toList();
} else {
_cachedSpaceModels = await fetchSpaceModels();
}
await fetchTags();
emit(SpaceModelLoaded(
communities: state is SpaceManagementLoaded
? (state as SpaceManagementLoaded).communities
: [],
communities:
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [],
products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? []));
@@ -127,8 +122,8 @@ class SpaceManagementBloc
int page = 1;
while (hasNext) {
final spaceModels = await _spaceModelApi.listSpaceModels(
page: page, projectId: projectUuid);
final spaceModels =
await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels);
page++;
@@ -169,12 +164,10 @@ class SpaceManagementBloc
await fetchTags();
emit(SpaceManagementLoading());
final success = await _api.updateCommunity(
event.communityUuid, event.name, projectUuid);
final success = await _api.updateCommunity(event.communityUuid, event.name, projectUuid);
if (success) {
if (previousState is SpaceManagementLoaded) {
final updatedCommunities =
List<CommunityModel>.from(previousState.communities);
final updatedCommunities = List<CommunityModel>.from(previousState.communities);
for (var community in updatedCommunities) {
if (community.uuid == event.communityUuid) {
community.name = event.name;
@@ -219,8 +212,7 @@ class SpaceManagementBloc
}
}
Future<List<SpaceModel>> _fetchSpacesForCommunity(
String communityUuid) async {
Future<List<SpaceModel>> _fetchSpacesForCommunity(String communityUuid) async {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
return await _api.getSpaceHierarchy(communityUuid, projectUuid);
@@ -250,23 +242,20 @@ class SpaceManagementBloc
}
}
Future<void> _onBlankState(
BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
Future<void> _onBlankState(BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
try {
final previousState = state;
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>();
var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
List<CommunityModel> communities =
await _waitForCommunityList(spaceBloc, spaceTreeState);
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
await fetchSpaceModels();
// await fetchTags();
await fetchTags();
var prevSpaceModels = await fetchSpaceModels();
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
if (previousState is SpaceManagementLoaded || previousState is BlankState) {
final prevCommunities = (previousState as dynamic).communities ?? [];
emit(BlankState(
communities: List<CommunityModel>.from(prevCommunities),
@@ -297,8 +286,7 @@ class SpaceManagementBloc
_onloadProducts();
await fetchTags();
// Wait until `communityList` is loaded
List<CommunityModel> communities =
await _waitForCommunityList(spaceBloc, spaceTreeState);
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
// Fetch space models after communities are available
final prevSpaceModels = await fetchSpaceModels();
@@ -322,9 +310,8 @@ class SpaceManagementBloc
final completer = Completer<List<CommunityModel>>();
final subscription = spaceBloc.stream.listen((state) {
if (!completer.isCompleted && state.communityList.isNotEmpty) {
completer.complete(state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList);
completer
.complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList);
}
});
try {
@@ -352,8 +339,7 @@ class SpaceManagementBloc
emit(SpaceManagementLoading());
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final success =
await _api.deleteCommunity(event.communityUuid, projectUuid);
final success = await _api.deleteCommunity(event.communityUuid, projectUuid);
if (success) {
// add(LoadCommunityAndSpacesEvent());
} else {
@@ -375,13 +361,12 @@ class SpaceManagementBloc
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
await fetchTags();
CommunityModel? newCommunity = await _api.createCommunity(
event.name, event.description, projectUuid);
CommunityModel? newCommunity =
await _api.createCommunity(event.name, event.description, projectUuid);
var prevSpaceModels = await fetchSpaceModels();
if (newCommunity != null) {
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
if (previousState is SpaceManagementLoaded || previousState is BlankState) {
final prevCommunities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
@@ -474,15 +459,12 @@ class SpaceManagementBloc
try {
final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
final updatedSpaces = await saveSpacesHierarchically(
event.context, event.spaces, event.communityUuid);
final updatedSpaces =
await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid);
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
// emit(SpaceCreationSuccess(spaces: updatedSpaces));
// updatedSpaces.forEach(
// (element) => element.uuid,
// );
// final lastUpdatedSpaced = updatedSpaces..addAll(allSpaces);
emit(SpaceCreationSuccess(spaces: updatedSpaces));
if (previousState is SpaceManagementLoaded) {
await _updateLoadedState(
spaceTreeState,
@@ -493,7 +475,7 @@ class SpaceManagementBloc
);
}
} catch (e) {
// emit(SpaceManagementError('Error saving spaces: $e'));
emit(SpaceManagementError('Error saving spaces: $e'));
if (previousState is SpaceManagementLoaded) {
emit(previousState);
@@ -533,15 +515,13 @@ class SpaceManagementBloc
return;
}
}
emit(previousState);
} catch (e, stackTrace) {
emit(previousState);
// rethrow;
rethrow;
}
}
Future<List<SpaceModel>> saveSpacesHierarchically(BuildContext context,
List<SpaceModel> spaces, String communityUuid) async {
Future<List<SpaceModel>> saveSpacesHierarchically(
BuildContext context, List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces);
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@@ -554,14 +534,6 @@ class SpaceManagementBloc
selectedCommunity = filteredCommunities.firstWhere(
(community) => community.uuid == communityUuid,
orElse: () => CommunityModel(
uuid: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
name: '',
description: '',
spaces: spaces,
),
);
} catch (e) {
return [];
@@ -576,7 +548,9 @@ class SpaceManagementBloc
if (parent.uuid != null) {
await _api.deleteSpace(communityUuid, parent.uuid!, projectUuid);
}
} catch (e) {}
} catch (e) {
rethrow;
}
}
orderedSpaces.removeWhere((space) => parentsToDelete.contains(space));
@@ -590,7 +564,7 @@ class SpaceManagementBloc
if (matchedSpaces.isEmpty) continue;
final prevSpace = matchedSpaces.elementAtOrNull(0);
final prevSpace = matchedSpaces[0];
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
final List<SubspaceModel>? prevSubspaces = prevSpace?.subspaces;
@@ -601,19 +575,17 @@ class SpaceManagementBloc
if (prevSubspaces != null || newSubspaces != null) {
if (prevSubspaces != null && newSubspaces != null) {
for (var prevSubspace in prevSubspaces) {
final existsInNew = newSubspaces
.any((subspace) => subspace.uuid == prevSubspace.uuid);
final existsInNew =
newSubspaces.any((subspace) => subspace.uuid == prevSubspace.uuid);
if (!existsInNew) {
subspaceUpdates.add(UpdateSubspaceTemplateModel(
action: custom_action.Action.delete,
uuid: prevSubspace.uuid));
action: custom_action.Action.delete, uuid: prevSubspace.uuid));
}
}
} else if (prevSubspaces != null && newSubspaces == null) {
for (var prevSubspace in prevSubspaces) {
subspaceUpdates.add(UpdateSubspaceTemplateModel(
action: custom_action.Action.delete,
uuid: prevSubspace.uuid));
action: custom_action.Action.delete, uuid: prevSubspace.uuid));
}
}
@@ -641,9 +613,7 @@ class SpaceManagementBloc
}
if (prevSubspaces != null && newSubspaces != null) {
final newSubspaceMap = {
for (var subspace in newSubspaces) subspace.uuid: subspace
};
final newSubspaceMap = {for (var subspace in newSubspaces) subspace.uuid: subspace};
for (var prevSubspace in prevSubspaces) {
final newSubspace = newSubspaceMap[prevSubspace.uuid];
@@ -669,10 +639,9 @@ class SpaceManagementBloc
isPrivate: space.isPrivate,
position: space.position,
icon: space.icon,
subspaces: space.subspaces,
// subspaceUpdates,
tags: space.tags,
// tagUpdates,
subspaces: subspaceUpdates,
tags: tagUpdates,
direction: space.incomingConnection?.direction,
spaceModelUuid: space.spaceModel?.uuid,
projectId: projectUuid);
} else {
@@ -682,10 +651,8 @@ class SpaceManagementBloc
: [];
var createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels = subspace.tags
?.map((tag) => tag.toCreateTagBodyModel())
.toList() ??
[];
final tagBodyModels =
subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? [];
return CreateSubspaceModel()
..subspaceName = subspace.subspaceName
..tags = tagBodyModels;
@@ -704,6 +671,7 @@ class SpaceManagementBloc
isPrivate: space.isPrivate,
position: space.position,
icon: space.icon,
direction: space.incomingConnection?.direction,
spaceModelUuid: space.spaceModel?.uuid,
tags: tagBodyModels,
subspaces: createSubspaceBodyModels,
@@ -711,8 +679,7 @@ class SpaceManagementBloc
space.uuid = response?.uuid;
}
} catch (e) {
return [];
// Stop further execution on failure
rethrow; // Stop further execution on failure
}
}
return spaces;
@@ -743,8 +710,7 @@ class SpaceManagementBloc
return result.toList(); // Convert back to a list
}
void _onLoadSpaceModel(
SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
void _onLoadSpaceModel(SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
emit(SpaceManagementLoading());
try {
@@ -791,17 +757,14 @@ class SpaceManagementBloc
// Case 1: Tags deleted
if (prevTags != null && newTags != null) {
for (var prevTag in prevTags) {
final existsInNew =
newTags.any((newTag) => newTag.uuid == prevTag.uuid);
final existsInNew = newTags.any((newTag) => newTag.uuid == prevTag.uuid);
if (!existsInNew) {
tagUpdates.add(TagModelUpdate(
action: custom_action.Action.delete, uuid: prevTag.uuid));
tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid));
}
}
} else if (prevTags != null && newTags == null) {
for (var prevTag in prevTags) {
tagUpdates.add(TagModelUpdate(
action: custom_action.Action.delete, uuid: prevTag.uuid));
tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid));
}
}
@@ -844,16 +807,15 @@ class SpaceManagementBloc
return tagUpdates;
}
List<SpaceModel> findMatchingSpaces(
List<SpaceModel> spaces, String targetUuid) {
List<SpaceModel> findMatchingSpaces(List<SpaceModel> spaces, String targetUuid) {
List<SpaceModel> matched = [];
for (var space in spaces) {
if (space.uuid == targetUuid) {
matched.add(space);
}
matched.addAll(findMatchingSpaces(
space.children, targetUuid)); // Recursively search in children
matched
.addAll(findMatchingSpaces(space.children, targetUuid)); // Recursively search in children
}
return matched;
@@ -3,26 +3,23 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model
class Connection {
final SpaceModel startSpace;
final SpaceModel endSpace;
final String direction;
Connection({
required this.startSpace,
required this.endSpace,
});
Connection({required this.startSpace, required this.endSpace, required this.direction});
Map<String, dynamic> toMap() {
return {
'startUuid': startSpace.uuid ??
'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces
'endUuid': endSpace.uuid ??
'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces
'startUuid': startSpace.uuid ?? 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces
'endUuid': endSpace.uuid ?? 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces
'direction': direction,
};
}
static Connection fromMap(
Map<String, dynamic> map, Map<String, SpaceModel> spaces) {
static Connection fromMap(Map<String, dynamic> map, Map<String, SpaceModel> spaces) {
return Connection(
startSpace: spaces[map['startUuid']]!,
endSpace: spaces[map['endUuid']]!,
direction: map['direction'],
);
}
}
@@ -1,7 +1,5 @@
import 'package:syncrow_web/utils/constants/assets.dart';
import 'selected_product_model.dart';
class ProductModel {
final String uuid;
final String catName;
@@ -40,15 +38,6 @@ class ProductModel {
};
}
SelectedProduct toSelectedProduct(int count) {
return SelectedProduct(
productId: uuid,
count: count,
productName: name!,
product: this,
);
}
static String _mapIconToProduct(String prodType) {
const iconMapping = {
'1G': Assets.Gang1SwitchIcon,
@@ -101,7 +101,7 @@ class SpaceModel {
spaceModel: json['spaceModel'] != null
? SpaceTemplateModel.fromJson(json['spaceModel'])
: null,
tags: (json['productAllocations'] as List<dynamic>?)
tags: (json['tags'] as List<dynamic>?)
?.where((item) => item is Map<String, dynamic>) // Validate type
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
.toList() ??
@@ -116,6 +116,7 @@ class SpaceModel {
instance.incomingConnection = Connection(
startSpace: instance.parent ?? instance, // Parent space
endSpace: instance, // This space instance
direction: conn['direction'],
);
}
@@ -27,7 +27,7 @@ class SubspaceModel {
subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false,
internalId: internalId,
tags: (json['productAllocations'] as List<dynamic>?)
tags: (json['tags'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item))
.toList() ??
[],
@@ -36,7 +36,7 @@ class SubspaceModel {
Map<String, dynamic> toJson() {
return {
if (uuid != null) 'uuid': uuid,
'uuid': uuid,
'subspaceName': subspaceName,
'disabled': disabled,
'tags': tags?.map((e) => e.toJson()).toList() ?? [],
@@ -23,13 +23,10 @@ class Tag extends BaseTag {
final String internalId = json['internalId'] ?? const Uuid().v4();
return Tag(
//TODO:insure UUId for tag or prodAlloc
uuid: json['name'] != null ? json['uuid'] : json['tag']?['uuid'] ?? '',
uuid: json['uuid'] ?? '',
internalId: internalId,
tag: json['name'] ?? json['tag']?['name'] ?? '',
product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
tag: json['name'] ?? '',
product: json['product'] != null ? ProductModel.fromMap(json['product']) : null,
);
}
@@ -52,10 +49,9 @@ class Tag extends BaseTag {
Map<String, dynamic> toJson() {
return {
if (uuid != null) 'uuid': uuid,
'name': tag,
'productUuid': product?.uuid,
// .toMap(),
'uuid': uuid,
'tag': tag,
'product': product?.toMap(),
};
}
}
@@ -67,8 +67,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void initState() {
super.initState();
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
_nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '',
@@ -97,15 +96,13 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
if (oldWidget.spaces != widget.spaces) {
setState(() {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
realignTree();
});
}
if (widget.selectedSpace != oldWidget.selectedSpace &&
widget.selectedSpace != null) {
if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToSpace(widget.selectedSpace!);
});
@@ -188,8 +185,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
connection, widget.selectedSpace)
? 1.0
: 0.3, // Adjust opacity
child: CustomPaint(
painter: CurvedLinePainter([connection])),
child: CustomPaint(painter: CurvedLinePainter([connection])),
),
for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted &&
@@ -199,11 +195,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
top: entry.value.position.dy,
child: SpaceCardWidget(
index: entry.key,
onButtonTap: (int index, Offset newPosition) {
onButtonTap: (int index, Offset newPosition, String direction) {
_showCreateSpaceDialog(screenSize,
position:
spaces[index].position + newPosition,
position: spaces[index].position + newPosition,
parentIndex: index,
direction: direction,
projectTags: widget.projectTags);
},
position: entry.value.position,
@@ -214,9 +210,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_updateNodePosition(entry.value, newPosition);
},
buildSpaceContainer: (int index) {
final bool isHighlighted =
SpaceHelper.isHighlightedSpace(
spaces[index], widget.selectedSpace);
final bool isHighlighted = SpaceHelper.isHighlightedSpace(
spaces[index], widget.selectedSpace);
return Opacity(
opacity: isHighlighted ? 1.0 : 0.3,
@@ -294,6 +289,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _showCreateSpaceDialog(Size screenSize,
{Offset? position,
int? parentIndex,
String? direction,
double? canvasWidth,
double? canvasHeight,
required List<Tag> projectTags}) {
@@ -303,25 +299,19 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
allTags:
TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
projectTags: projectTags,
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
setState(() {
// Set the first space in the center or use passed position
Offset newPosition;
if (parentIndex != null) {
newPosition = getBalancedChildPosition(
spaces[parentIndex]); // Ensure balanced position
} else {
newPosition =
position ?? ConnectionHelper.getCenterPosition(screenSize);
getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position
} else {
newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize);
}
SpaceModel newSpace = SpaceModel(
@@ -335,13 +325,14 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
subspaces: subspaces,
tags: tags);
if (parentIndex != null) {
if (parentIndex != null && direction != null) {
SpaceModel parentSpace = spaces[parentIndex];
parentSpace.internalId = spaces[parentIndex].internalId;
newSpace.parent = parentSpace;
final newConnection = Connection(
startSpace: parentSpace,
endSpace: newSpace,
direction: direction,
);
connections.add(newConnection);
newSpace.incomingConnection = newConnection;
@@ -369,21 +360,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
name: widget.selectedSpace!.name,
icon: widget.selectedSpace!.icon,
projectTags: widget.projectTags,
parentSpace: SpaceHelper.findSpaceByInternalId(
widget.selectedSpace?.parent?.internalId, spaces),
parentSpace:
SpaceHelper.findSpaceByInternalId(widget.selectedSpace?.parent?.internalId, spaces),
editSpace: widget.selectedSpace,
currentSpaceModel: widget.selectedSpace?.spaceModel,
tags: widget.selectedSpace?.tags,
subspaces: widget.selectedSpace?.subspaces,
isEdit: true,
allTags: TagHelper.getAllTagValues(
widget.communities, widget.spaceModels),
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
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;
@@ -393,8 +379,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
widget.selectedSpace!.tags = tags;
if (widget.selectedSpace!.status != SpaceStatus.newSpace) {
widget.selectedSpace!.status =
SpaceStatus.modified; // Mark as modified
widget.selectedSpace!.status = SpaceStatus.modified; // Mark as modified
}
for (var space in spaces) {
@@ -425,8 +410,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted ||
space.status == SpaceStatus.parentDeleted) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) {
return;
}
result.add(space);
@@ -463,6 +447,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Connection(
startSpace: parent,
endSpace: child,
direction: "down",
),
);
@@ -547,16 +532,13 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _selectSpace(BuildContext context, SpaceModel space) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: space),
);
}
void _deselectSpace(BuildContext context) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: null),
);
}
@@ -726,8 +708,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent);
duplicated.position =
Offset(space.position.dx + 300, space.position.dy + 100);
duplicated.position = Offset(space.position.dx + 300, space.position.dy + 100);
List<SpaceModel> duplicatedSubtree = [];
void collectSubtree(SpaceModel node) {
duplicatedSubtree.add(node);
@@ -745,6 +726,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection(
startSpace: parent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
@@ -757,8 +739,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) {
final duplicatedName =
SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final newSpace = SpaceModel(
name: duplicatedName,
@@ -780,6 +761,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection(
startSpace: newSpace,
endSpace: duplicatedChild,
direction: "down",
);
connections.add(newConnection);
@@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../../common/edit_chip.dart';
import '../../../../../utils/color_manager.dart';
import '../../../helper/tag_helper.dart';
import '../../../space_model/widgets/button_content_widget.dart';
import '../../model/subspace_model.dart';
import '../../model/tag.dart';
class DevicesPartWidget extends StatelessWidget {
const DevicesPartWidget({
super.key,
required this.tags,
required this.subspaces,
required this.screenWidth,
required this.onEditChip,
required this.onTextButtonPressed,
required this.isTagsAndSubspaceModelDisabled,
});
final bool isTagsAndSubspaceModelDisabled;
final void Function() onEditChip;
final void Function() onTextButtonPressed;
final double screenWidth;
final List<Tag>? tags;
final List<SubspaceModel>? subspaces;
@override
Widget build(BuildContext context) {
return Column(
children: [
(tags?.isNotEmpty == true ||
subspaces?.any(
(subspace) => subspace.tags?.isNotEmpty == true) ==
true)
? SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
...TagHelper.groupTags([
...?tags,
...?subspaces?.expand((subspace) => subspace.tags ?? [])
]).entries.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
entry.key.icon ?? 'assets/icons/gateway.svg',
fit: BoxFit.contain,
),
),
label: Text(
'x${entry.value}', // Show count
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
EditChip(onTap: onEditChip)
],
),
),
)
: TextButton(
onPressed: onTextButtonPressed,
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: ButtonContentWidget(
icon: Icons.add,
label: 'Add Devices',
disabled: isTagsAndSubspaceModelDisabled,
),
)
],
);
}
}
@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../../utils/color_manager.dart';
import '../../../../../utils/constants/assets.dart';
class IconChoosePartWidget extends StatelessWidget {
const IconChoosePartWidget({
super.key,
required this.selectedIcon,
required this.showIconSelection,
required this.screenWidth,
});
final double screenWidth;
final String selectedIcon;
final void Function() showIconSelection;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 50),
Stack(
alignment: Alignment.center,
children: [
Container(
width: screenWidth * 0.1,
height: screenWidth * 0.1,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 20,
right: 20,
child: InkWell(
onTap: showIconSelection,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
),
),
],
),
],
);
}
}
@@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../utils/color_manager.dart';
import '../../../../../utils/constants/assets.dart';
import '../../../space_model/models/space_template_model.dart';
import '../../../space_model/widgets/button_content_widget.dart';
class SpaceModelLinkingWidget extends StatelessWidget {
const SpaceModelLinkingWidget({
super.key,
required this.onDeleted,
required this.onPressed,
required this.screenWidth,
required this.selectedSpaceModel,
required this.isSpaceModelDisabled,
});
final bool isSpaceModelDisabled;
final void Function()? onDeleted;
final void Function()? onPressed;
final double screenWidth;
final SpaceTemplateModel? selectedSpaceModel;
@override
Widget build(BuildContext context) {
return Column(
children: [
selectedSpaceModel == null
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
onPressed: onPressed,
child: ButtonContentWidget(
svgAssets: Assets.link,
label: 'Link a space model',
disabled: isSpaceModelDisabled,
),
)
: Container(
width: screenWidth * 0.25,
padding: const EdgeInsets.symmetric(
vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Chip(
label: Text(
selectedSpaceModel?.modelName ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: onDeleted),
],
),
),
],
);
}
}
@@ -1,82 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../utils/color_manager.dart';
class SpaceNameTextfieldWidget extends StatelessWidget {
SpaceNameTextfieldWidget({
super.key,
required this.isNameFieldExist,
required this.isNameFieldInvalid,
required this.onChange,
required this.screenWidth,
required this.nameController,
});
TextEditingController nameController;
final void Function(String value) onChange;
final double screenWidth;
bool isNameFieldExist;
bool isNameFieldInvalid;
@override
Widget build(BuildContext context) {
return Column(
children: [
SizedBox(
width: screenWidth * 0.25,
child: TextField(
controller: nameController,
onChanged: onChange,
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,
),
),
),
),
),
if (isNameFieldInvalid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Space name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameFieldExist)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exist',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
],
);
}
}
@@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import '../../../../../common/edit_chip.dart';
import '../../../../../utils/color_manager.dart';
import '../../../space_model/widgets/button_content_widget.dart';
import '../../../space_model/widgets/subspace_name_label_widget.dart';
import '../../model/subspace_model.dart';
class SubSpacePartWidget extends StatefulWidget {
SubSpacePartWidget({
super.key,
required this.subspaces,
required this.onPressed,
required this.isTagsAndSubspaceModelDisabled,
required this.screenWidth,
required this.editChipOnTap,
});
double screenWidth;
bool isTagsAndSubspaceModelDisabled;
final void Function() editChipOnTap;
List<SubspaceModel>? subspaces;
final void Function() onPressed;
@override
State<SubSpacePartWidget> createState() => _SubSpacePartWidgetState();
}
class _SubSpacePartWidgetState extends State<SubSpacePartWidget> {
@override
Widget build(BuildContext context) {
return Column(
children: [
widget.subspaces == null || widget.subspaces!.isEmpty
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: widget.onPressed,
child: ButtonContentWidget(
icon: Icons.add,
label: 'Create Sub Spaces',
disabled: widget.isTagsAndSubspaceModelDisabled,
),
)
: SizedBox(
width: widget.screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
if (widget.subspaces != null)
...widget.subspaces!.map((subspace) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SubspaceNameDisplayWidget(
text: subspace.subspaceName,
validateName: (updatedName) {
bool nameExists = widget.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: widget.editChipOnTap,
)
],
),
),
)
],
);
}
}
@@ -30,13 +30,28 @@ class CurvedLinePainter extends CustomPainter {
Offset end = connection.endSpace.position +
const Offset(75, 0); // Center top of end space
// Curved line for down connections
final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50);
final path = Path()
..moveTo(start.dx, start.dy)
..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint);
if (connection.direction == 'down') {
// Curved line for down connections
final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50);
final path = Path()
..moveTo(start.dx, start.dy)
..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint);
} else if (connection.direction == 'right') {
start = connection.startSpace.position +
const Offset(150, 30); // Right center
end = connection.endSpace.position + const Offset(0, 30); // Left center
canvas.drawLine(start, end, paint);
} else if (connection.direction == 'left') {
start =
connection.startSpace.position + const Offset(0, 30); // Left center
end = connection.endSpace.position +
const Offset(150, 30); // Right center
canvas.drawLine(start, end, paint);
}
final dotPaint = Paint()..color = ColorsManager.blackColor;
canvas.drawCircle(start, 5, dotPaint); // Start dot
canvas.drawCircle(end, 5, dotPaint); // End dot
@@ -1,4 +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';
@@ -7,11 +9,6 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/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/create_space_widgets/devices_part_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.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';
@@ -19,6 +16,8 @@ import 'package:syncrow_web/pages/spaces_management/helper/space_helper.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';
@@ -83,10 +82,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
super.initState();
selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? '');
selectedProducts =
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty;
if (widget.currentSpaceModel != null) {
subspaces = [];
tags = [];
@@ -99,15 +96,13 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
@override
Widget build(BuildContext context) {
bool isSpaceModelDisabled = (tags != null && tags!.isNotEmpty ||
subspaces != null && subspaces!.isNotEmpty);
bool isSpaceModelDisabled =
(tags != null && tags!.isNotEmpty || subspaces != null && subspaces!.isNotEmpty);
bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null);
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
width: screenWidth * 0.5,
@@ -117,10 +112,50 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
children: [
Expanded(
flex: 1,
child: IconChoosePartWidget(
selectedIcon: selectedIcon,
showIconSelection: _showIconSelectionDialog,
screenWidth: screenWidth,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 50),
Stack(
alignment: Alignment.center,
children: [
Container(
width: screenWidth * 0.1,
height: screenWidth * 0.1,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 20,
right: 20,
child: InkWell(
onTap: _showIconSelectionDialog,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
),
),
],
),
],
),
),
const SizedBox(width: 20),
@@ -129,146 +164,342 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpaceNameTextfieldWidget(
isNameFieldExist: isNameFieldExist,
isNameFieldInvalid: isNameFieldInvalid,
nameController: nameController,
screenWidth: screenWidth,
onChange: (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 (SpaceHelper.isNameConflict(
value, widget.parentSpace, widget.editSpace)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
isOkButtonEnabled = true;
if (!isNameFieldInvalid) {
if (SpaceHelper.isNameConflict(
value, widget.parentSpace, widget.editSpace)) {
isNameFieldExist = true;
isOkButtonEnabled = false;
} else {
isNameFieldExist = false;
isOkButtonEnabled = true;
}
}
}
});
},
),
const SizedBox(height: 10),
// SpaceModelLinkingWidget(
// isSpaceModelDisabled: true,
// // isSpaceModelDisabled,
// onPressed: () {
// isSpaceModelDisabled
// ? null
// : _showLinkSpaceModelDialog(context);
// },
// onDeleted: () => setState(() {
// selectedSpaceModel = null;
// subspaces = widget.subspaces ?? [];
// tags = widget.tags ?? [];
// }),
// screenWidth: screenWidth,
// selectedSpaceModel: selectedSpaceModel,
// ),
const SizedBox(height: 25),
// Row(
// children: [
// const Expanded(
// child: Divider(
// color: ColorsManager.neutralGray,
// thickness: 1.0,
// ),
// ),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
// child: Text(
// 'OR',
// style: Theme.of(context)
// .textTheme
// .bodyMedium
// ?.copyWith(fontWeight: FontWeight.bold),
// ),
// ),
// const Expanded(
// child: Divider(
// color: ColorsManager.neutralGray,
// thickness: 1.0,
// ),
// ),
// ],
// ),
const SizedBox(height: 25),
SubSpacePartWidget(
subspaces: subspaces,
onPressed: () {
isTagsAndSubspaceModelDisabled
? null
: _showSubSpaceDialog(
context,
enteredName,
[],
false,
widget.products,
subspaces,
);
},
isTagsAndSubspaceModelDisabled:
isTagsAndSubspaceModelDisabled,
screenWidth: screenWidth,
editChipOnTap: () async {
_showSubSpaceDialog(
context,
enteredName,
[],
true,
widget.products,
subspaces,
);
},
),
const SizedBox(height: 10),
DevicesPartWidget(
tags: tags,
subspaces: subspaces,
screenWidth: screenWidth,
isTagsAndSubspaceModelDisabled:
isTagsAndSubspaceModelDisabled,
onEditChip: () async {
await showDialog(
context: context,
builder: (context) => AssignTagDialog(
products: widget.products,
subspaces: subspaces,
allTags: widget.allTags,
addedProducts:
TagHelper.createInitialSelectedProductsForTags(
tags ?? [], subspaces),
title: 'Edit Device',
initialTags: TagHelper.generateInitialForTags(
spaceTags: tags, subspaces: subspaces),
spaceName: widget.name ?? '',
projectTags: widget.projectTags,
onSave: (updatedTags, updatedSubspaces) {
setState(() {
tags = updatedTags;
subspaces = updatedSubspaces;
});
},
});
},
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,
),
),
);
},
onTextButtonPressed: () {
isTagsAndSubspaceModelDisabled
? null
: _showTagCreateDialog(
context,
enteredName,
widget.isEdit,
widget.products,
);
},
)
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(
color: ColorsManager.boxColor,
width: 1.5,
),
),
),
),
),
if (isNameFieldInvalid)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Space name should not be empty.',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
if (isNameFieldExist)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'*Name already exist',
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.red),
),
),
const SizedBox(height: 10),
selectedSpaceModel == null
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
onPressed: () {
isSpaceModelDisabled ? null : _showLinkSpaceModelDialog(context);
},
child: ButtonContentWidget(
svgAssets: Assets.link,
label: 'Link a space model',
disabled: isSpaceModelDisabled,
),
)
: Container(
width: screenWidth * 0.25,
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Chip(
label: Text(
selectedSpaceModel?.modelName ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: () => setState(() {
this.selectedSpaceModel = null;
subspaces = widget.subspaces ?? [];
tags = widget.tags ?? [];
})),
],
),
),
const SizedBox(height: 25),
Row(
children: [
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Text(
'OR',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
],
),
const SizedBox(height: 25),
subspaces == null || subspaces!.isEmpty
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () async {
isTagsAndSubspaceModelDisabled
? null
: _showSubSpaceDialog(
context, enteredName, [], false, widget.products, subspaces);
},
child: ButtonContentWidget(
icon: Icons.add,
label: 'Create Sub Space',
disabled: isTagsAndSubspaceModelDisabled,
),
)
: SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
if (subspaces != null)
...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, [], true,
widget.products, subspaces);
},
)
],
),
),
),
const SizedBox(height: 10),
(tags?.isNotEmpty == true ||
subspaces?.any((subspace) => subspace.tags?.isNotEmpty == true) == true)
? SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
...TagHelper.groupTags([
...?tags,
...?subspaces?.expand((subspace) => subspace.tags ?? [])
]).entries.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
entry.key.icon ?? 'assets/icons/gateway.svg',
fit: BoxFit.contain,
),
),
label: Text(
'x${entry.value}', // Show count
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
EditChip(onTap: () async {
await showDialog(
context: context,
builder: (context) => AssignTagDialog(
products: widget.products,
subspaces: subspaces,
allTags: widget.allTags,
addedProducts:
TagHelper.createInitialSelectedProductsForTags(
tags ?? [], subspaces),
title: 'Edit Device',
initialTags: TagHelper.generateInitialForTags(
spaceTags: tags, subspaces: subspaces),
spaceName: widget.name ?? '',
projectTags: widget.projectTags,
onSave: (updatedTags, updatedSubspaces) {
setState(() {
tags = updatedTags;
subspaces = updatedSubspaces;
});
},
),
);
})
],
),
),
)
: TextButton(
onPressed: () {
isTagsAndSubspaceModelDisabled
? null
: _showTagCreateDialog(
context,
enteredName,
widget.isEdit,
widget.products,
);
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: ButtonContentWidget(
icon: Icons.add,
label: 'Add Devices',
disabled: isTagsAndSubspaceModelDisabled,
))
],
),
),
@@ -298,32 +529,17 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
} else if (isNameFieldExist) {
return;
} else {
String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? '');
if (newName.isNotEmpty) {
if (tags != null && tags!.isNotEmpty) {
if (tags!.any(
(tag) => tag.uuid == null || tag.uuid!.isEmpty,
)) {
return;
}
}
widget.onCreateSpace(
newName,
selectedIcon,
selectedProducts,
selectedSpaceModel,
subspaces,
tags);
widget.onCreateSpace(newName, selectedIcon, selectedProducts,
selectedSpaceModel, subspaces, tags);
Navigator.of(context).pop();
}
}
},
borderRadius: 10,
backgroundColor: isOkButtonEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
backgroundColor:
isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
@@ -334,7 +550,6 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
//dialooogggs
void _showIconSelectionDialog() {
showDialog(
context: context,
@@ -371,50 +586,26 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showSubSpaceDialog(
BuildContext context,
String name,
final List<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? existingSubSpaces,
) {
void _showSubSpaceDialog(BuildContext context, String name, final List<Tag>? spaceTags,
bool isEdit, List<ProductModel>? products, final List<SubspaceModel>? existingSubSpaces) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSubSpaceDialog(
spaceName: name,
dialogTitle: isEdit ? 'Edit Sub-spaces' : 'Create Sub-spaces',
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space',
products: products,
existingSubSpaces: existingSubSpaces,
onSave: (slectedSubspaces, updatedSubSpaces) {
onSave: (slectedSubspaces) {
final List<Tag> tagsToAppendToSpace = [];
if (slectedSubspaces != null && slectedSubspaces.isNotEmpty) {
final updatedIds =
slectedSubspaces.map((s) => s.internalId).toSet();
if (slectedSubspaces != null) {
final updatedIds = slectedSubspaces.map((s) => s.internalId).toSet();
if (existingSubSpaces != null) {
final deletedSubspaces = existingSubSpaces
.where((s) => !updatedIds.contains(s.internalId))
.toList();
final deletedSubspaces =
existingSubSpaces.where((s) => !updatedIds.contains(s.internalId)).toList();
for (var s in deletedSubspaces) {
if (s.tags != null) {
s.tags!.forEach(
(tag) => tag.location = null,
);
tagsToAppendToSpace.addAll(s.tags!);
}
}
}
} else {
if (existingSubSpaces != null) {
final deletedSubspaces = existingSubSpaces;
for (var s in deletedSubspaces) {
if (s.tags != null) {
s.tags!.forEach(
(tag) => tag.location = null,
);
tagsToAppendToSpace.addAll(s.tags!);
}
}
@@ -432,16 +623,15 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
List<ProductModel>? products) {
void _showTagCreateDialog(
BuildContext context, String name, bool isEdit, List<ProductModel>? products) {
isEdit
? showDialog(
context: context,
builder: (BuildContext context) {
return AssignTagDialog(
title: 'Edit Device',
addedProducts: TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
addedProducts: TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
spaceName: name,
products: products,
subspaces: subspaces,
@@ -456,8 +646,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}
@@ -481,8 +670,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
allTags: widget.allTags,
projectTags: widget.projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() {
tags = selectedSpaceTags;
@@ -492,8 +680,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}
@@ -5,7 +5,7 @@ class PlusButtonWidget extends StatelessWidget {
final int index;
final String direction;
final Offset offset;
final Function(int index, Offset newPosition) onButtonTap;
final Function(int index, Offset newPosition, String direction) onButtonTap;
const PlusButtonWidget({
super.key,
@@ -17,21 +17,35 @@ class PlusButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
onButtonTap(index, const Offset(0, 150));
},
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
return Positioned(
left: offset.dx,
top: offset.dy,
child: GestureDetector(
onTap: () {
Offset newPosition;
switch (direction) {
case 'left':
newPosition = const Offset(-200, 0);
break;
case 'right':
newPosition = const Offset(200, 0);
break;
case 'down':
newPosition = const Offset(0, 150);
break;
default:
newPosition = Offset.zero;
}
onButtonTap(index, newPosition, direction);
},
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
),
),
);
@@ -7,7 +7,7 @@ class SpaceCardWidget extends StatelessWidget {
final Offset position;
final bool isHovered;
final Function(int index, bool isHovered) onHoverChanged;
final Function(int index, Offset newPosition) onButtonTap;
final Function(int index, Offset newPosition, String direction) onButtonTap;
final Widget Function(int index) buildSpaceContainer;
final ValueChanged<Offset> onPositionChanged;
@@ -25,34 +25,35 @@ class SpaceCardWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => onHoverChanged(index, true),
onExit: (_) => onHoverChanged(index, false),
child: SizedBox(
width: 140, // Make sure this covers both card and plus button
height: 90,
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// Call the provided callback to update the position
final newPosition = position + details.delta;
onPositionChanged(newPosition);
},
child: MouseRegion(
onEnter: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, true);
},
onExit: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, false);
},
child: Stack(
clipBehavior: Clip.none,
clipBehavior: Clip
.none, // Allow hovering elements to be displayed outside the boundary
children: [
// Main card
Container(
width: 140,
height: 80,
alignment: Alignment.center,
color: Colors.transparent,
child: buildSpaceContainer(index),
),
// Plus button (NO inner Positioned!)
if (isHovered)
Align(
alignment: Alignment.bottomCenter,
child: PlusButtonWidget(
index: index,
direction: 'down',
offset: Offset.zero,
onButtonTap: onButtonTap,
),
buildSpaceContainer(index), // Build the space container
if (isHovered) ...[
PlusButtonWidget(
index: index,
direction: 'down',
offset: const Offset(63, 50),
onButtonTap: onButtonTap,
),
],
],
),
),
@@ -13,8 +13,7 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final existingTagCounts = <String, int>{};
for (var tag in initialTags) {
if (tag.product != null) {
existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
existingTagCounts[tag.product!.uuid] = (existingTagCounts[tag.product!.uuid] ?? 0) + 1;
}
}
@@ -23,17 +22,14 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 ||
selectedProduct.count <= existingCount) {
tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
if (selectedProduct.count == 0 || selectedProduct.count <= existingCount) {
tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId));
continue;
}
final missingCount = selectedProduct.count - existingCount;
tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId));
if (missingCount > 0) {
tags.addAll(List.generate(
@@ -89,8 +85,7 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final tags = List<Tag>.from(currentState.tags);
// Update the location
tags[event.index] =
tags[event.index].copyWith(location: event.location);
tags[event.index] = tags[event.index].copyWith(location: event.location);
final updatedTags = _calculateAvailableTags(projectTags, tags);
@@ -122,8 +117,7 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete);
final tags = List<Tag>.from(currentState.tags)..remove(event.tagToDelete);
// Recalculate available tags
final updatedTags = _calculateAvailableTags(projectTags, tags);
@@ -147,10 +141,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
// Get validation error for duplicate tags
String? _getValidationError(List<Tag> tags) {
final nonEmptyTags = tags
.map((tag) => tag.tag?.trim() ?? '')
.where((tag) => tag.isNotEmpty)
.toList();
final nonEmptyTags =
tags.map((tag) => tag.tag?.trim() ?? '').where((tag) => tag.isNotEmpty).toList();
final duplicateTags = nonEmptyTags
.fold<Map<String, int>>({}, (map, tag) {
@@ -176,11 +168,9 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
.toSet();
final availableTags = allTags
.where((tag) =>
tag.tag != null && !selectedTagSet.contains(tag.tag!.trim()))
.where((tag) => tag.tag != null && !selectedTagSet.contains(tag.tag!.trim()))
.toList();
return projectTags;
// availableTags;
return availableTags;
}
}
@@ -1,5 +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/tag_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';
@@ -7,10 +12,9 @@ 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/assign_tag_models/views/widgets/assign_tags_tables_widget.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'widgets/save_add_device_row_widget.dart';
import 'package:uuid/uuid.dart';
class AssignTagDialog extends StatelessWidget {
final List<ProductModel>? products;
@@ -25,7 +29,7 @@ class AssignTagDialog extends StatelessWidget {
final List<Tag> projectTags;
const AssignTagDialog(
{super.key,
{Key? key,
required this.products,
required this.subspaces,
required this.addedProducts,
@@ -35,14 +39,13 @@ class AssignTagDialog extends StatelessWidget {
required this.spaceName,
required this.title,
this.onSave,
required this.projectTags});
required this.projectTags})
: super(key: key);
@override
Widget build(BuildContext context) {
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space');
return BlocProvider(
create: (_) => AssignTagBloc(projectTags)
@@ -64,31 +67,131 @@ class AssignTagDialog extends StatelessWidget {
content: SingleChildScrollView(
child: Column(
children: [
AssignTagsTable(
controllers: controllers,
locations: locations,
tags: state.tags,
updatedTags: state.updatedTags,
onDeleteDevice: ({required index, required tag}) {
context
.read<AssignTagBloc>()
.add(DeleteTag(tagToDelete: tag, tags: state.tags));
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(state.tags.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label: Text('Tag', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label:
Text('Location', style: Theme.of(context).textTheme.bodyMedium)),
],
rows: state.tags.isEmpty
? [
DataRow(cells: [
DataCell(
Center(
child: Text('No Data Available',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
)),
),
),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
controllers.removeAt(index);
},
onTagDropDownSelected: ({required index, required tag}) {
context.read<AssignTagBloc>().add(UpdateTagEvent(
index: index,
tag: tag,
));
},
onLocationDropDownSelected: (
{required index, required location}) {
context.read<AssignTagBloc>().add(UpdateLocation(
index: index,
location: location,
));
},
return DataRow(
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
context.read<AssignTagBloc>().add(
DeleteTag(tagToDelete: tag, tags: state.tags));
controllers.removeAt(index);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment:
Alignment.centerLeft, // Align cell content to the left
child: SizedBox(
width: double.infinity,
child: TagDialogTextfieldDropdown(
key: ValueKey('dropdown_${const Uuid().v4()}_$index'),
items: state.updatedTags,
product: tag.product?.uuid ?? 'Unknown',
initialValue: tag,
onSelected: (value) {
controller.text = value.tag ?? '';
context.read<AssignTagBloc>().add(UpdateTagEvent(
index: index,
tag: value,
));
},
),
),
),
),
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,
));
},
)),
),
],
);
}),
),
),
if (state.errorMessage != null)
Text(state.errorMessage!,
@@ -100,15 +203,69 @@ class AssignTagDialog extends StatelessWidget {
),
),
actions: [
SaveAddDeviceRowWidget(
subspaces: subspaces,
products: products,
projectTags: projectTags,
spaceName: spaceName,
onSave: onSave,
allTags: allTags,
tags: state.tags,
isSaveEnabled: state.isSaveEnabled,
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceModel>.from(result['subspaces'] as List<dynamic>);
Navigator.of(context).pop();
await showDialog(
context: context,
builder: (context) => AddDeviceTypeWidget(
products: products,
subspaces: processedSubspaces,
projectTags: projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
processedTags, processedSubspaces),
spaceName: spaceName,
spaceTags: processedTags,
isCreate: false,
onSave: onSave,
allTags: allTags,
),
);
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: state.isSaveEnabled
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
onPressed: state.isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceModel>.from(result['subspaces'] as List<dynamic>);
onSave?.call(processedTags, processedSubspaces);
Navigator.of(context).pop();
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
),
],
);
@@ -1,100 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import '../../../add_device_type/views/add_device_type_widget.dart';
import '../../../helper/tag_helper.dart';
class SaveAddDeviceRowWidget extends StatelessWidget {
const SaveAddDeviceRowWidget({
super.key,
required this.subspaces,
required this.products,
required this.projectTags,
required this.spaceName,
required this.onSave,
required this.allTags,
required this.tags,
required this.isSaveEnabled,
});
final List<Tag> tags;
final List<SubspaceModel>? subspaces;
final List<ProductModel>? products;
final List<Tag> projectTags;
final String spaceName;
final Function(List<Tag> p1, List<SubspaceModel>? p2)? onSave;
final List<String>? allTags;
final bool isSaveEnabled;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(tags);
final result = TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces = List<SubspaceModel>.from(
result['subspaces'] as List<dynamic>);
Navigator.of(context).pop();
await showDialog(
context: context,
builder: (context) => AddDeviceTypeWidget(
products: products,
subspaces: processedSubspaces,
projectTags: projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
processedTags, processedSubspaces),
spaceName: spaceName,
spaceTags: processedTags,
isCreate: false,
onSave: onSave,
allTags: allTags,
),
);
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: isSaveEnabled
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
onPressed: isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(tags);
final result =
TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces = List<SubspaceModel>.from(
result['subspaces'] as List<dynamic>);
onSave?.call(processedTags, processedSubspaces);
Navigator.of(context).pop();
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
);
}
}
@@ -1,5 +1,9 @@
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/tag_dialog_textfield_dropdown.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
@@ -8,10 +12,11 @@ import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assig
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/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 'widgets/RowOfCancelSaveWidget.dart';
import 'widgets/assign_tags_tables_widget.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:uuid/uuid.dart';
class AssignTagModelsDialog extends StatelessWidget {
final List<ProductModel>? products;
@@ -48,10 +53,8 @@ class AssignTagModelsDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space');
return BlocProvider(
create: (_) => AssignTagModelBloc(projectTags)
@@ -75,38 +78,137 @@ class AssignTagModelsDialog extends StatelessWidget {
content: SingleChildScrollView(
child: Column(
children: [
AssignTagsTable(
controllers: controllers,
locations: locations,
tags: state.tags,
updatedTags: state.updatedTags,
onDeleteDevice: ({required index, required tag}) {
context
.read<AssignTagModelBloc>()
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags,
));
controllers.removeAt(index);
},
onTagDropDownSelected: (
{required index, required tag}) {
context.read<AssignTagModelBloc>().add(
UpdateTag(
index: index,
tag: tag,
),
);
},
onLocationDropDownSelected: (
{required index, required location}) {
context.read<AssignTagModelBloc>().add(
UpdateLocation(
index: index,
location: location,
),
);
},
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(state.tags.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device',
style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label:
Text('Tag', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Location',
style: Theme.of(context).textTheme.bodyMedium)),
],
rows: state.tags.isEmpty
? [
DataRow(cells: [
DataCell(
Center(
child: Text('No Devices Available',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
)),
),
),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
return DataRow(
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
context.read<AssignTagModelBloc>().add(
DeleteTagModel(
tagToDelete: tag, tags: state.tags));
controllers.removeAt(index);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment
.centerLeft, // Align cell content to the left
child: SizedBox(
width: double.infinity,
child: TagDialogTextfieldDropdown(
key: ValueKey(
'dropdown_${const Uuid().v4()}_$index'),
product: tag.product?.uuid ?? 'Unknown',
items: state.updatedTags,
initialValue: tag,
onSelected: (value) {
controller.text = value.tag ?? '';
context.read<AssignTagModelBloc>().add(UpdateTag(
index: index,
tag: value,
));
},
),
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue: tag.location ?? 'Main Space',
onSelected: (value) {
context
.read<AssignTagModelBloc>()
.add(UpdateLocation(
index: index,
location: value,
));
},
)),
),
],
);
}),
),
),
if (state.errorMessage != null)
Text(state.errorMessage!,
@@ -118,16 +220,101 @@ class AssignTagModelsDialog extends StatelessWidget {
),
),
actions: [
RowOfSaveCancelWidget(
subspaces: subspaces,
products: products,
allTags: allTags,
spaceName: spaceName,
otherSpaceModels: otherSpaceModels,
pageContext: pageContext,
projectTags: projectTags,
spaceModel: spaceModel,
allSpaceModels: allSpaceModels,
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(state.tags);
final result =
TagHelper.updateSubspaceTagModels(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces = List<SubspaceTemplateModel>.from(
result['subspaces'] as List<dynamic>);
if (context.mounted) {
Navigator.of(context).pop();
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (dialogContext) => AddDeviceTypeModelWidget(
products: products,
subspaces: processedSubspaces,
isCreate: false,
initialSelectedProducts:
TagHelper.createInitialSelectedProducts(
processedTags, processedSubspaces),
allTags: allTags,
spaceName: spaceName,
otherSpaceModels: otherSpaceModels,
spaceTagModels: processedTags,
pageContext: pageContext,
projectTags: projectTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: updatedTags,
uuid: spaceModel?.uuid,
internalId: spaceModel?.internalId,
subspaceModels: processedSubspaces)),
);
}
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: state.isSaveEnabled
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
onPressed: state.isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(state.tags);
final result =
TagHelper.updateSubspaceTagModels(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces = List<SubspaceTemplateModel>.from(
result['subspaces'] as List<dynamic>);
Navigator.of(context).popUntil((route) => route.isFirst);
await showDialog(
context: context,
builder: (BuildContext dialogContext) {
return CreateSpaceModelDialog(
products: products,
allSpaceModels: allSpaceModels,
allTags: allTags,
projectTags: projectTags,
pageContext: pageContext,
otherSpaceModels: otherSpaceModels,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: processedTags,
uuid: spaceModel?.uuid,
internalId: spaceModel?.internalId,
subspaceModels: processedSubspaces),
);
},
);
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
),
],
);
@@ -1,152 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../utils/color_manager.dart';
import '../../../../common/buttons/cancel_button.dart';
import '../../../../common/buttons/default_button.dart';
import '../../../all_spaces/model/product_model.dart';
import '../../../all_spaces/model/tag.dart';
import '../../../helper/tag_helper.dart';
import '../../../space_model/models/space_template_model.dart';
import '../../../space_model/models/subspace_template_model.dart';
import '../../../space_model/widgets/dialog/create_space_model_dialog.dart';
import '../../../tag_model/views/add_device_type_model_widget.dart';
import '../../bloc/assign_tag_model_bloc.dart';
import '../../bloc/assign_tag_model_state.dart';
class RowOfSaveCancelWidget extends StatelessWidget {
const RowOfSaveCancelWidget({
super.key,
required this.subspaces,
required this.products,
required this.allTags,
required this.spaceName,
required this.otherSpaceModels,
required this.pageContext,
required this.projectTags,
required this.spaceModel,
required this.allSpaceModels,
});
final List<SubspaceTemplateModel>? subspaces;
final List<ProductModel>? products;
final List<String>? allTags;
final String spaceName;
final List<String>? otherSpaceModels;
final BuildContext? pageContext;
final List<Tag> projectTags;
final SpaceTemplateModel? spaceModel;
final List<SpaceTemplateModel>? allSpaceModels;
@override
Widget build(BuildContext context) {
return BlocBuilder<AssignTagModelBloc, AssignTagModelState>(
builder: (context, state) {
if (state is AssignTagModelLoaded) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.updateSubspaceTagModels(
updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceTemplateModel>.from(
result['subspaces'] as List<dynamic>);
if (context.mounted) {
Navigator.of(context).pop();
await showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (dialogContext) => AddDeviceTypeModelWidget(
products: products,
subspaces: processedSubspaces,
isCreate: false,
initialSelectedProducts:
TagHelper.createInitialSelectedProducts(
processedTags, processedSubspaces),
allTags: allTags,
spaceName: spaceName,
otherSpaceModels: otherSpaceModels,
spaceTagModels: processedTags,
pageContext: pageContext,
projectTags: projectTags,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: updatedTags,
uuid: spaceModel?.uuid,
internalId: spaceModel?.internalId,
subspaceModels: processedSubspaces)),
);
}
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: state.isSaveEnabled
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
onPressed: state.isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.updateSubspaceTagModels(
updatedTags, subspaces);
final processedTags =
result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceTemplateModel>.from(
result['subspaces'] as List<dynamic>);
Navigator.of(context)
.popUntil((route) => route.isFirst);
await showDialog(
context: context,
builder: (BuildContext dialogContext) {
return CreateSpaceModelDialog(
products: products,
allSpaceModels: allSpaceModels,
allTags: allTags,
projectTags: projectTags,
pageContext: pageContext,
otherSpaceModels: otherSpaceModels,
spaceModel: SpaceTemplateModel(
modelName: spaceName,
tags: processedTags,
uuid: spaceModel?.uuid,
internalId: spaceModel?.internalId,
subspaceModels: processedSubspaces),
);
},
);
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
);
} else {
return const SizedBox();
}
},
);
}
}
@@ -1,155 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:uuid/uuid.dart';
import '../../../../../common/dialog_dropdown.dart';
import '../../../../../common/tag_dialog_textfield_dropdown.dart';
import '../../../../../utils/color_manager.dart';
class AssignTagsTable extends StatelessWidget {
const AssignTagsTable({
super.key,
required this.controllers,
required this.locations,
required this.tags,
required this.updatedTags,
required this.onDeleteDevice,
required this.onLocationDropDownSelected,
required this.onTagDropDownSelected,
});
final void Function({required Tag tag, required int index})
onTagDropDownSelected;
final void Function({required String location, required int index})
onLocationDropDownSelected;
final void Function({required Tag tag, required int index}) onDeleteDevice;
final List<Tag> tags;
final List<Tag> updatedTags;
final List<TextEditingController> controllers;
final List<String> locations;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(tags.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device',
style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label:
Text('Tag', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Location',
style: Theme.of(context).textTheme.bodyMedium)),
],
rows: tags.isEmpty
? [
DataRow(cells: [
DataCell(
Center(
child: Text('No Devices Available',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
)),
),
),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(tags.length, (index) {
final tag = tags[index];
final controller = controllers[index];
return DataRow(
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
onDeleteDevice(tag: tag, index: index);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment
.centerLeft, // Align cell content to the left
child: SizedBox(
width: double.infinity,
child: TagDialogTextfieldDropdown(
key: ValueKey(
'dropdown_${const Uuid().v4()}_$index'),
product: tag.product?.uuid ?? 'Unknown',
items: updatedTags,
initialValue: tag,
onSelected: (value) {
controller.text = value.tag ?? '';
onTagDropDownSelected(tag: value, index: index);
},
),
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue: tag.location ?? 'Main Space',
onSelected: (value) {
onLocationDropDownSelected(
location: value, index: index);
},
)),
),
],
);
}),
),
);
}
}
@@ -77,7 +77,7 @@ class SubSpaceBloc extends Bloc<SubSpaceEvent, SubSpaceState> {
emit(SubSpaceState(
updatedSubSpaces,
updatedSubspaceModels,
errorMessage ,
errorMessage,
updatedDuplicates,
));
});
@@ -1,23 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'widgets/ok_cancel_sub_space_widget.dart';
import 'widgets/textfield_sub_space_dialog_widget.dart';
class CreateSubSpaceDialog extends StatefulWidget {
final String dialogTitle;
final List<SubspaceModel>? existingSubSpaces;
final String? spaceName;
final List<ProductModel>? products;
final void Function(
List<SubspaceModel>?, List<UpdateSubspaceModel> updatedSubSpaces)? onSave;
final void Function(List<SubspaceModel>?)? onSave;
const CreateSubSpaceDialog({
required this.dialogTitle,
@@ -33,7 +33,14 @@ class CreateSubSpaceDialog extends StatefulWidget {
}
class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
final TextEditingController _subspaceNameController = TextEditingController();
late final TextEditingController _subspaceNameController;
@override
void initState() {
_subspaceNameController = TextEditingController();
super.initState();
}
@override
void dispose() {
_subspaceNameController.dispose();
@@ -72,26 +79,83 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
color: ColorsManager.blackColor,
),
),
Row(
children: [
Text(
'press Enter to Save',
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 10,
color: ColorsManager.grayColor,
),
),
const SizedBox(
width: 5,
),
const Icon(Icons.save_as_sharp, size: 10),
],
),
const SizedBox(height: 16),
TextFieldSubSpaceDialogWidget(
subspaceNameController: _subspaceNameController,
subSpaces: state.subSpaces,
Container(
width: context.screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...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 SubspaceChip(
subSpace: SubspaceTemplateModel(
subspaceName: entry.value.subspaceName,
disabled: entry.value.disabled,
),
isDuplicate: isDuplicate,
onDeleted: () => context.read<SubSpaceBloc>().add(
RemoveSubSpace(subSpace),
),
);
},
),
SizedBox(
width: 200,
child: TextField(
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: state.subSpaces.isEmpty
? 'Please enter the name'
: null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
onSubmitted: (value) {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
context.read<SubSpaceBloc>().add(
AddSubSpace(
SubspaceModel(
subspaceName: trimmedValue,
disabled: false,
),
),
);
_subspaceNameController.clear();
}
},
style: context.textTheme.bodyMedium,
),
),
],
),
),
if (state.errorMessage.isNotEmpty)
Padding(
@@ -104,10 +168,36 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
),
),
const SizedBox(height: 16),
OkCancelSubSpaceWidget(
subspaceNameController: _subspaceNameController,
errorMessage: state.errorMessage,
onSave: widget.onSave,
Row(
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: state.errorMessage.isEmpty
? () {
final subSpacesBloc = context.read<SubSpaceBloc>();
final subSpaces = subSpacesBloc.state.subSpaces;
widget.onSave?.call(subSpaces);
Navigator.of(context).pop();
}
: null,
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: state.errorMessage.isNotEmpty
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
),
],
),
@@ -1,75 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import '../../bloc/subspace_bloc.dart';
import '../../bloc/subspace_event.dart';
class OkCancelSubSpaceWidget extends StatelessWidget {
const OkCancelSubSpaceWidget({
super.key,
required this.subspaceNameController,
required this.onSave,
required this.errorMessage,
});
final TextEditingController subspaceNameController;
final void Function(
List<SubspaceModel>?, List<UpdateSubspaceModel> updatedSubSpaces)? onSave;
final String errorMessage;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: errorMessage.isEmpty
? () async {
final trimmedValue = subspaceNameController.text.trim();
final subSpacesBloc = context.read<SubSpaceBloc>();
if (trimmedValue.isNotEmpty) {
subSpacesBloc.add(
AddSubSpace(
SubspaceModel(
subspaceName: trimmedValue,
disabled: false,
),
),
);
subspaceNameController.clear();
}
await Future.delayed(const Duration(milliseconds: 10));
final subSpaces = subSpacesBloc.state.subSpaces;
onSave?.call(
subSpaces, subSpacesBloc.state.updatedSubSpaceModels);
Navigator.of(context).pop();
}
: null,
backgroundColor: ColorsManager.secondaryColor,
borderRadius: 10,
foregroundColor: errorMessage.isNotEmpty
? ColorsManager.whiteColorsWithOpacity
: ColorsManager.whiteColors,
child: const Text('OK'),
),
),
],
);
}
}
@@ -1,98 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import '../../../../../utils/color_manager.dart';
import '../../../all_spaces/model/subspace_model.dart';
import '../../../create_subspace_model/widgets/subspace_chip.dart';
import '../../../space_model/models/subspace_template_model.dart';
import '../../bloc/subspace_bloc.dart';
import '../../bloc/subspace_event.dart';
class TextFieldSubSpaceDialogWidget extends StatelessWidget {
const TextFieldSubSpaceDialogWidget({
super.key,
required TextEditingController subspaceNameController,
required this.subSpaces,
}) : _subspaceNameController = subspaceNameController;
final TextEditingController _subspaceNameController;
final List<SubspaceModel> subSpaces;
@override
Widget build(BuildContext context) {
return Container(
width: context.screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.subspaceName.toLowerCase();
final duplicateIndices = 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 SubspaceChip(
subSpace: SubspaceTemplateModel(
subspaceName: entry.value.subspaceName,
disabled: entry.value.disabled,
),
isDuplicate: isDuplicate,
onDeleted: () => context.read<SubSpaceBloc>().add(
RemoveSubSpace(subSpace),
),
);
},
),
SizedBox(
width: 200,
child: TextField(
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: subSpaces.isEmpty ? 'Please enter the name' : null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
onSubmitted: (value) {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
context.read<SubSpaceBloc>().add(
AddSubSpace(
SubspaceModel(
subspaceName: trimmedValue,
disabled: false,
),
),
);
_subspaceNameController.clear();
}
},
style: context.textTheme.bodyMedium,
),
),
],
),
);
}
}
@@ -14,11 +14,11 @@ class CenterBodyWidget extends StatelessWidget {
if (state is InitialState) {
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
}
if (state is CommunityStructureState) {
if (state is CommunityStructureState) {
context.read<SpaceManagementBloc>().add(BlankStateEvent(context));
}
if (state is SpaceModelState) {
if (state is SpaceModelState) {
context.read<SpaceManagementBloc>().add(SpaceModelLoadEvent(context));
}
@@ -31,19 +31,15 @@ class CenterBodyWidget extends StatelessWidget {
children: [
GestureDetector(
onTap: () {
context
.read<CenterBodyBloc>()
.add(CommunityStructureSelectedEvent());
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
},
child: Text(
'Community Structure',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is CommunityStructureState ||
state is CommunitySelectedState
fontWeight: state is CommunityStructureState || state is CommunitySelectedState
? FontWeight.bold
: FontWeight.normal,
color: state is CommunityStructureState ||
state is CommunitySelectedState
color: state is CommunityStructureState || state is CommunitySelectedState
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
@@ -54,26 +50,26 @@ class CenterBodyWidget extends StatelessWidget {
),
),
const SizedBox(width: 20),
// GestureDetector(
// onTap: () {
// context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
// },
// child: Text(
// 'Space Model',
// style: Theme.of(context).textTheme.bodyLarge!.copyWith(
// fontWeight: state is SpaceModelState
// ? FontWeight.bold
// : FontWeight.normal,
// color: state is SpaceModelState
// ? Theme.of(context).textTheme.bodyLarge!.color
// : Theme.of(context)
// .textTheme
// .bodyLarge!
// .color!
// .withOpacity(0.5),
// ),
// ),
// ),
GestureDetector(
onTap: () {
context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
},
child: Text(
'Space Model',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is SpaceModelState
? FontWeight.bold
: FontWeight.normal,
color: state is SpaceModelState
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
.bodyLarge!
.color!
.withOpacity(0.5),
),
),
),
],
),
],
@@ -24,22 +24,37 @@ class AddDeviceTypeModelBloc
if (currentState is AddDeviceModelLoaded) {
final existingProduct = currentState.selectedProducts.firstWhere(
(p) => p.productId == event.selectedProduct.productId,
orElse: () => event.selectedProduct,
(p) => p.productId == event.productId,
orElse: () => SelectedProduct(
productId: event.productId,
count: 0,
productName: event.productName,
product: event.product,
),
);
List<SelectedProduct> updatedProducts;
if (event.selectedProduct.count > 0) {
if (event.count > 0) {
if (!currentState.selectedProducts.contains(existingProduct)) {
updatedProducts = [
...currentState.selectedProducts,
event.selectedProduct,
SelectedProduct(
productId: event.productId,
count: event.count,
productName: event.productName,
product: event.product,
),
];
} else {
updatedProducts = currentState.selectedProducts.map((p) {
if (p.productId == event.selectedProduct.productId) {
return event.selectedProduct;
if (p.productId == event.productId) {
return SelectedProduct(
productId: p.productId,
count: event.count,
productName: p.productName,
product: p.product,
);
}
return p;
}).toList();
@@ -47,7 +62,7 @@ class AddDeviceTypeModelBloc
} else {
// Remove the product if the count is 0
updatedProducts = currentState.selectedProducts
.where((p) => p.productId != event.selectedProduct.productId)
.where((p) => p.productId != event.productId)
.toList();
}
@@ -10,15 +10,20 @@ abstract class AddDeviceTypeModelEvent extends Equatable {
List<Object> get props => [];
}
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
final SelectedProduct selectedProduct;
UpdateProductCountEvent({required this.selectedProduct});
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
final String productId;
final int count;
final String productName;
final ProductModel product;
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
@override
List<Object> get props => [selectedProduct];
List<Object> get props => [productId, count];
}
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
final List<Tag> initialTags;
final List<SelectedProduct> addedProducts;
@@ -54,10 +54,10 @@ class DeviceTypeTileWidget extends StatelessWidget {
onCountChanged: (newCount) {
context.read<AddDeviceTypeModelBloc>().add(
UpdateProductCountEvent(
selectedProduct: product.toSelectedProduct(
newCount,
),
),
productId: product.uuid,
count: newCount,
productName: product.catName,
product: product),
);
},
),
@@ -38,7 +38,7 @@ class VisitorPasswordDialog extends StatelessWidget {
if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty)
Column(
children: [
const Text('Failed Devises'),
const Text('Failed Devices'),
SizedBox(
width: 200,
height: 50,
@@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget {
if (visitorBloc.passwordStatus!.successOperations.isNotEmpty)
Column(
children: [
const Text('Success Devises'),
const Text('Success Devices'),
SizedBox(
width: 200,
height: 50,
-12
View File
@@ -22,18 +22,6 @@ class HTTPService {
);
client.interceptors.add(serviceLocator.get<HTTPInterceptor>());
// Add this interceptor for logging requests and responses
// client.interceptors.add(
// LogInterceptor(
// request: true,
// requestHeader: true,
// requestBody: true,
// responseHeader: false,
// responseBody: true,
// error: true,
// logPrint: (object) => print(object),
// ),
// );
return client;
}
+24 -44
View File
@@ -4,27 +4,22 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_m
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_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/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
import '../pages/spaces_management/all_spaces/model/subspace_model.dart';
class CommunitySpaceManagementApi {
// Community Management APIs
Future<List<CommunityModel>> fetchCommunities(String projectId,
{int page = 1}) async {
Future<List<CommunityModel>> fetchCommunities(String projectId, {int page = 1}) async {
try {
List<CommunityModel> allCommunities = [];
bool hasNext = true;
while (hasNext) {
await HTTPService().get(
path: ApiEndpoints.getCommunityList
.replaceAll('{projectId}', projectId),
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {
'page': page,
},
@@ -60,14 +55,8 @@ class CommunitySpaceManagementApi {
try {
bool hasNext = false;
await HTTPService().get(
path:
ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {
'page': page,
'includeSpaces': true,
'size': 25,
'search': search
},
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search},
expectedResponseModel: (json) {
try {
List<dynamic> jsonData = json['data'] ?? [];
@@ -79,10 +68,7 @@ class CommunitySpaceManagementApi {
page = currentPage + 1;
paginationModel = PaginationModel(
pageNum: page,
hasNext: hasNext,
size: 25,
communities: communityList);
pageNum: page, hasNext: hasNext, size: 25, communities: communityList);
return paginationModel;
} catch (_) {
hasNext = false;
@@ -97,8 +83,7 @@ class CommunitySpaceManagementApi {
Future<CommunityModel?> getCommunityById(String communityId) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.getCommunityById
.replaceAll('{communityId}', communityId),
path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) {
return CommunityModel.fromJson(json['data']);
},
@@ -110,8 +95,7 @@ class CommunitySpaceManagementApi {
}
}
Future<CommunityModel?> createCommunity(
String name, String description, String projectId) async {
Future<CommunityModel?> createCommunity(String name, String description, String projectId) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId),
@@ -130,8 +114,7 @@ class CommunitySpaceManagementApi {
}
}
Future<bool> updateCommunity(
String communityId, String name, String projectId) async {
Future<bool> updateCommunity(String communityId, String name, String projectId) async {
try {
final response = await HTTPService().put(
path: ApiEndpoints.updateCommunity
@@ -168,8 +151,7 @@ class CommunitySpaceManagementApi {
}
}
Future<SpacesResponse> fetchSpaces(
String communityId, String projectId) async {
Future<SpacesResponse> fetchSpaces(String communityId, String projectId) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.listSpaces
@@ -195,8 +177,7 @@ class CommunitySpaceManagementApi {
}
}
Future<SpaceModel?> getSpace(
String communityId, String spaceId, String projectId) async {
Future<SpaceModel?> getSpace(String communityId, String spaceId, String projectId) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.getSpace
@@ -218,6 +199,7 @@ class CommunitySpaceManagementApi {
{required String communityId,
required String name,
String? parentId,
String? direction,
bool isPrivate = false,
required Offset position,
String? spaceModelUuid,
@@ -231,6 +213,7 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate,
'x': position.dx,
'y': position.dy,
'direction': direction,
'icon': icon,
};
if (parentId != null) {
@@ -265,10 +248,11 @@ class CommunitySpaceManagementApi {
required String name,
String? parentId,
String? icon,
String? direction,
bool isPrivate = false,
required Offset position,
List<Tag>? tags,
List<SubspaceModel>? subspaces,
List<TagModelUpdate>? tags,
List<UpdateSubspaceTemplateModel>? subspaces,
String? spaceModelUuid,
required String projectId}) async {
try {
@@ -277,8 +261,9 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate,
'x': position.dx,
'y': position.dy,
'direction': direction,
'icon': icon,
'subspaces': subspaces,
'subspace': subspaces,
'tags': tags,
'spaceModelUuid': spaceModelUuid,
};
@@ -304,8 +289,7 @@ class CommunitySpaceManagementApi {
}
}
Future<bool> deleteSpace(
String communityId, String spaceId, String projectId) async {
Future<bool> deleteSpace(String communityId, String spaceId, String projectId) async {
try {
final response = await HTTPService().delete(
path: ApiEndpoints.deleteSpace
@@ -323,17 +307,15 @@ class CommunitySpaceManagementApi {
}
}
Future<List<SpaceModel>> getSpaceHierarchy(
String communityId, String projectId) async {
Future<List<SpaceModel>> getSpaceHierarchy(String communityId, String projectId) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceHierarchy
.replaceAll('{communityId}', communityId)
.replaceAll('{projectId}', projectId),
expectedResponseModel: (json) {
final spaceModels = (json['data'] as List)
.map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
final spaceModels =
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList();
return spaceModels;
},
@@ -345,17 +327,15 @@ class CommunitySpaceManagementApi {
}
}
Future<List<SpaceModel>> getSpaceOnlyWithDevices(
{String? communityId, String? projectId}) async {
Future<List<SpaceModel>> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.spaceOnlyWithDevices
.replaceAll('{communityId}', communityId!)
.replaceAll('{projectId}', projectId!),
expectedResponseModel: (json) {
final spaceModels = (json['data'] as List)
.map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
final spaceModels =
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList();
return spaceModels;
},
);
+14 -23
View File
@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
@@ -9,23 +7,17 @@ import 'package:syncrow_web/utils/constants/api_const.dart';
class SpaceModelManagementApi {
Future<List<SpaceTemplateModel>> listSpaceModels(
{required String projectId, int page = 1}) async {
try {
// final response = await HTTPService().get(
// path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId),
// queryParameters: {'page': page},
// expectedResponseModel: (json) {
// List<dynamic> jsonData = json['data'];
// return jsonData.map((jsonItem) {
// return SpaceTemplateModel.fromJson(jsonItem);
// }).toList();
// },
// );
return [];
// response;
} catch (e) {
log(e.toString());
return [];
}
final response = await HTTPService().get(
path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId),
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
return jsonData.map((jsonItem) {
return SpaceTemplateModel.fromJson(jsonItem);
}).toList();
},
);
return response;
}
Future<SpaceTemplateModel?> createSpaceModel(
@@ -41,8 +33,8 @@ class SpaceModelManagementApi {
return response;
}
Future<String?> updateSpaceModel(CreateSpaceTemplateBodyModel spaceModel,
String spaceModelUuid, String projectId) async {
Future<String?> updateSpaceModel(
CreateSpaceTemplateBodyModel spaceModel, String spaceModelUuid, String projectId) async {
final response = await HTTPService().put(
path: ApiEndpoints.updateSpaceModel
.replaceAll('{projectId}', projectId)
@@ -55,8 +47,7 @@ class SpaceModelManagementApi {
return response;
}
Future<SpaceTemplateModel?> getSpaceModel(
String spaceModelUuid, String projectId) async {
Future<SpaceTemplateModel?> getSpaceModel(String spaceModelUuid, String projectId) async {
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceModel
.replaceAll('{projectId}', projectId)