Compare commits

...
Author SHA1 Message Date
raf-dev1 aa3b79bdaf stop fetching tags onBlank Event 2025-06-10 16:34:49 +03:00
raf-dev1 0e31a3ea96 no need to fetch use info in init state of homepage and agreement dialog 2025-06-10 16:33:33 +03:00
raf-dev1 fd192894cd stop calling initEvent for spactree bloc inside homeBloc 2025-06-10 16:32:33 +03:00
raf-dev1 692c9e7792 comment (SpaceModelLinking && Or)Widgets cuz no need for spaceModel for now&& fix the delete devices from subSpaces to keep in main space not to get deleted 2025-06-10 15:11:33 +03:00
raf-dev1 08a9a5c71f comment the tab of spaceModel to prevent routing there 2025-06-10 15:08:50 +03:00
raf-dev1 7eb1d5b0b0 comment listSpace func which calls SpaceModels that suspended for now 2025-06-10 15:07:32 +03:00
raf-dev1 0d5734a236 use tag instead of UpdatedTagModel cuz no need for updatedmodel anymore&&key should be spaces with S 2025-06-10 15:06:31 +03:00
raf-dev1 e22bab00d9 just format code in assign tag bloc 2025-06-10 10:27:08 +03:00
raf-dev1 d2a2d391e0 send subspaces with onSave in okCancel inside subspace dialog as parameters 2025-06-10 10:19:48 +03:00
raf-dev1 1d30c753f5 edit tag model keys to integrate with backend 2025-06-10 10:08:49 +03:00
raf-dev1 ca02de2093 edit subspace model to fix keys and integrate with backend 2025-06-10 10:07:26 +03:00
raf-dev1 8f7bfa984b edit spacemodel to use the right keys to integrate with backend 2025-06-10 10:06:36 +03:00
raf-dev1 8e9278c93c edits with faris and main task to fix loading state forever and prevent rethrow exceptions without catching them 2025-06-10 10:05:50 +03:00
raf-dev1 15d3a05553 assign tag dropDown now show all Tags without condition 2025-06-10 10:03:32 +03:00
raf-dev1 a1d7457065 Test commit with updated user 2025-05-29 14:34:15 +03:00
Rafeek Alkhoudare c99b32fb81 cancel direction 2025-05-28 06:50:04 -05:00
Rafeek Alkhoudare 321df401fd comment interceptor cuz it is crashing app 2025-05-28 01:56:57 -05:00
Rafeek Alkhoudare ee244fa5ed use textfield controller from constructor 2025-05-28 01:48:37 -05:00
Rafeek Alkhoudare 1db069e9a5 use devicePart in main widget 2025-05-28 01:21:06 -05:00
Rafeek Alkhoudare cf9bafef4d add devices part widget 2025-05-28 01:20:31 -05:00
Rafeek Alkhoudare 2c73dd6c31 call the new widgets 2025-05-28 00:42:05 -05:00
Rafeek Alkhoudare 6ec20e2d72 seperate subspace part into widget 2025-05-28 00:41:53 -05:00
Rafeek Alkhoudare 4feae9ad87 seperate spacemodel linking into widget 2025-05-28 00:41:33 -05:00
Rafeek Alkhoudare 52046909d5 seperate textfield of the space name 2025-05-28 00:41:01 -05:00
Rafeek Alkhoudare fc81555be3 seperate iconchoose widget in create space dialog 2025-05-28 00:40:34 -05:00
Rafeek Alkhoudare 8967852ca8 seperate assign tag dialog to widgets 2025-05-27 06:01:25 -05:00
Rafeek Alkhoudare a87e79878b add ok_candel row of buttons in seperated widget also text field widget 2025-05-27 05:32:42 -05:00
Rafeek Alkhoudare 056e7372e0 use assign table as widget 2025-05-27 01:15:30 -05:00
Rafeek Alkhoudare d69d867120 use assign_tag_dialog_widgetwidget 2025-05-26 06:27:48 -05:00
Rafeek Alkhoudare 644fe56478 seperaate UI into widgets 2025-05-26 06:12:24 -05:00
Rafeek Alkhoudare 766a39f161 use selected model in add device event 2025-05-26 06:11:48 -05:00
Rafeek Alkhoudare c97dd40b05 add toSelected function to convert from product to selectedProduct 2025-05-26 06:11:06 -05:00
Rafeek Alkhoudare 0b65c58947 fix add subspace bugs and plusButton widget 2025-05-26 02:45:58 -05:00
Rafeek Alkhoudare e0951aa13d add interceptor to print request and response 2025-05-23 08:52:53 -05:00
Rafeek Alkhoudare 9e8ebf3768 no need for models and blocs for now 2025-05-23 07:52:21 -05:00
Rafeek Alkhoudare b593e75c67 build main structure and build data layer and space managment bloc 2025-05-23 02:17:23 -05:00
Rafeek Alkhoudare 9eaa367d32 fix horizontal scroll bar 2025-05-22 05:48:49 -05:00
Faris Armoush 24372a0618 Merge pull request #198 from SyncrowIOT/SP-1580-FE-Watermark-Does-Not-Match-Design-Specification
SP-1580-FE-Watermark-Does-Not-Match-Design-Specification
2025-05-22 11:25:49 +03:00
Faris Armoush 8988947694 Merge pull request #191 from SyncrowIOT/syncrow_analytics_sidebar_selection_behavior
Syncrow analytics sidebar selection behavior
2025-05-22 11:25:22 +03:00
Faris Armoush ef875ef7dc Merge pull request #197 from SyncrowIOT/SP-1510-occupancy_chart_api_integration
Sp 1510 occupancy chart api integration
2025-05-22 11:24:55 +03:00
Faris Armoush 94e4fbd5db Apply correct business logic in OccupancyDataLoadingStrategy. 2025-05-21 16:08:48 +03:00
Faris Armoush 302ef36b17 Merge branch 'dev' of https://github.com/SyncrowIOT/web into syncrow_analytics_sidebar_selection_behavior 2025-05-21 15:56:29 +03:00
Faris Armoush c508d016c2 SP-1580-FE-Watermark-Does-Not-Match-Design-Specification 2025-05-21 11:08:00 +03:00
Faris Armoush e0ad7855d3 converted GetOccupancyParam.toJson to an expression method. 2025-05-21 10:59:04 +03:00
Faris Armoush ecf588cfcb reverted to dynamic endpoint. 2025-05-21 10:58:21 +03:00
Faris Armoush c9d15d102b fixes in OccupancyChart for a more pleasant UI. 2025-05-21 10:57:53 +03:00
mohammadnemer1 64a29681de Merge pull request #196 from SyncrowIOT/SP-1475-FE-Only-the-arrow-button-is-clickable-make-the-whole-name-clickable-with-the-arrow
Sp 1475 fe only the arrow button is clickable make the whole name clickable with the arrow
2025-05-21 10:31:25 +03:00
Faris Armoush 02b07cfdb6 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1510-occupancy_chart_api_integration 2025-05-21 10:23:56 +03:00
Faris Armoush 0a94557eee SP-1510-Occupancy Chart API Integration. 2025-05-21 10:23:31 +03:00
Faris Armoush 4f8d1c4ffd Merge pull request #195 from SyncrowIOT/charts-reworks
Charts reworks
2025-05-21 10:22:55 +03:00
mohammad 06b320a75d move icon to the center and change subspace title name 2025-05-21 10:16:12 +03:00
Faris Armoush 000fe70663 format. 2025-05-21 09:59:50 +03:00
Faris Armoush 4257f7f0f3 Corrected color of titles in charts. 2025-05-21 09:55:17 +03:00
Faris Armoush b2bf3866a9 Deleted pubspec.lock, and added it to .gitignore. 2025-05-21 09:09:32 +03:00
mohammad a15b5439f0 Refactor user dropdown menu to display user's full name and arrow icon in a row for better layout consistency 2025-05-20 16:39:10 +03:00
Faris Armoush fd2a09cada Deleted unused FakeEnergyConsumptionPerDeviceService. 2025-05-20 14:22:23 +03:00
Faris Armoush 4c2802acfc date picker decorations matched with design. 2025-05-20 14:20:16 +03:00
Faris Armoush 15343be258 show space uuid in analytics devices dropdown. 2025-05-20 14:11:25 +03:00
Faris Armoush c21842cc6d removed overflow and fixed sizing and text drawing of PowerClampEnergyStatusWidget. 2025-05-20 13:56:00 +03:00
Faris Armoush 4326559e14 shows OccupancyHeatMapBox instead of a Placeholder in vertical srcollable AnalyticsOccupancyView. 2025-05-20 13:51:04 +03:00
mohammadnemer1 4ded7d5202 Merge pull request #194 from SyncrowIOT/SP-1448-FE-Use-SliderValueSelector-widget-for-all-slider-widgets-in-Web-Routine
add step parameter in onTapFunction.
2025-05-19 11:37:56 +03:00
Faris Armoush 625f737791 SP-1506 rework
Remove extra line.

The colors of the data on X axis and Y axis are not identical to design.

Display days only on the X axis.

When the bar chart loads, we see it coming from the top (check the attached video).
2025-05-19 11:08:26 +03:00
Faris Armoush 494ae1c941 SP-1495 reworks.
1. Overlapping line not removed.
2. The colors of the data on X axis and Y axis are not identical to design.
3. Day 1 and 2 are missing on the X axis.
4. When the chart loads, we see it coming from the top right corner (check the attached video).
5. Display all available devices even if they have no data and make the chart empty state.
2025-05-19 10:52:44 +03:00
Faris Armoush f67d0e2912 SP-1494 reworks.
1. When the chart loads, we see it coming from the top right corner (check the attached video).
2. Day 1 is missing on the X axis.
3. Overlapping line not removed.
2025-05-19 10:17:48 +03:00
Faris Armoush 17aad13b2a Merge pull request #193 from SyncrowIOT/feature/make_analytics_date_picker_not_show_future_dates
Feature/make_analytics_date_picker_not_show_future_dates
2025-05-15 16:58:25 +03:00
Faris Armoush a849c1dafb removed unused import. 2025-05-15 16:31:11 +03:00
Faris Armoush 3e3e17019a format. 2025-05-15 16:22:54 +03:00
Faris Armoush b1bae3cb15 fixed overflow bug on charts. 2025-05-15 15:59:02 +03:00
Faris Armoush 051bf657ed Changed background color of analytics date pickers to match the design language of the platform. 2025-05-15 15:29:09 +03:00
Faris Armoush 5191c1e456 Performed selection validation, and made future dates disabled. 2025-05-15 15:28:36 +03:00
Faris Armoush 7a073f10aa Merge pull request #189 from SyncrowIOT/1495-calendar-bugfixes
1495 calendar bugfixes
2025-05-15 14:31:11 +03:00
Faris Armoush 900d47faae Merge pull request #190 from SyncrowIOT/SP-1506-FE-implement-chart-per-phase
SP-1506-FE-chart per phase api integration.
2025-05-15 14:30:58 +03:00
Faris Armoush e35a7fdc70 Merge pull request #192 from SyncrowIOT/bugfix/charts-horizontal-lines
bugfix/charts-horizontal-lines
2025-05-15 14:30:37 +03:00
Faris Armoush d80f5e1f3a Refactor energy consumption charts to enhance grid data configuration
Updated the grid data for EnergyConsumptionByPhasesChart, EnergyConsumptionPerDeviceChart, and TotalEnergyConsumptionChart to include horizontal line visibility and set a horizontal interval of 250. Removed unused phasesJson constant from TotalEnergyConsumptionChart for cleaner code.
2025-05-15 14:25:13 +03:00
Faris Armoush baaf5111b1 Applied correct business logic in EnergyManagementDataLoadingStrategy. 2025-05-15 12:48:18 +03:00
Faris Armoush 745205063e added correct behavior to OccupancyDataLoadingStrategy. 2025-05-15 12:46:12 +03:00
Faris Armoush c07b53107e SP-1506-FE-chart per phase api integration. 2025-05-15 10:51:09 +03:00
Faris Armoush 39d125ac7e loads energy management data on date changed. 2025-05-15 10:11:55 +03:00
Faris Armoush ad15d0e138 loads occupancy chart on date changed. 2025-05-15 10:08:41 +03:00
Faris Armoush e6d272a60d loads heatmap data on calendar change. 2025-05-15 10:06:13 +03:00
Faris Armoush 8dfe8d10d4 removed requestType from query parameters of RemoteOccupancyAnalyticsDevicesService._makeRequest. 2025-05-15 10:01:43 +03:00
Faris Armoush 5279020d08 Merge pull request #188 from SyncrowIOT/1495-energy-consumption-per-device-api-integration
1495-energy-consumption-per-device-api-integration.
2025-05-15 09:32:15 +03:00
Faris Armoush da481536c4 1495-energy-consumption-per-device-api-integration. 2025-05-14 16:55:28 +03:00
Faris Armoush f21366268a Merge pull request #187 from SyncrowIOT/SP-1509-FE-Implement-devices-status-based-on-the-selected-device-from-the-dropdown-list
Sp 1509 fe implement devices status based on the selected device from the dropdown list
2025-05-14 16:18:51 +03:00
Faris Armoush c3aef736fd Merge pull request #186 from SyncrowIOT/1511-occupancy-heat-map-tooltip
1511-occupancy-heat-map-tooltip.
2025-05-14 16:18:08 +03:00
Faris Armoush 887ac58f40 fixed import. 2025-05-14 15:59:40 +03:00
Faris Armoush c709477500 some refactors to further clarify intent. 2025-05-14 15:55:12 +03:00
Faris Armoush 63e7b3faa2 resets selection and clears data. 2025-05-14 15:47:07 +03:00
Faris Armoush 0e61e52bf8 Connected devices to widgets, and is currently making the necessary and correct api calls for everything to function properly. 2025-05-14 15:35:22 +03:00
Faris Armoush 7515b347ce analytics devices integtation. 2025-05-14 15:03:30 +03:00
Faris Armoush 3dfbcb5935 connect device dropdown to bloc. 2025-05-14 14:31:28 +03:00
Faris Armoush 4fd4a9b5bf loads analytics devices on sidebar selection. 2025-05-14 13:03:51 +03:00
Faris Armoush 14fa1b355e Added a uuid property to AnalyticsDevice. 2025-05-14 12:50:27 +03:00
Faris Armoush 78d4e58996 Added selected device state/event, and clear data event to AnalyticsDevicesBloc. 2025-05-14 12:50:16 +03:00
Faris Armoush 23b9cb5b78 Injected AnalyticsDevicesBloc into AnalyticsPage. 2025-05-14 12:42:51 +03:00
Faris Armoush 401d0a9788 Created AnalyticsDevicesBloc. 2025-05-14 12:41:44 +03:00
Faris Armoush ac2b0d3fac Created an initial remote implementation of AnalyticsDevicesService. 2025-05-14 12:38:07 +03:00
Faris Armoush 3be7a377c0 Created AnalyticsDevicesService interface. 2025-05-14 12:37:52 +03:00
Faris Armoush e4ee456384 Created empty AnalyticsDevice model. 2025-05-14 12:37:44 +03:00
Faris Armoush f02c5d71ba Created GetAnalyticsDevicesParam. 2025-05-14 12:26:16 +03:00
Faris Armoush d45ff262c7 Merge branch 'dev' into 1511-occupancy-heat-map-tooltip 2025-05-14 12:05:34 +03:00
Faris Armoush ad227febc1 Merge pull request #185 from SyncrowIOT/SP-1512-FE-Apply-Responsive-Behavior-for-Dashboard-Layout-and-Sidebar-Collapse
Sp 1512 fe apply responsive behavior for dashboard layout and sidebar collapse
2025-05-14 12:04:41 +03:00
Faris Armoush a9d6c6f4ee 1511-occupancy-heat-map-tooltip. 2025-05-14 12:03:47 +03:00
Faris Armoush 4d9e57c8b5 Created and connected a remote implementation that fetches the heat map occupancy per space from the API. 2025-05-14 10:51:37 +03:00
Faris Armoush d1bb8da484 Updated OccupancyHeatMapModel model with what the api returns, and only used the necessary fields that the api returns for this feature to work. 2025-05-14 10:51:19 +03:00
Faris Armoush 300f9ae358 Matched the GetOccupancyHeatMapParam with what the API expects and removed the communityId since it is no longer necessary for the api, and renamed spaceId to spaceUuid for more clarity. 2025-05-14 10:49:32 +03:00
Faris Armoush c1dab3400b removed a force unwrap from OccupancyHeatMap._maxValue to avoid any bugs. 2025-05-14 10:48:28 +03:00
Faris Armoush 46815585cb Fixed error in AnalyticsErrorWidget where it used to add the default error message to the errorMessage. 2025-05-14 10:47:54 +03:00
104 changed files with 3479 additions and 2762 deletions
+1
View File
@@ -30,6 +30,7 @@ migrate_working_dir/
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
pubspec.lock
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+18 -5
View File
@@ -17,7 +17,8 @@ class TagDialogTextfieldDropdown extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
_DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState(); _DialogTextfieldDropdownState createState() =>
_DialogTextfieldDropdownState();
} }
class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> { class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
@@ -36,6 +37,12 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
_focusNode.addListener(() { _focusNode.addListener(() {
if (!_focusNode.hasFocus) { 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(); _closeDropdown();
} }
}); });
@@ -43,7 +50,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
void _filterItems() { void _filterItems() {
setState(() { setState(() {
_filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList(); _filteredItems = widget.items;
// .where((tag) => tag.product?.uuid == widget.product)
// .toList();
}); });
} }
@@ -112,7 +121,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyMedium .bodyMedium
?.copyWith(color: ColorsManager.textPrimaryColor)), ?.copyWith(
color: ColorsManager
.textPrimaryColor)),
onTap: () { onTap: () {
_controller.text = tag.tag ?? ''; _controller.text = tag.tag ?? '';
widget.onSelected(tag); widget.onSelected(tag);
@@ -156,13 +167,15 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
controller: _controller, controller: _controller,
focusNode: _focusNode, focusNode: _focusNode,
onFieldSubmitted: (value) { onFieldSubmitted: (value) {
final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value, final selectedTag = _filteredItems.firstWhere(
(tag) => tag.tag == value,
orElse: () => Tag(tag: value)); orElse: () => Tag(tag: value));
widget.onSelected(selectedTag); widget.onSelected(selectedTag);
_closeDropdown(); _closeDropdown();
}, },
onTapOutside: (event) { 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))); orElse: () => Tag(tag: _controller.text)));
_closeDropdown(); _closeDropdown();
}, },
+16
View File
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,36 @@
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
@@ -0,0 +1,3 @@
class EndPoints {
static const String fetchCommunities = 'projects/{projectUuid}/communities';
}
+50
View File
@@ -0,0 +1,50 @@
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
@@ -0,0 +1,121 @@
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,71 @@
class AnalyticsDevice {
const AnalyticsDevice({
required this.uuid,
required this.name,
this.createdAt,
this.updatedAt,
this.deviceTuyaUuid,
this.isActive,
this.productDevice,
this.spaceUuid,
});
final String uuid;
final String name;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? deviceTuyaUuid;
final bool? isActive;
final ProductDevice? productDevice;
final String? spaceUuid;
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
deviceTuyaUuid: json['deviceTuyaUuid'] as String?,
isActive: json['isActive'] as bool?,
productDevice: json['productDevice'] != null
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: (json['spaces'] as List<dynamic>?)
?.map((e) => e['uuid'])
.firstOrNull
?.toString(),
);
}
}
class ProductDevice {
const ProductDevice({
this.uuid,
this.createdAt,
this.updatedAt,
this.catName,
this.prodId,
this.name,
this.prodType,
});
final String? uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? catName;
final String? prodId;
final String? name;
final String? prodType;
factory ProductDevice.fromJson(Map<String, dynamic> json) {
return ProductDevice(
uuid: json['uuid'] as String?,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
catName: json['catName'] as String?,
prodId: json['prodId'] as String?,
name: json['name'] as String?,
prodType: json['prodType'] as String?,
);
}
}
+19 -5
View File
@@ -1,18 +1,32 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class Occupacy extends Equatable { class Occupacy extends Equatable {
final String date; final DateTime date;
final String occupancy; final String occupancy;
final String spaceUuid;
final int occupiedSeconds;
const Occupacy({required this.date, required this.occupancy}); const Occupacy({
required this.date,
required this.occupancy,
required this.spaceUuid,
required this.occupiedSeconds,
});
factory Occupacy.fromJson(Map<String, dynamic> json) { factory Occupacy.fromJson(Map<String, dynamic> json) {
return Occupacy( return Occupacy(
date: json['date'] as String, date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'),
occupancy: json['occupancy'] as String, occupancy: (json['occupancy_percentage'] ?? 0).toString(),
spaceUuid: json['space_uuid'] as String? ?? '',
occupiedSeconds: json['occupied_seconds'] as int? ?? 0,
); );
} }
@override @override
List<Object?> get props => [date, occupancy]; List<Object?> get props => [
date,
occupancy,
spaceUuid,
occupiedSeconds,
];
} }
@@ -1,22 +1,28 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class OccupancyHeatMapModel extends Equatable { class OccupancyHeatMapModel extends Equatable {
final DateTime date; final String uuid;
final int occupancy; final DateTime eventDate;
final int countTotalPresenceDetected;
const OccupancyHeatMapModel({ const OccupancyHeatMapModel({
required this.date, required this.uuid,
required this.occupancy, required this.eventDate,
required this.countTotalPresenceDetected,
}); });
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) { factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
return OccupancyHeatMapModel( return OccupancyHeatMapModel(
date: DateTime.parse(json['date'] as String), uuid: json['uuid'] as String? ?? '',
occupancy: json['occupancy'] as int, eventDate: DateTime.parse(
json['event_date'] as String? ?? '${DateTime.now()}',
),
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
); );
} }
@override @override
List<Object?> get props => [date, occupancy]; List<Object?> get props => [uuid, eventDate, countTotalPresenceDetected];
} }
@@ -1,27 +1,66 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class PhasesEnergyConsumption extends Equatable { class PhasesEnergyConsumption extends Equatable {
final int month; final String uuid;
final double phaseA; final DateTime createdAt;
final double phaseB; final DateTime updatedAt;
final double phaseC; final String deviceUuid;
final DateTime date;
final double energyConsumedKw;
final double energyConsumedA;
final double energyConsumedB;
final double energyConsumedC;
const PhasesEnergyConsumption({ const PhasesEnergyConsumption({
required this.month, required this.uuid,
required this.phaseA, required this.createdAt,
required this.phaseB, required this.updatedAt,
required this.phaseC, required this.deviceUuid,
required this.date,
required this.energyConsumedKw,
required this.energyConsumedA,
required this.energyConsumedB,
required this.energyConsumedC,
}); });
@override @override
List<Object?> get props => [month, phaseA, phaseB, phaseC]; List<Object?> get props => [
uuid,
createdAt,
updatedAt,
deviceUuid,
date,
energyConsumedKw,
energyConsumedA,
energyConsumedB,
energyConsumedC,
];
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) { factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
return PhasesEnergyConsumption( return PhasesEnergyConsumption(
month: json['month'] as int, uuid: json['uuid'] as String,
phaseA: (json['phaseA'] as num).toDouble(), createdAt: DateTime.parse(json['createdAt'] as String),
phaseB: (json['phaseB'] as num).toDouble(), updatedAt: DateTime.parse(json['updatedAt'] as String),
phaseC: (json['phaseC'] as num).toDouble(), deviceUuid: json['deviceUuid'] as String,
date: DateTime.parse(json['date'] as String),
energyConsumedKw: double.parse(json['energyConsumedKw']),
energyConsumedA: double.parse(json['energyConsumedA']),
energyConsumedB: double.parse(json['energyConsumedB']),
energyConsumedC: double.parse(json['energyConsumedC']),
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'deviceUuid': deviceUuid,
'date': date.toIso8601String().split('T')[0],
'energyConsumedKw': energyConsumedKw.toString(),
'energyConsumedA': energyConsumedA.toString(),
'energyConsumedB': energyConsumedB.toString(),
'energyConsumedC': energyConsumedC.toString(),
};
}
} }
@@ -0,0 +1,69 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
part 'analytics_devices_event.dart';
part 'analytics_devices_state.dart';
class AnalyticsDevicesBloc
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
AnalyticsDevicesBloc(
this._analyticsDevicesService,
) : super(const AnalyticsDevicesState()) {
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
}
final AnalyticsDevicesService _analyticsDevicesService;
Future<void> _onLoadAnalyticsDevices(
LoadAnalyticsDevicesEvent event,
Emitter<AnalyticsDevicesState> emit,
) async {
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
try {
final devices = await _analyticsDevicesService.getDevices(event.param);
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.loaded,
devices: devices,
selectedDevice: devices.firstOrNull,
),
);
if (devices.isNotEmpty) {
event.onSuccess(devices.first);
}
} catch (e) {
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onSelectAnalyticsDevice(
SelectAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(
AnalyticsDevicesState(
selectedDevice: event.device,
devices: state.devices,
errorMessage: state.errorMessage,
status: state.status,
),
);
}
void _onClearAnalyticsDevice(
ClearAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(const AnalyticsDevicesState());
}
}
@@ -0,0 +1,31 @@
part of 'analytics_devices_bloc.dart';
sealed class AnalyticsDevicesEvent extends Equatable {
const AnalyticsDevicesEvent();
@override
List<Object> get props => [];
}
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
final GetAnalyticsDevicesParam param;
final void Function(AnalyticsDevice device) onSuccess;
@override
List<Object> get props => [param];
}
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const SelectAnalyticsDeviceEvent(this.device);
final AnalyticsDevice device;
@override
List<Object> get props => [device];
}
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const ClearAnalyticsDeviceEvent();
}
@@ -0,0 +1,20 @@
part of 'analytics_devices_bloc.dart';
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
final class AnalyticsDevicesState extends Equatable {
const AnalyticsDevicesState({
this.status = AnalyticsDevicesStatus.initial,
this.devices = const [],
this.errorMessage,
this.selectedDevice,
});
final AnalyticsDevicesStatus status;
final List<AnalyticsDevice> devices;
final AnalyticsDevice? selectedDevice;
final String? errorMessage;
@override
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
}
@@ -20,6 +20,13 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
spaces, spaces,
), ),
); );
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData( FetchEnergyManagementDataHelper.loadEnergyManagementData(
context, context,
communityId: community.uuid, communityId: community.uuid,
@@ -41,6 +48,13 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
), ),
); );
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData( FetchEnergyManagementDataHelper.loadEnergyManagementData(
context, context,
communityId: community.uuid, communityId: community.uuid,
@@ -54,7 +68,9 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community, CommunityModel community,
SpaceModel child, SpaceModel child,
) { ) {
// Do nothing if (child.children.isNotEmpty) {
return onSpaceSelected(context, community, child);
}
} }
@override @override
@@ -14,17 +14,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community, CommunityModel community,
List<SpaceModel> spaces, List<SpaceModel> spaces,
) { ) {
context.read<SpaceTreeBloc>().add( // Do Nothing
OnCommunitySelected(
community.uuid,
spaces.isNotEmpty ? [spaces.first] : [],
),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
} }
@override @override
@@ -34,18 +24,16 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
SpaceModel space, SpaceModel space,
) { ) {
final spaceTreeBloc = context.read<SpaceTreeBloc>(); final spaceTreeBloc = context.read<SpaceTreeBloc>();
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces; final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
final isSpaceSelected = selectedSpacesIds.contains(space.uuid);
if (isSpaceSelected) {
clearData(context);
return;
}
if (selectedSpacesIds.isEmpty) {
spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space]));
} else if (isSpaceSelected) {
spaceTreeBloc.add(const SpaceTreeClearSelectionEvent());
} else {
spaceTreeBloc spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent()) ..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', [])); ..add(OnSpaceSelected(community, space.uuid ?? '', []));
}
FetchOccupancyDataHelper.loadOccupancyData( FetchOccupancyDataHelper.loadOccupancyData(
context, context,
@@ -60,12 +48,12 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community, CommunityModel community,
SpaceModel child, SpaceModel child,
) { ) {
// Do nothing onSpaceSelected(context, community, child);
} }
@override @override
void clearData(BuildContext context) { void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent()); context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
// FetchOccupancyDataHelper.clearAllData(context); FetchOccupancyDataHelper.clearAllData(context);
} }
} }
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
@@ -11,10 +12,13 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/fake_occupacy_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/fake_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
@@ -23,9 +27,22 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AnalyticsPage extends StatelessWidget { class AnalyticsPage extends StatefulWidget {
const AnalyticsPage({super.key}); const AnalyticsPage({super.key});
@override
State<AnalyticsPage> createState() => _AnalyticsPageState();
}
class _AnalyticsPageState extends State<AnalyticsPage> {
late final HTTPService _httpService;
@override
void initState() {
super.initState();
_httpService = HTTPService();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
@@ -35,22 +52,22 @@ class AnalyticsPage extends StatelessWidget {
), ),
BlocProvider( BlocProvider(
create: (context) => TotalEnergyConsumptionBloc( create: (context) => TotalEnergyConsumptionBloc(
RemoteTotalEnergyConsumptionService(HTTPService()), RemoteTotalEnergyConsumptionService(_httpService),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc( create: (context) => EnergyConsumptionByPhasesBloc(
FakeEnergyConsumptionByPhasesService(), RemoteEnergyConsumptionByPhasesService(_httpService),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc( create: (context) => EnergyConsumptionPerDeviceBloc(
FakeEnergyConsumptionPerDeviceService(), RemoteEnergyConsumptionPerDeviceService(_httpService),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => PowerClampInfoBloc( create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(HTTPService()), RemotePowerClampInfoService(_httpService),
), ),
), ),
BlocProvider<RealtimeDeviceChangesBloc>( BlocProvider<RealtimeDeviceChangesBloc>(
@@ -58,11 +75,25 @@ class AnalyticsPage extends StatelessWidget {
FirebaseRealtimeDeviceService(), FirebaseRealtimeDeviceService(),
), ),
), ),
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
BlocProvider( BlocProvider(
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()), create: (context) => OccupancyBloc(
RemoteOccupancyService(_httpService),
),
),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(
RemoteOccupancyHeatMapService(_httpService),
),
), ),
BlocProvider(create: (context) => AnalyticsDatePickerBloc()), BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
BlocProvider(
create: (context) => AnalyticsDevicesBloc(
AnalyticsDevicesServiceDelegate(
RemoteOccupancyAnalyticsDevicesService(_httpService),
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
),
),
),
], ],
child: const AnalyticsPageForm(), child: const AnalyticsPageForm(),
); );
@@ -14,7 +14,6 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>( return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
buildWhen: (previous, current) => previous != current, buildWhen: (previous, current) => previous != current,
builder: (context, selectedTab) => Column( builder: (context, selectedTab) => Column(
@@ -68,8 +67,14 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add( context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value), UpdateAnalyticsDatePickerEvent(montlyDate: value),
); );
FetchEnergyManagementDataHelper.loadEnergyManagementData(
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context, context,
shouldFetchAnalyticsDevices: false,
selectedDate: value, selectedDate: value,
communityId: communityId:
spaceTreeState.selectedCommunities.firstOrNull ?? spaceTreeState.selectedCommunities.firstOrNull ??
@@ -77,6 +82,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
spaceId: spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '', spaceTreeState.selectedSpaces.firstOrNull ?? '',
); );
}
}, },
selectedDate: context selectedDate: context
.watch<AnalyticsDatePickerBloc>() .watch<AnalyticsDatePickerBloc>()
@@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog( return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: ColorsManager.whiteColors,
child: Container( child: Container(
padding: const EdgeInsetsDirectional.all(20), padding: const EdgeInsetsDirectional.all(20),
width: 320, width: 320,
@@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
} }
Row _buildYearSelector() { Row _buildYearSelector() {
final currentYear = DateTime.now().year;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -134,17 +135,35 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
), ),
const Spacer(), const Spacer(),
IconButton( IconButton(
onPressed: () => setState(() => _currentYear = _currentYear - 1), onPressed: () {
setState(() {
_currentYear = _currentYear - 1;
});
},
icon: const Icon( icon: const Icon(
Icons.chevron_left, Icons.chevron_left,
color: ColorsManager.grey700, color: ColorsManager.grey700,
), ),
), ),
IconButton( IconButton(
onPressed: () => setState(() => _currentYear = _currentYear + 1), onPressed: _currentYear < currentYear
icon: const Icon( ? () {
setState(() {
_currentYear = _currentYear + 1;
// Clear selected month if it becomes invalid in the new year
if (_currentYear == currentYear &&
_selectedMonth != null &&
_selectedMonth! > DateTime.now().month - 1) {
_selectedMonth = null;
}
});
}
: null,
icon: Icon(
Icons.chevron_right, Icons.chevron_right,
color: ColorsManager.grey700, color: _currentYear < currentYear
? ColorsManager.grey700
: ColorsManager.grey700.withValues(alpha: 0.3),
), ),
), ),
], ],
@@ -152,11 +171,13 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
} }
Widget _buildMonthsGrid() { Widget _buildMonthsGrid() {
final currentDate = DateTime.now();
final isCurrentYear = _currentYear == currentDate.year;
return GridView.builder( return GridView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: 12, itemCount: 12,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
childAspectRatio: 2.5, childAspectRatio: 2.5,
@@ -165,13 +186,28 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isSelected = _selectedMonth == index; final isSelected = _selectedMonth == index;
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
return InkWell( return InkWell(
onTap: () => setState(() => _selectedMonth = index), onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7) ? ColorsManager.vividBlue.withValues(alpha: 0.7)
: isFutureMonth
? ColorsManager.grey700.withValues(alpha: 0.1)
: const Color(0xFFEDF2F7), : const Color(0xFFEDF2F7),
borderRadius: borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero, isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
@@ -182,11 +218,14 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
fontSize: 12, fontSize: 12,
color: isSelected color: isSelected
? ColorsManager.whiteColors ? ColorsManager.whiteColors
: isFutureMonth
? ColorsManager.blackColor.withValues(alpha: 0.3)
: ColorsManager.blackColor.withValues(alpha: 0.8), : ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
), ),
),
); );
}, },
); );
@@ -20,9 +20,9 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
late int _currentYear; late int _currentYear;
static final years = List.generate( static final years = List.generate(
DateTime.now().year - 2020 + 1, DateTime.now().year - (DateTime.now().year - 5) + 1,
(index) => (2020 + index), (index) => (2020 + index),
); ).where((year) => year <= DateTime.now().year).toList();
@override @override
void initState() { void initState() {
@@ -33,7 +33,7 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Dialog( return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: ColorsManager.whiteColors,
child: Container( child: Container(
padding: const EdgeInsetsDirectional.all(20), padding: const EdgeInsetsDirectional.all(20),
width: 320, width: 320,
@@ -109,7 +109,6 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
shrinkWrap: true, shrinkWrap: true,
itemCount: years.length, itemCount: years.length,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
childAspectRatio: 2.5, childAspectRatio: 2.5,
@@ -120,6 +119,17 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
final isSelected = _currentYear == years[index]; final isSelected = _currentYear == years[index];
return InkWell( return InkWell(
onTap: () => setState(() => _currentYear = years[index]), onTap: () => setState(() => _currentYear = years[index]),
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -140,6 +150,7 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
), ),
), ),
), ),
),
); );
}, },
); );
@@ -1,20 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
abstract final class EnergyConsumptionByPhasesChartHelper {
const EnergyConsumptionByPhasesChartHelper._();
static const fakeData = <PhasesEnergyConsumption>[
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
];
}
@@ -21,12 +21,13 @@ abstract final class EnergyManagementChartsHelper {
reservedSize: 32, reservedSize: 32,
showTitles: true, showTitles: true,
maxIncluded: true, maxIncluded: true,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding( getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0), padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text( child: Text(
(value + 1).toString(), value.toString(),
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor, color: ColorsManager.lightGreyColor,
fontSize: 12, fontSize: 12,
), ),
), ),
@@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper {
leftTitles: AxisTitles( leftTitles: AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
maxIncluded: true, maxIncluded: false,
minIncluded: true,
interval: leftTitlesInterval, interval: leftTitlesInterval,
reservedSize: 110, reservedSize: 110,
getTitlesWidget: (value, meta) => Padding( getTitlesWidget: (value, meta) => Padding(
@@ -70,7 +72,7 @@ abstract final class EnergyManagementChartsHelper {
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) { static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) { return touchedSpots.map((spot) {
return LineTooltipItem( return LineTooltipItem(
getToolTipLabel(spot.x + 1, spot.y), getToolTipLabel(spot.x, spot.y),
const TextStyle( const TextStyle(
color: ColorsManager.textPrimaryColor, color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -91,31 +93,38 @@ abstract final class EnergyManagementChartsHelper {
); );
} }
static FlBorderData borderData() {
return FlBorderData(
show: true,
border: const Border.symmetric(
horizontal: BorderSide(
color: ColorsManager.greyColor,
style: BorderStyle.solid,
width: 1,
),
),
);
}
static FlGridData gridData() { static FlGridData gridData() {
return const FlGridData( return FlGridData(
show: true, show: true,
drawVerticalLine: false, drawVerticalLine: false,
drawHorizontalLine: true, drawHorizontalLine: true,
horizontalInterval: 250,
getDrawingHorizontalLine: (value) {
return FlLine(
color: ColorsManager.greyColor,
strokeWidth: 1,
dashArray: value == 0 ? null : [5, 5],
);
},
);
}
static FlBorderData borderData() {
return FlBorderData(
border: const Border(
bottom: BorderSide(
color: ColorsManager.greyColor,
style: BorderStyle.solid,
),
),
show: true,
); );
} }
static LineTouchData lineTouchData() { static LineTouchData lineTouchData() {
return LineTouchData( return LineTouchData(
handleBuiltInTouches: true, handleBuiltInTouches: true,
touchSpotThreshold: 2, touchSpotThreshold: 16,
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(), touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
); );
} }
@@ -1,11 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_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/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_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/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
@@ -13,13 +16,17 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_
abstract final class FetchEnergyManagementDataHelper { abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._(); const FetchEnergyManagementDataHelper._();
static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'; // static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
}
static void loadEnergyManagementData( static void loadEnergyManagementData(
BuildContext context, { BuildContext context, {
required String communityId, required String communityId,
required String spaceId, required String spaceId,
DateTime? selectedDate, DateTime? selectedDate,
bool shouldFetchAnalyticsDevices = true,
}) { }) {
if (communityId.isEmpty && spaceId.isEmpty) { if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context); clearAllData(context);
@@ -28,28 +35,46 @@ abstract final class FetchEnergyManagementDataHelper {
final datePickerState = context.read<AnalyticsDatePickerBloc>().state; final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate; final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityId,
spaceUuid: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
loadTotalEnergyConsumption( loadTotalEnergyConsumption(
context, context,
selectedDate: selectedDate0, selectedDate: selectedDate0,
communityId: communityId, communityId: communityId,
spaceId: spaceId, spaceId: spaceId,
); );
final selectedDevice = getSelectedDevice(context);
loadEnergyConsumptionByPhases(context, selectedDate: selectedDate); if (selectedDevice case final AnalyticsDevice device) {
loadEnergyConsumptionByPhases(
loadEnergyConsumptionPerDevice(context); context,
loadRealtimeDeviceChanges(context); powerClampUuid: device.uuid,
loadPowerClampInfo(context); selectedDate: selectedDate0,
);
}
loadEnergyConsumptionPerDevice(
context,
communityId: communityId,
spaceId: spaceId,
selectedDate: selectedDate0,
);
} }
static void loadEnergyConsumptionByPhases( static void loadEnergyConsumptionByPhases(
BuildContext context, { BuildContext context, {
required String powerClampUuid,
DateTime? selectedDate, DateTime? selectedDate,
}) { }) {
final param = GetEnergyConsumptionByPhasesParam( final param = GetEnergyConsumptionByPhasesParam(
startDate: selectedDate, date: selectedDate,
spaceId: '', powerClampUuid: powerClampUuid,
); );
context.read<EnergyConsumptionByPhasesBloc>().add( context.read<EnergyConsumptionByPhasesBloc>().add(
LoadEnergyConsumptionByPhasesEvent(param: param), LoadEnergyConsumptionByPhasesEvent(param: param),
@@ -72,33 +97,80 @@ abstract final class FetchEnergyManagementDataHelper {
); );
} }
static void loadEnergyConsumptionPerDevice(BuildContext context) { static void loadEnergyConsumptionPerDevice(
const param = GetEnergyConsumptionPerDeviceParam(); BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetEnergyConsumptionPerDeviceParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<EnergyConsumptionPerDeviceBloc>().add( context.read<EnergyConsumptionPerDeviceBloc>().add(
const LoadEnergyConsumptionPerDeviceEvent(param), LoadEnergyConsumptionPerDeviceEvent(param),
); );
} }
static void loadPowerClampInfo(BuildContext context) { static void loadPowerClampInfo(BuildContext context) {
final selectedDevice = getSelectedDevice(context);
if (selectedDevice case final AnalyticsDevice device) {
context.read<PowerClampInfoBloc>().add( context.read<PowerClampInfoBloc>().add(
const LoadPowerClampInfoEvent(_powerClampId), LoadPowerClampInfoEvent(device.uuid),
);
}
}
static void loadRealtimeDeviceChanges(
BuildContext context, {
String? deviceUuid,
}) {
final selectedDevice = getSelectedDevice(context);
context.read<RealtimeDeviceChangesBloc>().add(
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
); );
} }
static void loadRealtimeDeviceChanges(BuildContext context) { static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
required DateTime selectedDate,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
onSuccess: (device) {
context.read<PowerClampInfoBloc>().add(
LoadPowerClampInfoEvent(device.uuid),
);
loadEnergyConsumptionByPhases(
context,
powerClampUuid: device.uuid,
selectedDate: selectedDate,
);
context.read<RealtimeDeviceChangesBloc>().add( context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted(_powerClampId), RealtimeDeviceChangesStarted(device.uuid),
);
},
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['PC'],
requestType: AnalyticsDeviceRequestType.energyManagement,
),
),
); );
} }
static void clearAllData(BuildContext context) { static void clearAllData(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<PowerClampInfoBloc>().add( context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(), const ClearPowerClampInfoEvent(),
); );
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<EnergyConsumptionPerDeviceBloc>().add( context.read<EnergyConsumptionPerDeviceBloc>().add(
const ClearEnergyConsumptionPerDeviceEvent(), const ClearEnergyConsumptionPerDeviceEvent(),
@@ -111,5 +183,6 @@ abstract final class FetchEnergyManagementDataHelper {
context.read<EnergyConsumptionByPhasesBloc>().add( context.read<EnergyConsumptionByPhasesBloc>().add(
const ClearEnergyConsumptionByPhasesEvent(), const ClearEnergyConsumptionByPhasesEvent(),
); );
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
} }
} }
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsDeviceDropdown extends StatelessWidget {
const AnalyticsDeviceDropdown({required this.onChanged, super.key});
final ValueChanged<AnalyticsDevice> onChanged;
@override
Widget build(BuildContext context) {
return BlocBuilder<AnalyticsDevicesBloc, AnalyticsDevicesState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: Visibility(
visible: state.devices.isNotEmpty,
replacement: _buildNoDevicesFound(context),
child: _buildDevicesDropdown(context, state),
),
);
},
);
}
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
);
Widget _buildNoDevicesFound(BuildContext context) {
return Padding(
padding: _defaultPadding,
child: Text(
'no devices found',
style: _getTextStyle(context),
),
);
}
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
final spaceUuid = state.selectedDevice?.spaceUuid;
return DropdownButton<AnalyticsDevice?>(
value: state.selectedDevice,
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: _getTextStyle(context),
padding: _defaultPadding,
selectedItemBuilder: (context) {
return state.devices.map((e) => Text(e.name)).toList();
},
items: state.devices.map((e) {
return DropdownMenuItem(
value: e,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(e.name),
if (spaceUuid != null)
FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: Text(
spaceUuid,
style: _getTextStyle(context)?.copyWith(
fontSize: 10,
),
),
),
],
),
);
}).toList(),
onChanged: (value) {
if (value case final AnalyticsDevice device) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(device),
);
onChanged.call(device);
}
},
);
}
TextStyle? _getTextStyle(BuildContext context) {
return context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
);
}
}
@@ -1,6 +1,6 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart'; import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
@@ -18,7 +18,10 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BarChart( return BarChart(
BarChartData( BarChartData(
gridData: EnergyManagementChartsHelper.gridData(), gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context), barTouchData: _barTouchData(context),
titlesData: _titlesData(context), titlesData: _titlesData(context),
@@ -31,25 +34,29 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
barRods: [ barRods: [
BarChartRodData( BarChartRodData(
color: ColorsManager.vividBlue.withValues(alpha: 0.1), color: ColorsManager.vividBlue.withValues(alpha: 0.1),
toY: data.phaseA + data.phaseB + data.phaseC, toY: data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
rodStackItems: [ rodStackItems: [
BarChartRodStackItem( BarChartRodStackItem(
0, 0,
data.phaseA, data.energyConsumedA,
ColorsManager.vividBlue.withValues(alpha: 0.8), ColorsManager.vividBlue.withValues(alpha: 0.8),
), ),
BarChartRodStackItem( BarChartRodStackItem(
data.phaseA, data.energyConsumedA,
data.phaseA + data.phaseB, data.energyConsumedA + data.energyConsumedB,
ColorsManager.vividBlue.withValues(alpha: 0.4), ColorsManager.vividBlue.withValues(alpha: 0.4),
), ),
BarChartRodStackItem( BarChartRodStackItem(
data.phaseA + data.phaseB, data.energyConsumedA + data.energyConsumedB,
data.phaseA + data.phaseB + data.phaseC, data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
ColorsManager.vividBlue.withValues(alpha: 0.15), ColorsManager.vividBlue.withValues(alpha: 0.15),
), ),
], ],
width: 16, width: 8,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
topRight: Radius.circular(8), topRight: Radius.circular(8),
@@ -59,6 +66,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
); );
}).toList(), }).toList(),
), ),
duration: Duration.zero,
); );
} }
@@ -91,18 +99,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
}) { }) {
final data = energyData; final data = energyData;
final month = data[group.x.toInt()].month.getMonthName; final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
final phaseA = data[group.x.toInt()].phaseA; final phaseA = data[group.x.toInt()].energyConsumedA;
final phaseB = data[group.x.toInt()].phaseB; final phaseB = data[group.x.toInt()].energyConsumedB;
final phaseC = data[group.x.toInt()].phaseC; final phaseC = data[group.x.toInt()].energyConsumedC;
final total = data[group.x.toInt()].energyConsumedKw;
return BarTooltipItem( return BarTooltipItem(
'$month\n', '$date\n',
context.textTheme.bodyMedium!.copyWith( context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontSize: 14, fontSize: 14,
), ),
textAlign: TextAlign.start,
children: [ children: [
TextSpan(
text: 'Total: $total\n',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
TextSpan( TextSpan(
text: 'Phase A: $phaseA\n', text: 'Phase A: $phaseA\n',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
@@ -144,9 +161,9 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
getTitlesWidget: (value, _) { getTitlesWidget: (value, _) {
final month = energyData[value.toInt()].month.getMonthName; final month = DateFormat('d').format(energyData[value.toInt()].date);
return FittedBox( return FittedBox(
alignment: AlignmentDirectional.bottomCenter, alignment: AlignmentDirectional.center,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: RotatedBox( child: RotatedBox(
quarterTurns: 3, quarterTurns: 3,
@@ -160,7 +177,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
), ),
); );
}, },
reservedSize: 36, reservedSize: 18,
), ),
); );
@@ -16,7 +16,11 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
context, context,
leftTitlesInterval: 250, leftTitlesInterval: 250,
), ),
gridData: EnergyManagementChartsHelper.gridData(),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: chartData.map((e) { lineBarsData: chartData.map((e) {
@@ -33,7 +37,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
); );
}).toList(), }).toList(),
), ),
duration: Durations.extralong1, duration: Duration.zero,
curve: Curves.easeIn, curve: Curves.easeIn,
); );
} }
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
@@ -46,6 +47,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
flex: 2, flex: 2,
child: EnergyConsumptionPerDeviceDevicesList( child: EnergyConsumptionPerDeviceDevicesList(
chartData: state.chartData, chartData: state.chartData,
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
), ),
), ),
], ],
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key}); const EnergyConsumptionPerDeviceDevicesList({
required this.chartData,
required this.devices,
super.key,
});
final List<AnalyticsDevice> devices;
final List<DeviceEnergyDataModel> chartData; final List<DeviceEnergyDataModel> chartData;
@override @override
@@ -16,13 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: chartData.map((e) => _buildDeviceCell(context, e)).toList(), children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
), ),
); );
} }
Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) { Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
return Container( final deviceColor = chartData
.firstWhere(
(element) => element.deviceId == device.uuid,
orElse: () => const DeviceEnergyDataModel(
energy: [],
deviceName: '',
deviceId: '',
color: Colors.red,
),
)
.color;
return Tooltip(
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
child: Container(
height: MediaQuery.sizeOf(context).height * 0.0365, height: MediaQuery.sizeOf(context).height * 0.0365,
padding: const EdgeInsetsDirectional.symmetric( padding: const EdgeInsetsDirectional.symmetric(
vertical: 8, vertical: 8,
@@ -43,10 +63,10 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
children: [ children: [
CircleAvatar( CircleAvatar(
radius: 4, radius: 4,
backgroundColor: device.color, backgroundColor: deviceColor,
), ),
Text( Text(
device.deviceName, device.name,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
@@ -57,6 +77,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }
@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
const PowerClampEnergyDataDeviceDropdown({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: DropdownButton<String>(
value: 'Device 1',
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
),
items: [
for (var i = 1; i < 10; i++)
DropdownMenuItem(
value: 'Device $i',
child: Text(
'Device $i',
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
],
onChanged: (value) {},
),
);
}
}
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart'; import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_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/power_clamp_info/power_clamp_info_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_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/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_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/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
@@ -50,7 +53,8 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
SelectableText( SelectableText(
state.powerClampModel?.productUuid ?? 'N/A', context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -107,7 +111,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Expanded(
flex: 2, flex: 3,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
@@ -122,11 +126,25 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
), ),
), ),
const Spacer(), const Spacer(),
const Expanded( Expanded(
flex: 2,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(), child: AnalyticsDeviceDropdown(
onChanged: (value) {
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
context,
powerClampUuid: value.uuid,
selectedDate:
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
), ),
), ),
], ],
@@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
fontSize: 16, fontSize: 16,
), ),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
), ),
trailing: Text.rich( trailing: Text.rich(
TextSpan( TextSpan(
@@ -4,15 +4,6 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
// energy_consumption_chart will return id, name and consumption
const phasesJson = {
"1": {
"phaseOne": 1000,
"phaseTwo": 2000,
"phaseThree": 3000,
}
};
class TotalEnergyConsumptionChart extends StatelessWidget { class TotalEnergyConsumptionChart extends StatelessWidget {
const TotalEnergyConsumptionChart({required this.chartData, super.key}); const TotalEnergyConsumptionChart({required this.chartData, super.key});
@@ -23,13 +14,19 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
return Expanded( return Expanded(
child: LineChart( child: LineChart(
LineChartData( LineChartData(
titlesData: EnergyManagementChartsHelper.titlesData(context), titlesData: EnergyManagementChartsHelper.titlesData(
gridData: EnergyManagementChartsHelper.gridData(), context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: _lineBarsData, lineBarsData: _lineBarsData,
), ),
duration: Durations.extralong1, duration: Duration.zero,
curve: Curves.easeIn, curve: Curves.easeIn,
), ),
); );
@@ -46,7 +43,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
.entries .entries
.map( .map(
(entry) => FlSpot( (entry) => FlSpot(
entry.key.toDouble(), entry.value.date.day.toDouble(),
entry.value.value, entry.value.value,
), ),
) )
@@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_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/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
@@ -17,35 +20,76 @@ abstract final class FetchOccupancyDataHelper {
}) { }) {
if (communityId.isEmpty && spaceId.isEmpty) { if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context); clearAllData(context);
return;
} }
final datePickerState = context.read<AnalyticsDatePickerBloc>().state; final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
context.read<OccupancyBloc>().add( loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId);
LoadOccupancyEvent( final selectedDevice = context.read<AnalyticsDevicesBloc>().state.selectedDevice;
GetOccupancyParam(
monthDate: loadOccupancyChartData(
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}', context,
spaceUuid: spaceId, spaceUuid: spaceId,
communityUuid: communityId, date: datePickerState.monthlyDate,
),
),
);
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(
spaceId: spaceId,
communityId: communityId,
year: datePickerState.yearlyDate,
),
),
); );
loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate);
if (selectedDevice case final AnalyticsDevice device) {
context.read<RealtimeDeviceChangesBloc>() context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed()) ..add(const RealtimeDeviceChangesClosed())
..add( ..add(
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'), RealtimeDeviceChangesStarted(device.uuid),
);
}
}
static void loadHeatMapData(
BuildContext context, {
required String spaceUuid,
required DateTime year,
}) {
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year),
),
);
}
static void loadOccupancyChartData(
BuildContext context, {
required String spaceUuid,
required DateTime date,
}) {
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}',
spaceUuid: spaceUuid,
),
),
);
}
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['WPS', 'CPS'],
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
},
),
); );
} }
@@ -59,5 +103,9 @@ abstract final class FetchOccupancyDataHelper {
context.read<RealtimeDeviceChangesBloc>().add( context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(), const RealtimeDeviceChangesClosed(),
); );
context.read<AnalyticsDevicesBloc>().add(
const ClearAnalyticsDeviceEvent(),
);
} }
} }
@@ -20,9 +20,9 @@ class AnalyticsOccupancyView extends StatelessWidget {
child: Column( child: Column(
spacing: 32, spacing: 32,
children: [ children: [
SizedBox(height: height * 0.45, child: const OccupancyEndSideBar()), SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()), SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()), SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
], ],
), ),
); );
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class HeatMapTooltip extends StatelessWidget {
const HeatMapTooltip({
required this.date,
required this.value,
super.key,
});
final DateTime date;
final int value;
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.topStart,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.grey700,
borderRadius: BorderRadius.circular(3),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
DateFormat('MMM d, yyyy').format(date),
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
fontWeight: FontWeight.w700,
color: ColorsManager.whiteColors,
),
),
const Divider(height: 2, thickness: 1),
Text(
'$value Occupants',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
color: ColorsManager.whiteColors,
),
),
],
),
),
);
}
}
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
class InteractiveHeatMap extends StatefulWidget {
const InteractiveHeatMap({
required this.items,
required this.maxValue,
required this.cellSize,
super.key,
});
final List<OccupancyPaintItem> items;
final int maxValue;
final double cellSize;
@override
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
}
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
OccupancyPaintItem? _hoveredItem;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void dispose() {
_removeOverlay();
_overlayEntry?.dispose();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
_removeOverlay();
final column = item.index ~/ 7;
final row = item.index % 7;
final x = column * widget.cellSize;
final y = row * widget.cellSize;
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
child: CompositedTransformFollower(
link: _layerLink,
offset: Offset(x + widget.cellSize, y),
child: Material(
color: Colors.transparent,
child: Transform.translate(
offset: Offset(-(widget.cellSize * 2.5), -50),
child: HeatMapTooltip(date: item.date, value: item.value),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: MouseRegion(
onHover: (event) {
final column = event.localPosition.dx ~/ widget.cellSize;
final row = event.localPosition.dy ~/ widget.cellSize;
final index = column * 7 + row;
if (index >= 0 && index < widget.items.length) {
final item = widget.items[index];
if (_hoveredItem != item) {
setState(() => _hoveredItem = item);
_showTooltip(item, event.localPosition);
}
} else {
_removeOverlay();
setState(() => _hoveredItem = null);
}
},
onExit: (_) {
_removeOverlay();
setState(() => _hoveredItem = null);
},
child: CustomPaint(
isComplex: true,
size: _painterSize,
painter: OccupancyPainter(
items: widget.items,
maxValue: widget.maxValue,
hoveredItem: _hoveredItem,
),
),
),
);
}
Size get _painterSize {
final height = 7 * widget.cellSize;
final width = widget.items.length ~/ 7 * widget.cellSize;
return Size(width, height);
}
}
@@ -16,30 +16,38 @@ class OccupancyChart extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BarChart( return BarChart(
BarChartData( BarChartData(
maxY: 1.0, maxY: 100.0,
gridData: EnergyManagementChartsHelper.gridData().copyWith( gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true, checkToShowHorizontalLine: (value) => true,
horizontalInterval: 0.25, horizontalInterval: 20,
), ),
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context), barTouchData: _barTouchData(context),
titlesData: _titlesData(context), titlesData: _titlesData(context).copyWith(
leftTitles: _titlesData(context).leftTitles.copyWith(
sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith(
maxIncluded: true,
minIncluded: true,
),
),
),
barGroups: List.generate(chartData.length, (index) { barGroups: List.generate(chartData.length, (index) {
final actual = chartData[index]; final actual = chartData[index];
final occupancyValue = double.parse(actual.occupancy);
return BarChartGroupData( return BarChartGroupData(
x: index, x: index,
barsSpace: 0, barsSpace: 0,
groupVertically: true, groupVertically: true,
barRods: [ barRods: [
BarChartRodData( BarChartRodData(
toY: 1.0, toY: 100.0,
fromY: double.parse(actual.occupancy) + 0.025, fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5,
color: ColorsManager.graysColor, color: ColorsManager.graysColor,
width: _chartWidth, width: _chartWidth,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
BarChartRodData( BarChartRodData(
toY: double.parse(actual.occupancy), toY: occupancyValue,
color: ColorsManager.vividBlue.withValues(alpha: 0.8), color: ColorsManager.vividBlue.withValues(alpha: 0.8),
width: _chartWidth, width: _chartWidth,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@@ -81,7 +89,7 @@ class OccupancyChart extends StatelessWidget {
final data = chartData; final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy); final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue * 100).toStringAsFixed(0)}%'; final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
return BarTooltipItem( return BarTooltipItem(
percentage, percentage,
@@ -101,14 +109,14 @@ class OccupancyChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith( final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70, reservedSize: 70,
interval: 0.25, interval: 20,
getTitlesWidget: (value, meta) => Padding( getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12), padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox( child: FittedBox(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
'${(value * 100).toStringAsFixed(0)}%', '${(value).toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
fontSize: 12, fontSize: 12,
color: ColorsManager.greyColor, color: ColorsManager.greyColor,
@@ -47,11 +47,14 @@ class OccupancyChartBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add( context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value), UpdateAnalyticsDatePickerEvent(montlyDate: value),
); );
FetchOccupancyDataHelper.loadOccupancyData( if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadOccupancyChartData(
context, context,
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '', spaceUuid:
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '', spaceTreeState.selectedSpaces.firstOrNull ?? '',
date: value,
); );
}
}, },
selectedDate: context selectedDate: context
.watch<AnalyticsDatePickerBloc>() .watch<AnalyticsDatePickerBloc>()
@@ -1,15 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart'; import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.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/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
import 'package:uuid/uuid.dart';
class OccupancyEndSideBar extends StatelessWidget { class OccupancyEndSideBar extends StatelessWidget {
const OccupancyEndSideBar({super.key}); const OccupancyEndSideBar({super.key});
@@ -37,7 +38,8 @@ class OccupancyEndSideBar extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
SelectableText( SelectableText(
(const Uuid().v4()), context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -105,7 +107,7 @@ class OccupancyEndSideBar extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( Expanded(
flex: 2, flex: 3,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
@@ -120,11 +122,18 @@ class OccupancyEndSideBar extends StatelessWidget {
), ),
), ),
const Spacer(), const Spacer(),
const Expanded( Expanded(
flex: 2,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(), child: AnalyticsDeviceDropdown(
onChanged: (value) =>
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
),
),
), ),
), ),
], ],
@@ -1,6 +1,7 @@
import 'dart:math' as math show max; import 'dart:math' as math show max;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
@@ -15,7 +16,7 @@ class OccupancyHeatMap extends StatelessWidget {
static const _totalWeeks = 53; static const _totalWeeks = 53;
int get _maxValue => heatMapData.isNotEmpty int get _maxValue => heatMapData.isNotEmpty
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max) ? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
: 0; : 0;
DateTime _getStartingDate() { DateTime _getStartingDate() {
@@ -28,7 +29,7 @@ class OccupancyHeatMap extends StatelessWidget {
return List.generate(_totalWeeks * 7, (index) { return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index)); final date = startDate.add(Duration(days: index));
final value = heatMapData[date] ?? 0; final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value); return OccupancyPaintItem(index: index, value: value, date: date);
}); });
} }
@@ -58,15 +59,13 @@ class OccupancyHeatMap extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
const OccupancyHeatMapDays(cellSize: _cellSize), const OccupancyHeatMapDays(cellSize: _cellSize),
CustomPaint( SizedBox(
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize), width: _totalWeeks * _cellSize,
child: CustomPaint( height: 7 * _cellSize,
isComplex: true, child: InteractiveHeatMap(
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
painter: OccupancyPainter(
items: paintItems, items: paintItems,
maxValue: _maxValue, maxValue: _maxValue,
), cellSize: _cellSize,
), ),
), ),
], ],
@@ -47,12 +47,14 @@ class OccupancyHeatMapBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add( context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(yearlyDate: value), UpdateAnalyticsDatePickerEvent(yearlyDate: value),
); );
FetchOccupancyDataHelper.loadOccupancyData( if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadHeatMapData(
context, context,
communityId: spaceUuid:
spaceTreeState.selectedCommunities.firstOrNull ?? '', spaceTreeState.selectedSpaces.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '', year: value,
); );
}
}, },
datePickerType: DatePickerType.year, datePickerType: DatePickerType.year,
selectedDate: context selectedDate: context
@@ -68,7 +70,10 @@ class OccupancyHeatMapBox extends StatelessWidget {
Expanded( Expanded(
child: OccupancyHeatMap( child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map( heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(value.date, value.occupancy), (_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
), ),
), ),
), ),
@@ -4,18 +4,25 @@ import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyPaintItem { class OccupancyPaintItem {
final int index; final int index;
final int value; final int value;
final DateTime date;
const OccupancyPaintItem({required this.index, required this.value}); const OccupancyPaintItem({
required this.index,
required this.value,
required this.date,
});
} }
class OccupancyPainter extends CustomPainter { class OccupancyPainter extends CustomPainter {
OccupancyPainter({ OccupancyPainter({
required this.items, required this.items,
required this.maxValue, required this.maxValue,
this.hoveredItem,
}); });
final List<OccupancyPaintItem> items; final List<OccupancyPaintItem> items;
final int maxValue; final int maxValue;
final OccupancyPaintItem? hoveredItem;
static const double cellSize = 16.0; static const double cellSize = 16.0;
@@ -25,6 +32,10 @@ class OccupancyPainter extends CustomPainter {
final Paint borderPaint = Paint() final Paint borderPaint = Paint()
..color = ColorsManager.grayBorder.withValues(alpha: 0.4) ..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
final Paint hoveredBorderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
for (final item in items) { for (final item in items) {
final column = item.index ~/ 7; final column = item.index ~/ 7;
@@ -37,6 +48,10 @@ class OccupancyPainter extends CustomPainter {
final rect = Rect.fromLTWH(x, y, cellSize, cellSize); final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
canvas.drawRect(rect, fillPaint); canvas.drawRect(rect, fillPaint);
// Highlight the hovered item
if (hoveredItem != null && hoveredItem!.index == item.index) {
canvas.drawRect(rect, hoveredBorderPaint);
} else {
_drawDashedLine( _drawDashedLine(
canvas, canvas,
Offset(x, y), Offset(x, y),
@@ -51,8 +66,9 @@ class OccupancyPainter extends CustomPainter {
); );
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint); canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine( canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize),
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint); borderPaint);
}
} }
} }
@@ -80,5 +96,6 @@ class OccupancyPainter extends CustomPainter {
} }
@override @override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
oldDelegate.hoveredItem != hoveredItem;
} }
@@ -0,0 +1,22 @@
enum AnalyticsDeviceRequestType { energyManagement, occupancy }
class GetAnalyticsDevicesParam {
final String? spaceUuid;
final List<String> deviceTypes;
final String? communityUuid;
final AnalyticsDeviceRequestType requestType;
const GetAnalyticsDevicesParam({
required this.requestType,
required this.spaceUuid,
required this.deviceTypes,
required this.communityUuid,
});
Map<String, dynamic> toJson() {
return <String, dynamic>{
if (spaceUuid != null) 'spaceUuid': spaceUuid,
if (communityUuid != null) 'communityUuid': communityUuid,
};
}
}
@@ -1,24 +1,20 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class GetEnergyConsumptionByPhasesParam extends Equatable { class GetEnergyConsumptionByPhasesParam extends Equatable {
final DateTime? startDate; final String powerClampUuid;
final DateTime? endDate; final DateTime? date;
final String? spaceId;
const GetEnergyConsumptionByPhasesParam({ const GetEnergyConsumptionByPhasesParam({
this.startDate, required this.powerClampUuid,
this.endDate, this.date,
this.spaceId,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'startDate': startDate?.toIso8601String(), 'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}',
'endDate': endDate?.toIso8601String(),
'spaceId': spaceId,
}; };
} }
@override @override
List<Object?> get props => [startDate, endDate, spaceId]; List<Object?> get props => [powerClampUuid, date];
} }
@@ -1,3 +1,19 @@
class GetEnergyConsumptionPerDeviceParam { class GetEnergyConsumptionPerDeviceParam {
const GetEnergyConsumptionPerDeviceParam(); const GetEnergyConsumptionPerDeviceParam({
this.monthDate,
this.spaceId,
this.communityId,
});
final DateTime? monthDate;
final String? spaceId;
final String? communityId;
Map<String, dynamic> toJson() => {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': true,
};
} }
@@ -1,19 +1,13 @@
class GetOccupancyHeatMapParam { class GetOccupancyHeatMapParam {
final DateTime year; final DateTime year;
final String communityId; final String spaceUuid;
final String spaceId;
const GetOccupancyHeatMapParam({ const GetOccupancyHeatMapParam({
required this.year, required this.year,
required this.communityId, required this.spaceUuid,
required this.spaceId,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {'year': year.year};
'year': year.toIso8601String(),
'communityId': communityId,
'spaceId': spaceId,
};
} }
} }
@@ -1,19 +1,11 @@
class GetOccupancyParam { class GetOccupancyParam {
final String monthDate; final String monthDate;
final String? spaceUuid; final String? spaceUuid;
final String communityUuid;
GetOccupancyParam({ GetOccupancyParam({
required this.monthDate, required this.monthDate,
required this.spaceUuid, required this.spaceUuid,
required this.communityUuid,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => {'monthDate': monthDate};
return {
'monthDate': monthDate,
'spaceUuid': spaceUuid,
'communityUuid': communityUuid,
};
}
} }
@@ -13,7 +13,7 @@ class GetTotalEnergyConsumptionParam {
return { return {
'monthDate': 'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (communityId == null || communityId!.isEmpty) 'spaceUuid': spaceId, if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId, 'communityUuid': communityId,
'groupByDevice': false, 'groupByDevice': false,
}; };
@@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
abstract interface class AnalyticsDevicesService {
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param);
}
@@ -0,0 +1,24 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService {
const AnalyticsDevicesServiceDelegate(
this._occupancyService,
this._energyManagementService,
);
final AnalyticsDevicesService _occupancyService;
final AnalyticsDevicesService _energyManagementService;
@override
Future<List<AnalyticsDevice>> getDevices(
GetAnalyticsDevicesParam param,
) {
return switch (param.requestType) {
AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param),
AnalyticsDeviceRequestType.energyManagement =>
_energyManagementService.getDevices(param),
};
}
}
@@ -0,0 +1,36 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyManagementAnalyticsDevicesService
implements AnalyticsDevicesService {
const RemoteEnergyManagementAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final response = await _httpService.get(
path: '/devices-space-community/recursive-child',
queryParameters: param.toJson()
..addAll({'productType': param.deviceTypes.first}),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}
@@ -0,0 +1,61 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
const RemoteOccupancyAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final requests = await Future.wait<List<AnalyticsDevice>>(
param.deviceTypes.map((e) {
final mappedParam = GetAnalyticsDevicesParam(
requestType: AnalyticsDeviceRequestType.occupancy,
spaceUuid: param.spaceUuid,
deviceTypes: [e],
communityUuid: param.communityUuid,
);
return _makeRequest(mappedParam);
}).toList(),
);
final result = requests.map((e) => e.first).toList();
return result;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
Future<List<AnalyticsDevice>> _makeRequest(GetAnalyticsDevicesParam param) async {
try {
final projectUuid = await ProjectManager.getProjectUUID();
final response = await _httpService.get(
path:
'/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}/devices',
queryParameters: {
'communityUuid': param.communityUuid,
'spaceUuid': param.spaceUuid,
'productType': param.deviceTypes.first,
},
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
rethrow;
}
}
}
@@ -1,29 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
class FakeEnergyConsumptionByPhasesService
implements EnergyConsumptionByPhasesService {
@override
Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param,
) {
return Future.delayed(
const Duration(milliseconds: 500),
() => const [
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 100, phaseC: 100),
],
);
}
}
@@ -15,8 +15,9 @@ final class RemoteEnergyConsumptionByPhasesService
) async { ) async {
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: 'endpoint', path: '/power-clamp/${param.powerClampUuid}/historical',
showServerMessage: true, showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (data) { expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {}; final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? []; final mappedData = json['data'] as List<dynamic>? ?? [];
@@ -28,7 +29,7 @@ final class RemoteEnergyConsumptionByPhasesService
); );
return response; return response;
} catch (e) { } catch (e) {
throw Exception('Failed to load energy consumption per device: $e'); throw Exception('Failed to load energy consumption per phase: $e');
} }
} }
} }
@@ -1,39 +0,0 @@
import 'dart:math' as math show Random;
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
class FakeEnergyConsumptionPerDeviceService
implements EnergyConsumptionPerDeviceService {
@override
Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param,
) {
final random = math.Random();
return Future.delayed(const Duration(milliseconds: 500), () {
return [
(Colors.redAccent, 1),
(Colors.lightBlueAccent, 2),
(Colors.purpleAccent, 3),
].map((e) {
final (color, index) = e;
return DeviceEnergyDataModel(
color: color,
energy: List.generate(30, (i) => i)
.map(
(index) => EnergyDataModel(
date: DateTime(2025, 1, index + 1),
value: random.nextInt(100) + (index * 100),
),
)
.toList(),
deviceName: 'Device $index',
deviceId: 'device_$index',
);
}).toList();
});
}
}
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
@@ -15,16 +17,10 @@ class RemoteEnergyConsumptionPerDeviceService
) async { ) async {
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: 'endpoint', path: '/power-clamp/historical',
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (data) { queryParameters: param.toJson(),
final json = data as Map<String, dynamic>? ?? {}; expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return DeviceEnergyDataModel.fromJson(jsonData);
}).toList();
},
); );
return response; return response;
} catch (e) { } catch (e) {
@@ -32,3 +28,30 @@ class RemoteEnergyConsumptionPerDeviceService
} }
} }
} }
abstract final class _EnergyConsumptionPerDeviceMapper {
const _EnergyConsumptionPerDeviceMapper._();
static List<DeviceEnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final deviceData = e as Map<String, dynamic>;
final energyData = deviceData['data'] as List<dynamic>;
return DeviceEnergyDataModel(
deviceId: deviceData['deviceUuid'] as String,
deviceName: deviceData['deviceName'] as String,
color: Color((DateTime.now().microsecondsSinceEpoch +
deviceData['deviceUuid'].hashCode) |
0xFF000000),
energy: energyData.map((data) {
final energyJson = data as Map<String, dynamic>;
return EnergyDataModel(
date: DateTime.parse(energyJson['date'] as String),
value: double.parse(energyJson['total_energy_consumed_kw'] as String),
);
}).toList(),
);
}).toList();
}
}
@@ -1,19 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
class FakeOccupacyService implements OccupacyService {
@override
Future<List<Occupacy>> load(GetOccupancyParam param) async {
return await Future.delayed(
const Duration(seconds: 1),
() => List.generate(
30,
(index) => Occupacy(
date: DateTime.now().subtract(Duration(days: index)).toString(),
occupancy: ((index / 100)).toString(),
),
),
);
}
}
@@ -0,0 +1,32 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyService implements OccupacyService {
const RemoteOccupancyService(this._httpService);
final HTTPService _httpService;
@override
Future<List<Occupacy>> load(GetOccupancyParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/duration/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return Occupacy.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}
@@ -1,25 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
class FakeOccupancyHeatMapService implements OccupancyHeatMapService {
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) {
return Future.delayed(const Duration(milliseconds: 200), () {
final now = DateTime.now();
final startOfYear = DateTime(now.year, 1, 1);
final endOfYear = DateTime(now.year, 12, 31);
final daysInYear = endOfYear.difference(startOfYear).inDays + 1;
final List<OccupancyHeatMapModel> data = List.generate(
daysInYear,
(index) => OccupancyHeatMapModel(
date: startOfYear.add(Duration(days: index)),
occupancy: ((index + 1) * 10) % 100,
),
);
return data;
});
}
}
@@ -0,0 +1,35 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
const RemoteOccupancyHeatMapService(this._httpService);
final HTTPService _httpService;
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/heat-map/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => OccupancyHeatMapModel.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption:');
}
}
}
@@ -12,7 +12,7 @@ class AnalyticsErrorWidget extends StatelessWidget {
return Visibility( return Visibility(
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false), visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
child: Text( child: Text(
'$errorMessage ?? "Something went wrong"', errorMessage ?? 'Something went wrong',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
+31 -12
View File
@@ -63,7 +63,8 @@ class _DynamicTableState extends State<DynamicTable> {
} }
} }
bool _compareListOfLists(List<List<dynamic>> oldList, List<List<dynamic>> newList) { bool _compareListOfLists(
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
// Check if the old and new lists are the same // Check if the old and new lists are the same
if (oldList.length != newList.length) return false; if (oldList.length != newList.length) return false;
@@ -111,8 +112,8 @@ class _DynamicTableState extends State<DynamicTable> {
trackVisibility: true, trackVisibility: true,
child: Scrollbar( child: Scrollbar(
controller: _horizontalScrollController, controller: _horizontalScrollController,
thumbVisibility: false, thumbVisibility: true,
trackVisibility: false, trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1, notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _verticalScrollController, controller: _verticalScrollController,
@@ -132,14 +133,18 @@ class _DynamicTableState extends State<DynamicTable> {
children: [ children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(), if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) { ...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(widget.headers[index], index); return _buildTableHeaderCell(
widget.headers[index], index);
}) })
//...widget.headers.map((header) => _buildTableHeaderCell(header)), //...widget.headers.map((header) => _buildTableHeaderCell(header)),
], ],
), ),
), ),
widget.isEmpty widget.isEmpty
? Column( ? SizedBox(
height: widget.size.height * 0.5,
width: widget.size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Row( Row(
@@ -153,25 +158,35 @@ class _DynamicTableState extends State<DynamicTable> {
height: 15, height: 15,
), ),
Text( Text(
widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', widget.tableName == 'AccessManagement'
? 'No Password '
: 'No Devices',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodySmall! .bodySmall!
.copyWith(color: ColorsManager.grayColor), .copyWith(
color:
ColorsManager.grayColor),
) )
], ],
), ),
], ],
), ),
], ],
),
) )
: Column( : Column(
children: List.generate(widget.data.length, (index) { children:
List.generate(widget.data.length, (index) {
final row = widget.data[index]; final row = widget.data[index];
return Row( return Row(
children: [ children: [
if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), if (widget.withCheckBox)
...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), _buildRowCheckbox(
index, widget.size.height * 0.08),
...row.map((cell) => _buildTableCell(
cell.toString(),
widget.size.height * 0.08)),
], ],
); );
}), }),
@@ -196,7 +211,9 @@ class _DynamicTableState extends State<DynamicTable> {
), ),
child: Checkbox( child: Checkbox(
value: _selectAll, value: _selectAll,
onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
), ),
); );
} }
@@ -238,7 +255,9 @@ class _DynamicTableState extends State<DynamicTable> {
constraints: const BoxConstraints.expand(height: 40), constraints: const BoxConstraints.expand(height: 40),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text( child: Text(
title, title,
style: context.textTheme.titleSmall!.copyWith( style: context.textTheme.titleSmall!.copyWith(
@@ -97,7 +97,8 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
children: [ children: [
_buildInfoRow('Space Name:', _buildInfoRow('Space Name:',
device.spaces?.firstOrNull?.spaceName ?? 'N/A'), device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'), _buildInfoRow(
'Sub space:', device.subspace?.subspaceName ?? 'N/A'),
], ],
), ),
TableRow( TableRow(
+7 -4
View File
@@ -51,12 +51,13 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async { Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
try { try {
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
user = await HomeApi().fetchUserInfo(uuid); user = await HomeApi().fetchUserInfo(uuid);
if (user != null && user!.project != null) { if (user != null && user!.project != null) {
await ProjectManager.setProjectUUID(user!.project!.uuid); await ProjectManager.setProjectUUID(user!.project!.uuid);
NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent()); // NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
} }
add(FetchTermEvent()); add(FetchTermEvent());
add(FetchPolicyEvent()); add(FetchPolicyEvent());
@@ -88,10 +89,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
} }
} }
Future _confirmUserAgreement(ConfirmUserAgreementEvent event, Emitter<HomeState> emit) async { Future _confirmUserAgreement(
ConfirmUserAgreementEvent event, Emitter<HomeState> emit) async {
try { try {
emit(LoadingHome()); 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); policy = await HomeApi().confirmUserAgreements(uuid);
emit(PolicyAgreement()); emit(PolicyAgreement());
} catch (e) { } catch (e) {
+10 -6
View File
@@ -24,7 +24,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
void initState() { void initState() {
super.initState(); super.initState();
final homeBloc = BlocProvider.of<HomeBloc>(context); final homeBloc = BlocProvider.of<HomeBloc>(context);
homeBloc.add(const FetchUserInfo()); // homeBloc.add(const FetchUserInfo());
} }
@override @override
@@ -38,8 +38,10 @@ class _HomeWebPageState extends State<HomeWebPage> {
child: BlocConsumer<HomeBloc, HomeState>( child: BlocConsumer<HomeBloc, HomeState>(
listener: (BuildContext context, state) { listener: (BuildContext context, state) {
if (state is HomeInitial) { if (state is HomeInitial) {
if (homeBloc.user!.hasAcceptedWebAgreement == false && !_dialogShown) { if (homeBloc.user!.hasAcceptedWebAgreement == false &&
_dialogShown = true; // Set the flag to true to indicate the dialog is showing. !_dialogShown) {
_dialogShown =
true; // Set the flag to true to indicate the dialog is showing.
Future.delayed(const Duration(seconds: 1), () { Future.delayed(const Duration(seconds: 1), () {
showDialog( showDialog(
context: context, context: context,
@@ -54,7 +56,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
_dialogShown = false; _dialogShown = false;
if (v != null) { if (v != null) {
homeBloc.add(ConfirmUserAgreementEvent()); homeBloc.add(ConfirmUserAgreementEvent());
homeBloc.add(const FetchUserInfo()); // homeBloc.add(const FetchUserInfo());
} }
}); });
}); });
@@ -98,7 +100,8 @@ class _HomeWebPageState extends State<HomeWebPage> {
width: size.width * 0.68, width: size.width * 0.68,
child: GridView.builder( child: GridView.builder(
itemCount: homeBloc.homeItems.length, itemCount: homeBloc.homeItems.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // Adjust as needed. crossAxisCount: 3, // Adjust as needed.
crossAxisSpacing: 20.0, crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0, mainAxisSpacing: 20.0,
@@ -110,7 +113,8 @@ class _HomeWebPageState extends State<HomeWebPage> {
active: homeBloc.homeItems[index].active!, active: homeBloc.homeItems[index].active!,
name: homeBloc.homeItems[index].title!, name: homeBloc.homeItems[index].title!,
img: homeBloc.homeItems[index].icon!, img: homeBloc.homeItems[index].icon!,
onTap: () => homeBloc.homeItems[index].onPress(context), onTap: () =>
homeBloc.homeItems[index].onPress(context),
); );
}, },
), ),
@@ -21,7 +21,8 @@ import 'package:syncrow_web/services/space_mana_api.dart';
import 'package:syncrow_web/services/space_model_mang_api.dart'; import 'package:syncrow_web/services/space_model_mang_api.dart';
import 'package:syncrow_web/utils/constants/action_enum.dart' as custom_action; 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 CommunitySpaceManagementApi _api;
final ProductApi _productApi; final ProductApi _productApi;
final SpaceModelManagementApi _spaceModelApi; final SpaceModelManagementApi _spaceModelApi;
@@ -62,7 +63,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
int page = 1; int page = 1;
while (hasNext) { while (hasNext) {
final spaceModels = await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid); final spaceModels = await _spaceModelApi.listSpaceModels(
page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) { if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels); allSpaceModels.addAll(spaceModels);
page++; page++;
@@ -75,26 +77,29 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
await fetchTags(); await fetchTags();
emit(SpaceModelLoaded( emit(SpaceModelLoaded(
communities: communities: state is SpaceManagementLoaded
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [], ? (state as SpaceManagementLoaded).communities
: [],
products: _cachedProducts ?? [], products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []), spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? [])); allTags: _cachedTags ?? []));
} }
void _deleteSpaceModelFromCache( void _deleteSpaceModelFromCache(DeleteSpaceModelFromCache event,
DeleteSpaceModelFromCache event, Emitter<SpaceManagementState> emit) async { Emitter<SpaceManagementState> emit) async {
if (_cachedSpaceModels != null) { if (_cachedSpaceModels != null) {
_cachedSpaceModels = _cachedSpaceModels = _cachedSpaceModels!
_cachedSpaceModels!.where((model) => model.uuid != event.deletedUuid).toList(); .where((model) => model.uuid != event.deletedUuid)
.toList();
} else { } else {
_cachedSpaceModels = await fetchSpaceModels(); _cachedSpaceModels = await fetchSpaceModels();
} }
await fetchTags(); await fetchTags();
emit(SpaceModelLoaded( emit(SpaceModelLoaded(
communities: communities: state is SpaceManagementLoaded
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [], ? (state as SpaceManagementLoaded).communities
: [],
products: _cachedProducts ?? [], products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []), spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? [])); allTags: _cachedTags ?? []));
@@ -122,8 +127,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
int page = 1; int page = 1;
while (hasNext) { while (hasNext) {
final spaceModels = final spaceModels = await _spaceModelApi.listSpaceModels(
await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid); page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) { if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels); allSpaceModels.addAll(spaceModels);
page++; page++;
@@ -164,10 +169,12 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
await fetchTags(); await fetchTags();
emit(SpaceManagementLoading()); 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 (success) {
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
final updatedCommunities = List<CommunityModel>.from(previousState.communities); final updatedCommunities =
List<CommunityModel>.from(previousState.communities);
for (var community in updatedCommunities) { for (var community in updatedCommunities) {
if (community.uuid == event.communityUuid) { if (community.uuid == event.communityUuid) {
community.name = event.name; community.name = event.name;
@@ -212,7 +219,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
} }
Future<List<SpaceModel>> _fetchSpacesForCommunity(String communityUuid) async { Future<List<SpaceModel>> _fetchSpacesForCommunity(
String communityUuid) async {
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
return await _api.getSpaceHierarchy(communityUuid, projectUuid); return await _api.getSpaceHierarchy(communityUuid, projectUuid);
@@ -242,20 +250,23 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
} }
Future<void> _onBlankState(BlankStateEvent event, Emitter<SpaceManagementState> emit) async { Future<void> _onBlankState(
BlankStateEvent event, Emitter<SpaceManagementState> emit) async {
try { try {
final previousState = state; final previousState = state;
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>(); var spaceBloc = event.context.read<SpaceTreeBloc>();
var spaceTreeState = event.context.read<SpaceTreeBloc>().state; var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState); List<CommunityModel> communities =
await _waitForCommunityList(spaceBloc, spaceTreeState);
await fetchSpaceModels(); await fetchSpaceModels();
await fetchTags(); // await fetchTags();
var prevSpaceModels = await fetchSpaceModels(); var prevSpaceModels = await fetchSpaceModels();
if (previousState is SpaceManagementLoaded || previousState is BlankState) { if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final prevCommunities = (previousState as dynamic).communities ?? []; final prevCommunities = (previousState as dynamic).communities ?? [];
emit(BlankState( emit(BlankState(
communities: List<CommunityModel>.from(prevCommunities), communities: List<CommunityModel>.from(prevCommunities),
@@ -286,7 +297,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
_onloadProducts(); _onloadProducts();
await fetchTags(); await fetchTags();
// Wait until `communityList` is loaded // 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 // Fetch space models after communities are available
final prevSpaceModels = await fetchSpaceModels(); final prevSpaceModels = await fetchSpaceModels();
@@ -310,8 +322,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
final completer = Completer<List<CommunityModel>>(); final completer = Completer<List<CommunityModel>>();
final subscription = spaceBloc.stream.listen((state) { final subscription = spaceBloc.stream.listen((state) {
if (!completer.isCompleted && state.communityList.isNotEmpty) { if (!completer.isCompleted && state.communityList.isNotEmpty) {
completer completer.complete(state.searchQuery.isNotEmpty
.complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList); ? state.filteredCommunity
: state.communityList);
} }
}); });
try { try {
@@ -339,7 +352,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
emit(SpaceManagementLoading()); emit(SpaceManagementLoading());
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final success = await _api.deleteCommunity(event.communityUuid, projectUuid); final success =
await _api.deleteCommunity(event.communityUuid, projectUuid);
if (success) { if (success) {
// add(LoadCommunityAndSpacesEvent()); // add(LoadCommunityAndSpacesEvent());
} else { } else {
@@ -361,12 +375,13 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try { try {
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
await fetchTags(); await fetchTags();
CommunityModel? newCommunity = CommunityModel? newCommunity = await _api.createCommunity(
await _api.createCommunity(event.name, event.description, projectUuid); event.name, event.description, projectUuid);
var prevSpaceModels = await fetchSpaceModels(); var prevSpaceModels = await fetchSpaceModels();
if (newCommunity != null) { if (newCommunity != null) {
if (previousState is SpaceManagementLoaded || previousState is BlankState) { if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final prevCommunities = List<CommunityModel>.from( final prevCommunities = List<CommunityModel>.from(
(previousState as dynamic).communities, (previousState as dynamic).communities,
); );
@@ -459,12 +474,15 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try { try {
final spaceTreeState = event.context.read<SpaceTreeBloc>().state; final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
final updatedSpaces = final updatedSpaces = await saveSpacesHierarchically(
await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid); event.context, event.spaces, event.communityUuid);
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid); final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
emit(SpaceCreationSuccess(spaces: updatedSpaces)); // emit(SpaceCreationSuccess(spaces: updatedSpaces));
// updatedSpaces.forEach(
// (element) => element.uuid,
// );
// final lastUpdatedSpaced = updatedSpaces..addAll(allSpaces);
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
await _updateLoadedState( await _updateLoadedState(
spaceTreeState, spaceTreeState,
@@ -475,7 +493,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
); );
} }
} catch (e) { } catch (e) {
emit(SpaceManagementError('Error saving spaces: $e')); // emit(SpaceManagementError('Error saving spaces: $e'));
if (previousState is SpaceManagementLoaded) { if (previousState is SpaceManagementLoaded) {
emit(previousState); emit(previousState);
@@ -515,13 +533,15 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return; return;
} }
} }
emit(previousState);
} catch (e, stackTrace) { } catch (e, stackTrace) {
rethrow; emit(previousState);
// rethrow;
} }
} }
Future<List<SpaceModel>> saveSpacesHierarchically( Future<List<SpaceModel>> saveSpacesHierarchically(BuildContext context,
BuildContext context, List<SpaceModel> spaces, String communityUuid) async { List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces); final orderedSpaces = flattenHierarchy(spaces);
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@@ -534,6 +554,14 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
selectedCommunity = filteredCommunities.firstWhere( selectedCommunity = filteredCommunities.firstWhere(
(community) => community.uuid == communityUuid, (community) => community.uuid == communityUuid,
orElse: () => CommunityModel(
uuid: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
name: '',
description: '',
spaces: spaces,
),
); );
} catch (e) { } catch (e) {
return []; return [];
@@ -548,9 +576,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (parent.uuid != null) { if (parent.uuid != null) {
await _api.deleteSpace(communityUuid, parent.uuid!, projectUuid); await _api.deleteSpace(communityUuid, parent.uuid!, projectUuid);
} }
} catch (e) { } catch (e) {}
rethrow;
}
} }
orderedSpaces.removeWhere((space) => parentsToDelete.contains(space)); orderedSpaces.removeWhere((space) => parentsToDelete.contains(space));
@@ -564,7 +590,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (matchedSpaces.isEmpty) continue; if (matchedSpaces.isEmpty) continue;
final prevSpace = matchedSpaces[0]; final prevSpace = matchedSpaces.elementAtOrNull(0);
final List<UpdateSubspaceTemplateModel> subspaceUpdates = []; final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
final List<SubspaceModel>? prevSubspaces = prevSpace?.subspaces; final List<SubspaceModel>? prevSubspaces = prevSpace?.subspaces;
@@ -575,17 +601,19 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (prevSubspaces != null || newSubspaces != null) { if (prevSubspaces != null || newSubspaces != null) {
if (prevSubspaces != null && newSubspaces != null) { if (prevSubspaces != null && newSubspaces != null) {
for (var prevSubspace in prevSubspaces) { for (var prevSubspace in prevSubspaces) {
final existsInNew = final existsInNew = newSubspaces
newSubspaces.any((subspace) => subspace.uuid == prevSubspace.uuid); .any((subspace) => subspace.uuid == prevSubspace.uuid);
if (!existsInNew) { if (!existsInNew) {
subspaceUpdates.add(UpdateSubspaceTemplateModel( 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) { } else if (prevSubspaces != null && newSubspaces == null) {
for (var prevSubspace in prevSubspaces) { for (var prevSubspace in prevSubspaces) {
subspaceUpdates.add(UpdateSubspaceTemplateModel( subspaceUpdates.add(UpdateSubspaceTemplateModel(
action: custom_action.Action.delete, uuid: prevSubspace.uuid)); action: custom_action.Action.delete,
uuid: prevSubspace.uuid));
} }
} }
@@ -613,7 +641,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
} }
if (prevSubspaces != null && newSubspaces != null) { 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) { for (var prevSubspace in prevSubspaces) {
final newSubspace = newSubspaceMap[prevSubspace.uuid]; final newSubspace = newSubspaceMap[prevSubspace.uuid];
@@ -639,9 +669,10 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
isPrivate: space.isPrivate, isPrivate: space.isPrivate,
position: space.position, position: space.position,
icon: space.icon, icon: space.icon,
subspaces: subspaceUpdates, subspaces: space.subspaces,
tags: tagUpdates, // subspaceUpdates,
direction: space.incomingConnection?.direction, tags: space.tags,
// tagUpdates,
spaceModelUuid: space.spaceModel?.uuid, spaceModelUuid: space.spaceModel?.uuid,
projectId: projectUuid); projectId: projectUuid);
} else { } else {
@@ -651,8 +682,10 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
: []; : [];
var createSubspaceBodyModels = space.subspaces?.map((subspace) { var createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels = final tagBodyModels = subspace.tags
subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? []; ?.map((tag) => tag.toCreateTagBodyModel())
.toList() ??
[];
return CreateSubspaceModel() return CreateSubspaceModel()
..subspaceName = subspace.subspaceName ..subspaceName = subspace.subspaceName
..tags = tagBodyModels; ..tags = tagBodyModels;
@@ -671,7 +704,6 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
isPrivate: space.isPrivate, isPrivate: space.isPrivate,
position: space.position, position: space.position,
icon: space.icon, icon: space.icon,
direction: space.incomingConnection?.direction,
spaceModelUuid: space.spaceModel?.uuid, spaceModelUuid: space.spaceModel?.uuid,
tags: tagBodyModels, tags: tagBodyModels,
subspaces: createSubspaceBodyModels, subspaces: createSubspaceBodyModels,
@@ -679,7 +711,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
space.uuid = response?.uuid; space.uuid = response?.uuid;
} }
} catch (e) { } catch (e) {
rethrow; // Stop further execution on failure return [];
// Stop further execution on failure
} }
} }
return spaces; return spaces;
@@ -710,7 +743,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return result.toList(); // Convert back to a list 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()); emit(SpaceManagementLoading());
try { try {
@@ -757,14 +791,17 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
// Case 1: Tags deleted // Case 1: Tags deleted
if (prevTags != null && newTags != null) { if (prevTags != null && newTags != null) {
for (var prevTag in prevTags) { 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) { 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) { } else if (prevTags != null && newTags == null) {
for (var prevTag in prevTags) { 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));
} }
} }
@@ -807,15 +844,16 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return tagUpdates; return tagUpdates;
} }
List<SpaceModel> findMatchingSpaces(List<SpaceModel> spaces, String targetUuid) { List<SpaceModel> findMatchingSpaces(
List<SpaceModel> spaces, String targetUuid) {
List<SpaceModel> matched = []; List<SpaceModel> matched = [];
for (var space in spaces) { for (var space in spaces) {
if (space.uuid == targetUuid) { if (space.uuid == targetUuid) {
matched.add(space); matched.add(space);
} }
matched matched.addAll(findMatchingSpaces(
.addAll(findMatchingSpaces(space.children, targetUuid)); // Recursively search in children space.children, targetUuid)); // Recursively search in children
} }
return matched; return matched;
@@ -3,23 +3,26 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model
class Connection { class Connection {
final SpaceModel startSpace; final SpaceModel startSpace;
final SpaceModel endSpace; final SpaceModel endSpace;
final String direction;
Connection({required this.startSpace, required this.endSpace, required this.direction}); Connection({
required this.startSpace,
required this.endSpace,
});
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'startUuid': startSpace.uuid ?? 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces 'startUuid': startSpace.uuid ??
'endUuid': endSpace.uuid ?? 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces
'direction': direction, 'endUuid': endSpace.uuid ??
'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces
}; };
} }
static Connection fromMap(Map<String, dynamic> map, Map<String, SpaceModel> spaces) { static Connection fromMap(
Map<String, dynamic> map, Map<String, SpaceModel> spaces) {
return Connection( return Connection(
startSpace: spaces[map['startUuid']]!, startSpace: spaces[map['startUuid']]!,
endSpace: spaces[map['endUuid']]!, endSpace: spaces[map['endUuid']]!,
direction: map['direction'],
); );
} }
} }
@@ -1,5 +1,7 @@
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'selected_product_model.dart';
class ProductModel { class ProductModel {
final String uuid; final String uuid;
final String catName; final String catName;
@@ -38,6 +40,15 @@ class ProductModel {
}; };
} }
SelectedProduct toSelectedProduct(int count) {
return SelectedProduct(
productId: uuid,
count: count,
productName: name!,
product: this,
);
}
static String _mapIconToProduct(String prodType) { static String _mapIconToProduct(String prodType) {
const iconMapping = { const iconMapping = {
'1G': Assets.Gang1SwitchIcon, '1G': Assets.Gang1SwitchIcon,
@@ -101,7 +101,7 @@ class SpaceModel {
spaceModel: json['spaceModel'] != null spaceModel: json['spaceModel'] != null
? SpaceTemplateModel.fromJson(json['spaceModel']) ? SpaceTemplateModel.fromJson(json['spaceModel'])
: null, : null,
tags: (json['tags'] as List<dynamic>?) tags: (json['productAllocations'] as List<dynamic>?)
?.where((item) => item is Map<String, dynamic>) // Validate type ?.where((item) => item is Map<String, dynamic>) // Validate type
.map((item) => Tag.fromJson(item as Map<String, dynamic>)) .map((item) => Tag.fromJson(item as Map<String, dynamic>))
.toList() ?? .toList() ??
@@ -116,7 +116,6 @@ class SpaceModel {
instance.incomingConnection = Connection( instance.incomingConnection = Connection(
startSpace: instance.parent ?? instance, // Parent space startSpace: instance.parent ?? instance, // Parent space
endSpace: instance, // This space instance endSpace: instance, // This space instance
direction: conn['direction'],
); );
} }
@@ -27,7 +27,7 @@ class SubspaceModel {
subspaceName: json['subspaceName'] ?? '', subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false, disabled: json['disabled'] ?? false,
internalId: internalId, internalId: internalId,
tags: (json['tags'] as List<dynamic>?) tags: (json['productAllocations'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item)) ?.map((item) => Tag.fromJson(item))
.toList() ?? .toList() ??
[], [],
@@ -36,7 +36,7 @@ class SubspaceModel {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'uuid': uuid, if (uuid != null) 'uuid': uuid,
'subspaceName': subspaceName, 'subspaceName': subspaceName,
'disabled': disabled, 'disabled': disabled,
'tags': tags?.map((e) => e.toJson()).toList() ?? [], 'tags': tags?.map((e) => e.toJson()).toList() ?? [],
@@ -23,10 +23,13 @@ class Tag extends BaseTag {
final String internalId = json['internalId'] ?? const Uuid().v4(); final String internalId = json['internalId'] ?? const Uuid().v4();
return Tag( return Tag(
uuid: json['uuid'] ?? '', //TODO:insure UUId for tag or prodAlloc
uuid: json['name'] != null ? json['uuid'] : json['tag']?['uuid'] ?? '',
internalId: internalId, internalId: internalId,
tag: json['name'] ?? '', tag: json['name'] ?? json['tag']?['name'] ?? '',
product: json['product'] != null ? ProductModel.fromMap(json['product']) : null, product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
); );
} }
@@ -49,9 +52,10 @@ class Tag extends BaseTag {
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'uuid': uuid, if (uuid != null) 'uuid': uuid,
'tag': tag, 'name': tag,
'product': product?.toMap(), 'productUuid': product?.uuid,
// .toMap(),
}; };
} }
} }
@@ -67,7 +67,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void initState() { void initState() {
super.initState(); super.initState();
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces(); _adjustCanvasSizeForSpaces();
_nameController = TextEditingController( _nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '', text: widget.selectedCommunity?.name ?? '',
@@ -96,13 +97,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
if (oldWidget.spaces != widget.spaces) { if (oldWidget.spaces != widget.spaces) {
setState(() { setState(() {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces(); _adjustCanvasSizeForSpaces();
realignTree(); realignTree();
}); });
} }
if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) { if (widget.selectedSpace != oldWidget.selectedSpace &&
widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToSpace(widget.selectedSpace!); _moveToSpace(widget.selectedSpace!);
}); });
@@ -185,7 +188,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
connection, widget.selectedSpace) connection, widget.selectedSpace)
? 1.0 ? 1.0
: 0.3, // Adjust opacity : 0.3, // Adjust opacity
child: CustomPaint(painter: CurvedLinePainter([connection])), child: CustomPaint(
painter: CurvedLinePainter([connection])),
), ),
for (var entry in spaces.asMap().entries) for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted && if (entry.value.status != SpaceStatus.deleted &&
@@ -195,11 +199,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
top: entry.value.position.dy, top: entry.value.position.dy,
child: SpaceCardWidget( child: SpaceCardWidget(
index: entry.key, index: entry.key,
onButtonTap: (int index, Offset newPosition, String direction) { onButtonTap: (int index, Offset newPosition) {
_showCreateSpaceDialog(screenSize, _showCreateSpaceDialog(screenSize,
position: spaces[index].position + newPosition, position:
spaces[index].position + newPosition,
parentIndex: index, parentIndex: index,
direction: direction,
projectTags: widget.projectTags); projectTags: widget.projectTags);
}, },
position: entry.value.position, position: entry.value.position,
@@ -210,7 +214,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_updateNodePosition(entry.value, newPosition); _updateNodePosition(entry.value, newPosition);
}, },
buildSpaceContainer: (int index) { buildSpaceContainer: (int index) {
final bool isHighlighted = SpaceHelper.isHighlightedSpace( final bool isHighlighted =
SpaceHelper.isHighlightedSpace(
spaces[index], widget.selectedSpace); spaces[index], widget.selectedSpace);
return Opacity( return Opacity(
@@ -289,7 +294,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _showCreateSpaceDialog(Size screenSize, void _showCreateSpaceDialog(Size screenSize,
{Offset? position, {Offset? position,
int? parentIndex, int? parentIndex,
String? direction,
double? canvasWidth, double? canvasWidth,
double? canvasHeight, double? canvasHeight,
required List<Tag> projectTags}) { required List<Tag> projectTags}) {
@@ -299,19 +303,25 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog( return CreateSpaceDialog(
products: widget.products, products: widget.products,
spaceModels: widget.spaceModels, spaceModels: widget.spaceModels,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels), allTags:
TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
parentSpace: parentIndex != null ? spaces[parentIndex] : null, parentSpace: parentIndex != null ? spaces[parentIndex] : null,
projectTags: projectTags, projectTags: projectTags,
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts, onCreateSpace: (String name,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) { String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() { setState(() {
// Set the first space in the center or use passed position // Set the first space in the center or use passed position
Offset newPosition; Offset newPosition;
if (parentIndex != null) { if (parentIndex != null) {
newPosition = newPosition = getBalancedChildPosition(
getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position spaces[parentIndex]); // Ensure balanced position
} else { } else {
newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize); newPosition =
position ?? ConnectionHelper.getCenterPosition(screenSize);
} }
SpaceModel newSpace = SpaceModel( SpaceModel newSpace = SpaceModel(
@@ -325,14 +335,13 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
subspaces: subspaces, subspaces: subspaces,
tags: tags); tags: tags);
if (parentIndex != null && direction != null) { if (parentIndex != null) {
SpaceModel parentSpace = spaces[parentIndex]; SpaceModel parentSpace = spaces[parentIndex];
parentSpace.internalId = spaces[parentIndex].internalId; parentSpace.internalId = spaces[parentIndex].internalId;
newSpace.parent = parentSpace; newSpace.parent = parentSpace;
final newConnection = Connection( final newConnection = Connection(
startSpace: parentSpace, startSpace: parentSpace,
endSpace: newSpace, endSpace: newSpace,
direction: direction,
); );
connections.add(newConnection); connections.add(newConnection);
newSpace.incomingConnection = newConnection; newSpace.incomingConnection = newConnection;
@@ -360,16 +369,21 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
name: widget.selectedSpace!.name, name: widget.selectedSpace!.name,
icon: widget.selectedSpace!.icon, icon: widget.selectedSpace!.icon,
projectTags: widget.projectTags, projectTags: widget.projectTags,
parentSpace: parentSpace: SpaceHelper.findSpaceByInternalId(
SpaceHelper.findSpaceByInternalId(widget.selectedSpace?.parent?.internalId, spaces), widget.selectedSpace?.parent?.internalId, spaces),
editSpace: widget.selectedSpace, editSpace: widget.selectedSpace,
currentSpaceModel: widget.selectedSpace?.spaceModel, currentSpaceModel: widget.selectedSpace?.spaceModel,
tags: widget.selectedSpace?.tags, tags: widget.selectedSpace?.tags,
subspaces: widget.selectedSpace?.subspaces, subspaces: widget.selectedSpace?.subspaces,
isEdit: true, isEdit: true,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels), allTags: TagHelper.getAllTagValues(
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts, widget.communities, widget.spaceModels),
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(() { setState(() {
// Update the space's properties // Update the space's properties
widget.selectedSpace!.name = name; widget.selectedSpace!.name = name;
@@ -379,7 +393,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
widget.selectedSpace!.tags = tags; widget.selectedSpace!.tags = tags;
if (widget.selectedSpace!.status != SpaceStatus.newSpace) { 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) { for (var space in spaces) {
@@ -410,7 +425,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Map<String, SpaceModel> idToSpace = {}; Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) { void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) { if (space.status == SpaceStatus.deleted ||
space.status == SpaceStatus.parentDeleted) {
return; return;
} }
result.add(space); result.add(space);
@@ -447,7 +463,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Connection( Connection(
startSpace: parent, startSpace: parent,
endSpace: child, endSpace: child,
direction: "down",
), ),
); );
@@ -532,13 +547,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _selectSpace(BuildContext context, SpaceModel space) { void _selectSpace(BuildContext context, SpaceModel space) {
context.read<SpaceManagementBloc>().add( context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: space), SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
); );
} }
void _deselectSpace(BuildContext context) { void _deselectSpace(BuildContext context) {
context.read<SpaceManagementBloc>().add( context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: null), SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
); );
} }
@@ -708,7 +726,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent); 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 = []; List<SpaceModel> duplicatedSubtree = [];
void collectSubtree(SpaceModel node) { void collectSubtree(SpaceModel node) {
duplicatedSubtree.add(node); duplicatedSubtree.add(node);
@@ -726,7 +745,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection( final newConnection = Connection(
startSpace: parent, startSpace: parent,
endSpace: duplicated, endSpace: duplicated,
direction: "down",
); );
connections.add(newConnection); connections.add(newConnection);
duplicated.incomingConnection = newConnection; duplicated.incomingConnection = newConnection;
@@ -739,7 +757,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) { SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) {
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces); final duplicatedName =
SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final newSpace = SpaceModel( final newSpace = SpaceModel(
name: duplicatedName, name: duplicatedName,
@@ -761,7 +780,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection( final newConnection = Connection(
startSpace: newSpace, startSpace: newSpace,
endSpace: duplicatedChild, endSpace: duplicatedChild,
direction: "down",
); );
connections.add(newConnection); connections.add(newConnection);
@@ -0,0 +1,101 @@
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,
),
)
],
);
}
}
@@ -0,0 +1,65 @@
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,
),
),
),
),
],
),
],
);
}
}
@@ -0,0 +1,89 @@
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),
],
),
),
],
);
}
}
@@ -0,0 +1,82 @@
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),
),
),
],
);
}
}
@@ -0,0 +1,101 @@
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,27 +30,12 @@ class CurvedLinePainter extends CustomPainter {
Offset end = connection.endSpace.position + Offset end = connection.endSpace.position +
const Offset(75, 0); // Center top of end space const Offset(75, 0); // Center top of end space
if (connection.direction == 'down') {
// Curved line for down connections // Curved line for down connections
final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50);
final path = Path() final path = Path()
..moveTo(start.dx, start.dy) ..moveTo(start.dx, start.dy)
..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint); 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; final dotPaint = Paint()..color = ColorsManager.blackColor;
canvas.drawCircle(start, 5, dotPaint); // Start dot canvas.drawCircle(start, 5, dotPaint); // Start dot
@@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; 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/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_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/add_device_type/views/add_device_type_widget.dart';
@@ -9,6 +7,11 @@ 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/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/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/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/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/assign_tag/views/assign_tag_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
@@ -16,8 +19,6 @@ 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/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/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/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/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/space_icon_const.dart'; import 'package:syncrow_web/utils/constants/space_icon_const.dart';
@@ -82,8 +83,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
super.initState(); super.initState();
selectedIcon = widget.icon ?? Assets.location; selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? ''); nameController = TextEditingController(text: widget.name ?? '');
selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; selectedProducts =
isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty; widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
if (widget.currentSpaceModel != null) { if (widget.currentSpaceModel != null) {
subspaces = []; subspaces = [];
tags = []; tags = [];
@@ -96,13 +99,15 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isSpaceModelDisabled = bool isSpaceModelDisabled = (tags != null && tags!.isNotEmpty ||
(tags != null && tags!.isNotEmpty || subspaces != null && subspaces!.isNotEmpty); subspaces != null && subspaces!.isNotEmpty);
bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null); bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog( 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, backgroundColor: ColorsManager.whiteColors,
content: SizedBox( content: SizedBox(
width: screenWidth * 0.5, width: screenWidth * 0.5,
@@ -112,50 +117,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
children: [ children: [
Expanded( Expanded(
flex: 1, flex: 1,
child: Column( child: IconChoosePartWidget(
mainAxisAlignment: MainAxisAlignment.center, selectedIcon: selectedIcon,
// crossAxisAlignment: CrossAxisAlignment.center, showIconSelection: _showIconSelectionDialog,
children: [ screenWidth: screenWidth,
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), const SizedBox(width: 20),
@@ -164,11 +129,12 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( SpaceNameTextfieldWidget(
width: screenWidth * 0.25, isNameFieldExist: isNameFieldExist,
child: TextField( isNameFieldInvalid: isNameFieldInvalid,
controller: nameController, nameController: nameController,
onChanged: (value) { screenWidth: screenWidth,
onChange: (value) {
enteredName = value.trim(); enteredName = value.trim();
setState(() { setState(() {
isNameFieldExist = false; isNameFieldExist = false;
@@ -187,273 +153,88 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
} }
}); });
}, },
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),
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
selectedSpaceModel == null // SpaceModelLinkingWidget(
? TextButton( // isSpaceModelDisabled: true,
style: TextButton.styleFrom( // // isSpaceModelDisabled,
padding: EdgeInsets.zero, // 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: () { 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 isTagsAndSubspaceModelDisabled
? null ? null
: _showSubSpaceDialog( : _showSubSpaceDialog(
context, enteredName, [], false, widget.products, subspaces); context,
enteredName,
[],
false,
widget.products,
subspaces,
);
}, },
child: ButtonContentWidget( isTagsAndSubspaceModelDisabled:
icon: Icons.add, isTagsAndSubspaceModelDisabled,
label: 'Create Sub Space', screenWidth: screenWidth,
disabled: isTagsAndSubspaceModelDisabled, editChipOnTap: () async {
), _showSubSpaceDialog(
) context,
: SizedBox( enteredName,
width: screenWidth * 0.25, [],
child: Container( true,
padding: const EdgeInsets.all(8.0), widget.products,
decoration: BoxDecoration( subspaces,
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), const SizedBox(height: 10),
(tags?.isNotEmpty == true || DevicesPartWidget(
subspaces?.any((subspace) => subspace.tags?.isNotEmpty == true) == true) tags: tags,
? SizedBox( subspaces: subspaces,
width: screenWidth * 0.25, screenWidth: screenWidth,
child: Container( isTagsAndSubspaceModelDisabled:
padding: const EdgeInsets.all(8.0), isTagsAndSubspaceModelDisabled,
decoration: BoxDecoration( onEditChip: () async {
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( await showDialog(
context: context, context: context,
builder: (context) => AssignTagDialog( builder: (context) => AssignTagDialog(
@@ -476,13 +257,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
}, },
), ),
); );
}) },
], onTextButtonPressed: () {
),
),
)
: TextButton(
onPressed: () {
isTagsAndSubspaceModelDisabled isTagsAndSubspaceModelDisabled
? null ? null
: _showTagCreateDialog( : _showTagCreateDialog(
@@ -492,14 +268,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
widget.products, widget.products,
); );
}, },
style: TextButton.styleFrom( )
padding: EdgeInsets.zero,
),
child: ButtonContentWidget(
icon: Icons.add,
label: 'Add Devices',
disabled: isTagsAndSubspaceModelDisabled,
))
], ],
), ),
), ),
@@ -529,17 +298,32 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
} else if (isNameFieldExist) { } else if (isNameFieldExist) {
return; return;
} else { } else {
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? ''); String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) { if (newName.isNotEmpty) {
widget.onCreateSpace(newName, selectedIcon, selectedProducts, if (tags != null && tags!.isNotEmpty) {
selectedSpaceModel, subspaces, tags); if (tags!.any(
(tag) => tag.uuid == null || tag.uuid!.isEmpty,
)) {
return;
}
}
widget.onCreateSpace(
newName,
selectedIcon,
selectedProducts,
selectedSpaceModel,
subspaces,
tags);
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
}, },
borderRadius: 10, borderRadius: 10,
backgroundColor: backgroundColor: isOkButtonEnabled
isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor, ? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors, foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'), child: const Text('OK'),
), ),
@@ -550,6 +334,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
); );
} }
//dialooogggs
void _showIconSelectionDialog() { void _showIconSelectionDialog() {
showDialog( showDialog(
context: context, context: context,
@@ -586,26 +371,50 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
); );
} }
void _showSubSpaceDialog(BuildContext context, String name, final List<Tag>? spaceTags, void _showSubSpaceDialog(
bool isEdit, List<ProductModel>? products, final List<SubspaceModel>? existingSubSpaces) { BuildContext context,
String name,
final List<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? existingSubSpaces,
) {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return CreateSubSpaceDialog( return CreateSubSpaceDialog(
spaceName: name, spaceName: name,
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space', dialogTitle: isEdit ? 'Edit Sub-spaces' : 'Create Sub-spaces',
products: products, products: products,
existingSubSpaces: existingSubSpaces, existingSubSpaces: existingSubSpaces,
onSave: (slectedSubspaces) { onSave: (slectedSubspaces, updatedSubSpaces) {
final List<Tag> tagsToAppendToSpace = []; final List<Tag> tagsToAppendToSpace = [];
if (slectedSubspaces != null) { if (slectedSubspaces != null && slectedSubspaces.isNotEmpty) {
final updatedIds = slectedSubspaces.map((s) => s.internalId).toSet(); final updatedIds =
slectedSubspaces.map((s) => s.internalId).toSet();
if (existingSubSpaces != null) { if (existingSubSpaces != null) {
final deletedSubspaces = final deletedSubspaces = existingSubSpaces
existingSubSpaces.where((s) => !updatedIds.contains(s.internalId)).toList(); .where((s) => !updatedIds.contains(s.internalId))
.toList();
for (var s in deletedSubspaces) { for (var s in deletedSubspaces) {
if (s.tags != null) { 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!); tagsToAppendToSpace.addAll(s.tags!);
} }
} }
@@ -623,15 +432,16 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
); );
} }
void _showTagCreateDialog( void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
BuildContext context, String name, bool isEdit, List<ProductModel>? products) { List<ProductModel>? products) {
isEdit isEdit
? showDialog( ? showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AssignTagDialog( return AssignTagDialog(
title: 'Edit Device', title: 'Edit Device',
addedProducts: TagHelper.createInitialSelectedProductsForTags(tags, subspaces), addedProducts: TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
spaceName: name, spaceName: name,
products: products, products: products,
subspaces: subspaces, subspaces: subspaces,
@@ -646,7 +456,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) { if (subspaces != null) {
for (final subspace in subspaces!) { for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) { for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) { if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags; subspace.tags = selectedSubspace.tags;
} }
} }
@@ -670,7 +481,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
allTags: widget.allTags, allTags: widget.allTags,
projectTags: widget.projectTags, projectTags: widget.projectTags,
initialSelectedProducts: initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(tags, subspaces), TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) { onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() { setState(() {
tags = selectedSpaceTags; tags = selectedSpaceTags;
@@ -680,7 +492,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) { if (subspaces != null) {
for (final subspace in subspaces!) { for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) { for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) { if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags; subspace.tags = selectedSubspace.tags;
} }
} }
@@ -5,7 +5,7 @@ class PlusButtonWidget extends StatelessWidget {
final int index; final int index;
final String direction; final String direction;
final Offset offset; final Offset offset;
final Function(int index, Offset newPosition, String direction) onButtonTap; final Function(int index, Offset newPosition) onButtonTap;
const PlusButtonWidget({ const PlusButtonWidget({
super.key, super.key,
@@ -17,26 +17,9 @@ class PlusButtonWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Positioned( return GestureDetector(
left: offset.dx,
top: offset.dy,
child: GestureDetector(
onTap: () { onTap: () {
Offset newPosition; onButtonTap(index, const Offset(0, 150));
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( child: Container(
width: 30, width: 30,
@@ -45,7 +28,10 @@ class PlusButtonWidget extends StatelessWidget {
color: ColorsManager.spaceColor, color: ColorsManager.spaceColor,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20), child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
), ),
), ),
); );
@@ -7,7 +7,7 @@ class SpaceCardWidget extends StatelessWidget {
final Offset position; final Offset position;
final bool isHovered; final bool isHovered;
final Function(int index, bool isHovered) onHoverChanged; final Function(int index, bool isHovered) onHoverChanged;
final Function(int index, Offset newPosition, String direction) onButtonTap; final Function(int index, Offset newPosition) onButtonTap;
final Widget Function(int index) buildSpaceContainer; final Widget Function(int index) buildSpaceContainer;
final ValueChanged<Offset> onPositionChanged; final ValueChanged<Offset> onPositionChanged;
@@ -25,35 +25,34 @@ class SpaceCardWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return MouseRegion(
behavior: HitTestBehavior.opaque, onEnter: (_) => onHoverChanged(index, true),
onPanUpdate: (details) { onExit: (_) => onHoverChanged(index, false),
// Call the provided callback to update the position child: SizedBox(
final newPosition = position + details.delta; width: 140, // Make sure this covers both card and plus button
onPositionChanged(newPosition); height: 90,
},
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( child: Stack(
clipBehavior: Clip clipBehavior: Clip.none,
.none, // Allow hovering elements to be displayed outside the boundary
children: [ children: [
buildSpaceContainer(index), // Build the space container // Main card
if (isHovered) ...[ Container(
PlusButtonWidget( 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, index: index,
direction: 'down', direction: 'down',
offset: const Offset(63, 50), offset: Offset.zero,
onButtonTap: onButtonTap, onButtonTap: onButtonTap,
), ),
], ),
], ],
), ),
), ),
@@ -13,7 +13,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final existingTagCounts = <String, int>{}; final existingTagCounts = <String, int>{};
for (var tag in initialTags) { for (var tag in initialTags) {
if (tag.product != null) { if (tag.product != null) {
existingTagCounts[tag.product!.uuid] = (existingTagCounts[tag.product!.uuid] ?? 0) + 1; existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
} }
} }
@@ -22,14 +23,17 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
for (var selectedProduct in event.addedProducts) { for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 || selectedProduct.count <= existingCount) { if (selectedProduct.count == 0 ||
tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId)); selectedProduct.count <= existingCount) {
tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
continue; continue;
} }
final missingCount = selectedProduct.count - existingCount; 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) { if (missingCount > 0) {
tags.addAll(List.generate( tags.addAll(List.generate(
@@ -85,7 +89,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final tags = List<Tag>.from(currentState.tags); final tags = List<Tag>.from(currentState.tags);
// Update the location // 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); final updatedTags = _calculateAvailableTags(projectTags, tags);
@@ -117,7 +122,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final currentState = state; final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { 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 // Recalculate available tags
final updatedTags = _calculateAvailableTags(projectTags, tags); final updatedTags = _calculateAvailableTags(projectTags, tags);
@@ -141,8 +147,10 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
// Get validation error for duplicate tags // Get validation error for duplicate tags
String? _getValidationError(List<Tag> tags) { String? _getValidationError(List<Tag> tags) {
final nonEmptyTags = final nonEmptyTags = tags
tags.map((tag) => tag.tag?.trim() ?? '').where((tag) => tag.isNotEmpty).toList(); .map((tag) => tag.tag?.trim() ?? '')
.where((tag) => tag.isNotEmpty)
.toList();
final duplicateTags = nonEmptyTags final duplicateTags = nonEmptyTags
.fold<Map<String, int>>({}, (map, tag) { .fold<Map<String, int>>({}, (map, tag) {
@@ -168,9 +176,11 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
.toSet(); .toSet();
final availableTags = allTags final availableTags = allTags
.where((tag) => tag.tag != null && !selectedTagSet.contains(tag.tag!.trim())) .where((tag) =>
tag.tag != null && !selectedTagSet.contains(tag.tag!.trim()))
.toList(); .toList();
return availableTags; return projectTags;
// availableTags;
} }
} }
@@ -1,10 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/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/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/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
@@ -12,9 +7,10 @@ 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_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_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/bloc/assign_tag_state.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:uuid/uuid.dart';
import 'widgets/save_add_device_row_widget.dart';
class AssignTagDialog extends StatelessWidget { class AssignTagDialog extends StatelessWidget {
final List<ProductModel>? products; final List<ProductModel>? products;
@@ -29,7 +25,7 @@ class AssignTagDialog extends StatelessWidget {
final List<Tag> projectTags; final List<Tag> projectTags;
const AssignTagDialog( const AssignTagDialog(
{Key? key, {super.key,
required this.products, required this.products,
required this.subspaces, required this.subspaces,
required this.addedProducts, required this.addedProducts,
@@ -39,13 +35,14 @@ class AssignTagDialog extends StatelessWidget {
required this.spaceName, required this.spaceName,
required this.title, required this.title,
this.onSave, this.onSave,
required this.projectTags}) required this.projectTags});
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> locations = final List<String> locations = (subspaces ?? [])
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space'); .map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider( return BlocProvider(
create: (_) => AssignTagBloc(projectTags) create: (_) => AssignTagBloc(projectTags)
@@ -67,131 +64,31 @@ class AssignTagDialog extends StatelessWidget {
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
ClipRRect( AssignTagsTable(
borderRadius: BorderRadius.circular(20), controllers: controllers,
child: DataTable( locations: locations,
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), tags: state.tags,
key: ValueKey(state.tags.length), updatedTags: state.updatedTags,
border: TableBorder.all( onDeleteDevice: ({required index, required tag}) {
color: ColorsManager.dataHeaderGrey, context
width: 1, .read<AssignTagBloc>()
borderRadius: BorderRadius.circular(20), .add(DeleteTag(tagToDelete: tag, tags: state.tags));
),
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];
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); controllers.removeAt(index);
}, },
tooltip: 'Delete Tag', onTagDropDownSelected: ({required index, required 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( context.read<AssignTagBloc>().add(UpdateTagEvent(
index: index, index: index,
tag: value, tag: tag,
)); ));
}, },
), onLocationDropDownSelected: (
), {required index, required location}) {
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue: tag.location ?? 'Main Space',
onSelected: (value) {
context.read<AssignTagBloc>().add(UpdateLocation( context.read<AssignTagBloc>().add(UpdateLocation(
index: index, index: index,
location: value, location: location,
)); ));
}, },
)),
),
],
);
}),
),
), ),
if (state.errorMessage != null) if (state.errorMessage != null)
Text(state.errorMessage!, Text(state.errorMessage!,
@@ -203,69 +100,15 @@ class AssignTagDialog extends StatelessWidget {
), ),
), ),
actions: [ actions: [
Row( SaveAddDeviceRowWidget(
mainAxisAlignment: MainAxisAlignment.spaceAround, subspaces: subspaces,
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, products: products,
subspaces: processedSubspaces,
projectTags: projectTags, projectTags: projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
processedTags, processedSubspaces),
spaceName: spaceName, spaceName: spaceName,
spaceTags: processedTags,
isCreate: false,
onSave: onSave, onSave: onSave,
allTags: allTags, allTags: allTags,
), tags: state.tags,
); isSaveEnabled: state.isSaveEnabled,
},
),
),
),
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),
],
), ),
], ],
); );
@@ -0,0 +1,100 @@
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,9 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/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/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/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
@@ -12,11 +8,10 @@ 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/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/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/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 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:uuid/uuid.dart'; import 'widgets/RowOfCancelSaveWidget.dart';
import 'widgets/assign_tags_tables_widget.dart';
class AssignTagModelsDialog extends StatelessWidget { class AssignTagModelsDialog extends StatelessWidget {
final List<ProductModel>? products; final List<ProductModel>? products;
@@ -53,8 +48,10 @@ class AssignTagModelsDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<String> locations = final List<String> locations = (subspaces ?? [])
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space'); .map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider( return BlocProvider(
create: (_) => AssignTagModelBloc(projectTags) create: (_) => AssignTagModelBloc(projectTags)
@@ -78,137 +75,38 @@ class AssignTagModelsDialog extends StatelessWidget {
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
ClipRRect( AssignTagsTable(
borderRadius: BorderRadius.circular(20), controllers: controllers,
child: DataTable( locations: locations,
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), tags: state.tags,
key: ValueKey(state.tags.length), updatedTags: state.updatedTags,
border: TableBorder.all( onDeleteDevice: ({required index, required tag}) {
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 context
.read<AssignTagModelBloc>() .read<AssignTagModelBloc>()
.add(UpdateLocation( .add(DeleteTagModel(
index: index, tagToDelete: tag,
location: value, 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,
), ),
);
},
), ),
if (state.errorMessage != null) if (state.errorMessage != null)
Text(state.errorMessage!, Text(state.errorMessage!,
@@ -220,101 +118,16 @@ class AssignTagModelsDialog extends StatelessWidget {
), ),
), ),
actions: [ actions: [
Row( RowOfSaveCancelWidget(
mainAxisAlignment: MainAxisAlignment.spaceAround, subspaces: subspaces,
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, products: products,
subspaces: processedSubspaces,
isCreate: false,
initialSelectedProducts:
TagHelper.createInitialSelectedProducts(
processedTags, processedSubspaces),
allTags: allTags, allTags: allTags,
spaceName: spaceName, spaceName: spaceName,
otherSpaceModels: otherSpaceModels, otherSpaceModels: otherSpaceModels,
spaceTagModels: processedTags,
pageContext: pageContext, pageContext: pageContext,
projectTags: projectTags, projectTags: projectTags,
spaceModel: SpaceTemplateModel( spaceModel: spaceModel,
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, 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),
],
), ),
], ],
); );
@@ -0,0 +1,152 @@
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();
}
},
);
}
}
@@ -0,0 +1,155 @@
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( emit(SubSpaceState(
updatedSubSpaces, updatedSubSpaces,
updatedSubspaceModels, updatedSubspaceModels,
errorMessage, errorMessage ,
updatedDuplicates, updatedDuplicates,
)); ));
}); });
@@ -1,23 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/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/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/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_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_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.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 { class CreateSubSpaceDialog extends StatefulWidget {
final String dialogTitle; final String dialogTitle;
final List<SubspaceModel>? existingSubSpaces; final List<SubspaceModel>? existingSubSpaces;
final String? spaceName; final String? spaceName;
final List<ProductModel>? products; final List<ProductModel>? products;
final void Function(List<SubspaceModel>?)? onSave; final void Function(
List<SubspaceModel>?, List<UpdateSubspaceModel> updatedSubSpaces)? onSave;
const CreateSubSpaceDialog({ const CreateSubSpaceDialog({
required this.dialogTitle, required this.dialogTitle,
@@ -33,14 +33,7 @@ class CreateSubSpaceDialog extends StatefulWidget {
} }
class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> { class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
late final TextEditingController _subspaceNameController; final TextEditingController _subspaceNameController = TextEditingController();
@override
void initState() {
_subspaceNameController = TextEditingController();
super.initState();
}
@override @override
void dispose() { void dispose() {
_subspaceNameController.dispose(); _subspaceNameController.dispose();
@@ -79,83 +72,26 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
), ),
const SizedBox(height: 16), Row(
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: [ children: [
...state.subSpaces.asMap().entries.map( Text(
(entry) { 'press Enter to Save',
final index = entry.key; style: context.textTheme.headlineLarge?.copyWith(
final subSpace = entry.value; fontWeight: FontWeight.w500,
fontSize: 10,
final lowerName = subSpace.subspaceName.toLowerCase(); color: ColorsManager.grayColor,
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) { const SizedBox(
final trimmedValue = value.trim(); width: 5,
if (trimmedValue.isNotEmpty) {
context.read<SubSpaceBloc>().add(
AddSubSpace(
SubspaceModel(
subspaceName: trimmedValue,
disabled: false,
),
),
);
_subspaceNameController.clear();
}
},
style: context.textTheme.bodyMedium,
),
), ),
const Icon(Icons.save_as_sharp, size: 10),
], ],
), ),
const SizedBox(height: 16),
TextFieldSubSpaceDialogWidget(
subspaceNameController: _subspaceNameController,
subSpaces: state.subSpaces,
), ),
if (state.errorMessage.isNotEmpty) if (state.errorMessage.isNotEmpty)
Padding( Padding(
@@ -168,36 +104,10 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( OkCancelSubSpaceWidget(
children: [ subspaceNameController: _subspaceNameController,
Expanded( errorMessage: state.errorMessage,
child: CancelButton( onSave: widget.onSave,
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'),
),
),
],
), ),
], ],
), ),
@@ -0,0 +1,75 @@
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'),
),
),
],
);
}
}
@@ -0,0 +1,98 @@
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,
),
),
],
),
);
}
}
@@ -31,15 +31,19 @@ class CenterBodyWidget extends StatelessWidget {
children: [ children: [
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent()); context
.read<CenterBodyBloc>()
.add(CommunityStructureSelectedEvent());
}, },
child: Text( child: Text(
'Community Structure', 'Community Structure',
style: Theme.of(context).textTheme.bodyLarge!.copyWith( style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is CommunityStructureState || state is CommunitySelectedState fontWeight: state is CommunityStructureState ||
state is CommunitySelectedState
? FontWeight.bold ? FontWeight.bold
: FontWeight.normal, : 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.bodyLarge!.color
: Theme.of(context) : Theme.of(context)
.textTheme .textTheme
@@ -50,26 +54,26 @@ class CenterBodyWidget extends StatelessWidget {
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
GestureDetector( // GestureDetector(
onTap: () { // onTap: () {
context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent()); // context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
}, // },
child: Text( // child: Text(
'Space Model', // 'Space Model',
style: Theme.of(context).textTheme.bodyLarge!.copyWith( // style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is SpaceModelState // fontWeight: state is SpaceModelState
? FontWeight.bold // ? FontWeight.bold
: FontWeight.normal, // : FontWeight.normal,
color: state is SpaceModelState // color: state is SpaceModelState
? Theme.of(context).textTheme.bodyLarge!.color // ? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context) // : Theme.of(context)
.textTheme // .textTheme
.bodyLarge! // .bodyLarge!
.color! // .color!
.withOpacity(0.5), // .withOpacity(0.5),
), // ),
), // ),
), // ),
], ],
), ),
], ],
@@ -24,37 +24,22 @@ class AddDeviceTypeModelBloc
if (currentState is AddDeviceModelLoaded) { if (currentState is AddDeviceModelLoaded) {
final existingProduct = currentState.selectedProducts.firstWhere( final existingProduct = currentState.selectedProducts.firstWhere(
(p) => p.productId == event.productId, (p) => p.productId == event.selectedProduct.productId,
orElse: () => SelectedProduct( orElse: () => event.selectedProduct,
productId: event.productId,
count: 0,
productName: event.productName,
product: event.product,
),
); );
List<SelectedProduct> updatedProducts; List<SelectedProduct> updatedProducts;
if (event.count > 0) { if (event.selectedProduct.count > 0) {
if (!currentState.selectedProducts.contains(existingProduct)) { if (!currentState.selectedProducts.contains(existingProduct)) {
updatedProducts = [ updatedProducts = [
...currentState.selectedProducts, ...currentState.selectedProducts,
SelectedProduct( event.selectedProduct,
productId: event.productId,
count: event.count,
productName: event.productName,
product: event.product,
),
]; ];
} else { } else {
updatedProducts = currentState.selectedProducts.map((p) { updatedProducts = currentState.selectedProducts.map((p) {
if (p.productId == event.productId) { if (p.productId == event.selectedProduct.productId) {
return SelectedProduct( return event.selectedProduct;
productId: p.productId,
count: event.count,
productName: p.productName,
product: p.product,
);
} }
return p; return p;
}).toList(); }).toList();
@@ -62,7 +47,7 @@ class AddDeviceTypeModelBloc
} else { } else {
// Remove the product if the count is 0 // Remove the product if the count is 0
updatedProducts = currentState.selectedProducts updatedProducts = currentState.selectedProducts
.where((p) => p.productId != event.productId) .where((p) => p.productId != event.selectedProduct.productId)
.toList(); .toList();
} }
@@ -10,20 +10,15 @@ abstract class AddDeviceTypeModelEvent extends Equatable {
List<Object> get props => []; List<Object> get props => [];
} }
class UpdateProductCountEvent extends AddDeviceTypeModelEvent { class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
final String productId; final SelectedProduct selectedProduct;
final int count;
final String productName;
final ProductModel product;
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product}); UpdateProductCountEvent({required this.selectedProduct});
@override @override
List<Object> get props => [productId, count]; List<Object> get props => [selectedProduct];
} }
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent { class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
final List<Tag> initialTags; final List<Tag> initialTags;
final List<SelectedProduct> addedProducts; final List<SelectedProduct> addedProducts;
@@ -54,10 +54,10 @@ class DeviceTypeTileWidget extends StatelessWidget {
onCountChanged: (newCount) { onCountChanged: (newCount) {
context.read<AddDeviceTypeModelBloc>().add( context.read<AddDeviceTypeModelBloc>().add(
UpdateProductCountEvent( UpdateProductCountEvent(
productId: product.uuid, selectedProduct: product.toSelectedProduct(
count: newCount, newCount,
productName: product.catName, ),
product: product), ),
); );
}, },
), ),
+12
View File
@@ -22,6 +22,18 @@ class HTTPService {
); );
client.interceptors.add(serviceLocator.get<HTTPInterceptor>()); 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; return client;
} }
+44 -24
View File
@@ -4,22 +4,27 @@ 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/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_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/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/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_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_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/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/api_const.dart';
import '../pages/spaces_management/all_spaces/model/subspace_model.dart';
class CommunitySpaceManagementApi { class CommunitySpaceManagementApi {
// Community Management APIs // Community Management APIs
Future<List<CommunityModel>> fetchCommunities(String projectId, {int page = 1}) async { Future<List<CommunityModel>> fetchCommunities(String projectId,
{int page = 1}) async {
try { try {
List<CommunityModel> allCommunities = []; List<CommunityModel> allCommunities = [];
bool hasNext = true; bool hasNext = true;
while (hasNext) { while (hasNext) {
await HTTPService().get( await HTTPService().get(
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), path: ApiEndpoints.getCommunityList
.replaceAll('{projectId}', projectId),
queryParameters: { queryParameters: {
'page': page, 'page': page,
}, },
@@ -55,8 +60,14 @@ class CommunitySpaceManagementApi {
try { try {
bool hasNext = false; bool hasNext = false;
await HTTPService().get( await HTTPService().get(
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), path:
queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search}, ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {
'page': page,
'includeSpaces': true,
'size': 25,
'search': search
},
expectedResponseModel: (json) { expectedResponseModel: (json) {
try { try {
List<dynamic> jsonData = json['data'] ?? []; List<dynamic> jsonData = json['data'] ?? [];
@@ -68,7 +79,10 @@ class CommunitySpaceManagementApi {
page = currentPage + 1; page = currentPage + 1;
paginationModel = PaginationModel( paginationModel = PaginationModel(
pageNum: page, hasNext: hasNext, size: 25, communities: communityList); pageNum: page,
hasNext: hasNext,
size: 25,
communities: communityList);
return paginationModel; return paginationModel;
} catch (_) { } catch (_) {
hasNext = false; hasNext = false;
@@ -83,7 +97,8 @@ class CommunitySpaceManagementApi {
Future<CommunityModel?> getCommunityById(String communityId) async { Future<CommunityModel?> getCommunityById(String communityId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), path: ApiEndpoints.getCommunityById
.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) { expectedResponseModel: (json) {
return CommunityModel.fromJson(json['data']); return CommunityModel.fromJson(json['data']);
}, },
@@ -95,7 +110,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<CommunityModel?> createCommunity(String name, String description, String projectId) async { Future<CommunityModel?> createCommunity(
String name, String description, String projectId) async {
try { try {
final response = await HTTPService().post( final response = await HTTPService().post(
path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId), path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId),
@@ -114,7 +130,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<bool> updateCommunity(String communityId, String name, String projectId) async { Future<bool> updateCommunity(
String communityId, String name, String projectId) async {
try { try {
final response = await HTTPService().put( final response = await HTTPService().put(
path: ApiEndpoints.updateCommunity path: ApiEndpoints.updateCommunity
@@ -151,7 +168,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<SpacesResponse> fetchSpaces(String communityId, String projectId) async { Future<SpacesResponse> fetchSpaces(
String communityId, String projectId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.listSpaces path: ApiEndpoints.listSpaces
@@ -177,7 +195,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<SpaceModel?> getSpace(String communityId, String spaceId, String projectId) async { Future<SpaceModel?> getSpace(
String communityId, String spaceId, String projectId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getSpace path: ApiEndpoints.getSpace
@@ -199,7 +218,6 @@ class CommunitySpaceManagementApi {
{required String communityId, {required String communityId,
required String name, required String name,
String? parentId, String? parentId,
String? direction,
bool isPrivate = false, bool isPrivate = false,
required Offset position, required Offset position,
String? spaceModelUuid, String? spaceModelUuid,
@@ -213,7 +231,6 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate, 'isPrivate': isPrivate,
'x': position.dx, 'x': position.dx,
'y': position.dy, 'y': position.dy,
'direction': direction,
'icon': icon, 'icon': icon,
}; };
if (parentId != null) { if (parentId != null) {
@@ -248,11 +265,10 @@ class CommunitySpaceManagementApi {
required String name, required String name,
String? parentId, String? parentId,
String? icon, String? icon,
String? direction,
bool isPrivate = false, bool isPrivate = false,
required Offset position, required Offset position,
List<TagModelUpdate>? tags, List<Tag>? tags,
List<UpdateSubspaceTemplateModel>? subspaces, List<SubspaceModel>? subspaces,
String? spaceModelUuid, String? spaceModelUuid,
required String projectId}) async { required String projectId}) async {
try { try {
@@ -261,9 +277,8 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate, 'isPrivate': isPrivate,
'x': position.dx, 'x': position.dx,
'y': position.dy, 'y': position.dy,
'direction': direction,
'icon': icon, 'icon': icon,
'subspace': subspaces, 'subspaces': subspaces,
'tags': tags, 'tags': tags,
'spaceModelUuid': spaceModelUuid, 'spaceModelUuid': spaceModelUuid,
}; };
@@ -289,7 +304,8 @@ class CommunitySpaceManagementApi {
} }
} }
Future<bool> deleteSpace(String communityId, String spaceId, String projectId) async { Future<bool> deleteSpace(
String communityId, String spaceId, String projectId) async {
try { try {
final response = await HTTPService().delete( final response = await HTTPService().delete(
path: ApiEndpoints.deleteSpace path: ApiEndpoints.deleteSpace
@@ -307,15 +323,17 @@ class CommunitySpaceManagementApi {
} }
} }
Future<List<SpaceModel>> getSpaceHierarchy(String communityId, String projectId) async { Future<List<SpaceModel>> getSpaceHierarchy(
String communityId, String projectId) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getSpaceHierarchy path: ApiEndpoints.getSpaceHierarchy
.replaceAll('{communityId}', communityId) .replaceAll('{communityId}', communityId)
.replaceAll('{projectId}', projectId), .replaceAll('{projectId}', projectId),
expectedResponseModel: (json) { expectedResponseModel: (json) {
final spaceModels = final spaceModels = (json['data'] as List)
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); .map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
return spaceModels; return spaceModels;
}, },
@@ -327,15 +345,17 @@ class CommunitySpaceManagementApi {
} }
} }
Future<List<SpaceModel>> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async { Future<List<SpaceModel>> getSpaceOnlyWithDevices(
{String? communityId, String? projectId}) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.spaceOnlyWithDevices path: ApiEndpoints.spaceOnlyWithDevices
.replaceAll('{communityId}', communityId!) .replaceAll('{communityId}', communityId!)
.replaceAll('{projectId}', projectId!), .replaceAll('{projectId}', projectId!),
expectedResponseModel: (json) { expectedResponseModel: (json) {
final spaceModels = final spaceModels = (json['data'] as List)
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); .map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
return spaceModels; return spaceModels;
}, },
); );
+23 -14
View File
@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; 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/create_space_template_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
@@ -7,17 +9,23 @@ import 'package:syncrow_web/utils/constants/api_const.dart';
class SpaceModelManagementApi { class SpaceModelManagementApi {
Future<List<SpaceTemplateModel>> listSpaceModels( Future<List<SpaceTemplateModel>> listSpaceModels(
{required String projectId, int page = 1}) async { {required String projectId, int page = 1}) async {
final response = await HTTPService().get( try {
path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId), // final response = await HTTPService().get(
queryParameters: {'page': page}, // path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId),
expectedResponseModel: (json) { // queryParameters: {'page': page},
List<dynamic> jsonData = json['data']; // expectedResponseModel: (json) {
return jsonData.map((jsonItem) { // List<dynamic> jsonData = json['data'];
return SpaceTemplateModel.fromJson(jsonItem); // return jsonData.map((jsonItem) {
}).toList(); // return SpaceTemplateModel.fromJson(jsonItem);
}, // }).toList();
); // },
return response; // );
return [];
// response;
} catch (e) {
log(e.toString());
return [];
}
} }
Future<SpaceTemplateModel?> createSpaceModel( Future<SpaceTemplateModel?> createSpaceModel(
@@ -33,8 +41,8 @@ class SpaceModelManagementApi {
return response; return response;
} }
Future<String?> updateSpaceModel( Future<String?> updateSpaceModel(CreateSpaceTemplateBodyModel spaceModel,
CreateSpaceTemplateBodyModel spaceModel, String spaceModelUuid, String projectId) async { String spaceModelUuid, String projectId) async {
final response = await HTTPService().put( final response = await HTTPService().put(
path: ApiEndpoints.updateSpaceModel path: ApiEndpoints.updateSpaceModel
.replaceAll('{projectId}', projectId) .replaceAll('{projectId}', projectId)
@@ -47,7 +55,8 @@ class SpaceModelManagementApi {
return response; return response;
} }
Future<SpaceTemplateModel?> getSpaceModel(String spaceModelUuid, String projectId) async { Future<SpaceTemplateModel?> getSpaceModel(
String spaceModelUuid, String projectId) async {
final response = await HTTPService().get( final response = await HTTPService().get(
path: ApiEndpoints.getSpaceModel path: ApiEndpoints.getSpaceModel
.replaceAll('{projectId}', projectId) .replaceAll('{projectId}', projectId)
+1
View File
@@ -2,6 +2,7 @@ class Assets {
Assets._(); Assets._();
static const String background = "assets/images/Background.png"; static const String background = "assets/images/Background.png";
static const String webBackground = "assets/images/web_Background.svg"; static const String webBackground = "assets/images/web_Background.svg";
static const String webBackgroundPng = "assets/images/web_Background.png";
static const String blackLogo = "assets/images/black-logo.png"; static const String blackLogo = "assets/images/black-logo.png";
static const String logo = "assets/images/Logo.svg"; static const String logo = "assets/images/Logo.svg";
static const String logoHorizontal = "assets/images/logo_horizontal.png"; static const String logoHorizontal = "assets/images/logo_horizontal.png";

Some files were not shown because too many files have changed in this diff Show More