Compare commits

..

63 Commits

Author SHA1 Message Date
aa3b79bdaf stop fetching tags onBlank Event 2025-06-10 16:34:49 +03:00
0e31a3ea96 no need to fetch use info in init state of homepage and agreement dialog 2025-06-10 16:33:33 +03:00
fd192894cd stop calling initEvent for spactree bloc inside homeBloc 2025-06-10 16:32:33 +03:00
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
08a9a5c71f comment the tab of spaceModel to prevent routing there 2025-06-10 15:08:50 +03:00
7eb1d5b0b0 comment listSpace func which calls SpaceModels that suspended for now 2025-06-10 15:07:32 +03:00
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
e22bab00d9 just format code in assign tag bloc 2025-06-10 10:27:08 +03:00
d2a2d391e0 send subspaces with onSave in okCancel inside subspace dialog as parameters 2025-06-10 10:19:48 +03:00
1d30c753f5 edit tag model keys to integrate with backend 2025-06-10 10:08:49 +03:00
ca02de2093 edit subspace model to fix keys and integrate with backend 2025-06-10 10:07:26 +03:00
8f7bfa984b edit spacemodel to use the right keys to integrate with backend 2025-06-10 10:06:36 +03:00
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
15d3a05553 assign tag dropDown now show all Tags without condition 2025-06-10 10:03:32 +03:00
a1d7457065 Test commit with updated user 2025-05-29 14:34:15 +03:00
c99b32fb81 cancel direction 2025-05-28 06:50:04 -05:00
321df401fd comment interceptor cuz it is crashing app 2025-05-28 01:56:57 -05:00
ee244fa5ed use textfield controller from constructor 2025-05-28 01:48:37 -05:00
1db069e9a5 use devicePart in main widget 2025-05-28 01:21:06 -05:00
cf9bafef4d add devices part widget 2025-05-28 01:20:31 -05:00
2c73dd6c31 call the new widgets 2025-05-28 00:42:05 -05:00
6ec20e2d72 seperate subspace part into widget 2025-05-28 00:41:53 -05:00
4feae9ad87 seperate spacemodel linking into widget 2025-05-28 00:41:33 -05:00
52046909d5 seperate textfield of the space name 2025-05-28 00:41:01 -05:00
fc81555be3 seperate iconchoose widget in create space dialog 2025-05-28 00:40:34 -05:00
8967852ca8 seperate assign tag dialog to widgets 2025-05-27 06:01:25 -05:00
a87e79878b add ok_candel row of buttons in seperated widget also text field widget 2025-05-27 05:32:42 -05:00
056e7372e0 use assign table as widget 2025-05-27 01:15:30 -05:00
d69d867120 use assign_tag_dialog_widgetwidget 2025-05-26 06:27:48 -05:00
644fe56478 seperaate UI into widgets 2025-05-26 06:12:24 -05:00
766a39f161 use selected model in add device event 2025-05-26 06:11:48 -05:00
c97dd40b05 add toSelected function to convert from product to selectedProduct 2025-05-26 06:11:06 -05:00
0b65c58947 fix add subspace bugs and plusButton widget 2025-05-26 02:45:58 -05:00
e0951aa13d add interceptor to print request and response 2025-05-23 08:52:53 -05:00
9e8ebf3768 no need for models and blocs for now 2025-05-23 07:52:21 -05:00
b593e75c67 build main structure and build data layer and space managment bloc 2025-05-23 02:17:23 -05:00
9eaa367d32 fix horizontal scroll bar 2025-05-22 05:48:49 -05:00
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
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
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
94e4fbd5db Apply correct business logic in OccupancyDataLoadingStrategy. 2025-05-21 16:08:48 +03:00
302ef36b17 Merge branch 'dev' of https://github.com/SyncrowIOT/web into syncrow_analytics_sidebar_selection_behavior 2025-05-21 15:56:29 +03:00
c508d016c2 SP-1580-FE-Watermark-Does-Not-Match-Design-Specification 2025-05-21 11:08:00 +03:00
e0ad7855d3 converted GetOccupancyParam.toJson to an expression method. 2025-05-21 10:59:04 +03:00
ecf588cfcb reverted to dynamic endpoint. 2025-05-21 10:58:21 +03:00
c9d15d102b fixes in OccupancyChart for a more pleasant UI. 2025-05-21 10:57:53 +03:00
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
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
0a94557eee SP-1510-Occupancy Chart API Integration. 2025-05-21 10:23:31 +03:00
4f8d1c4ffd Merge pull request #195 from SyncrowIOT/charts-reworks
Charts reworks
2025-05-21 10:22:55 +03:00
000fe70663 format. 2025-05-21 09:59:50 +03:00
4257f7f0f3 Corrected color of titles in charts. 2025-05-21 09:55:17 +03:00
b2bf3866a9 Deleted pubspec.lock, and added it to .gitignore. 2025-05-21 09:09:32 +03:00
fd2a09cada Deleted unused FakeEnergyConsumptionPerDeviceService. 2025-05-20 14:22:23 +03:00
4c2802acfc date picker decorations matched with design. 2025-05-20 14:20:16 +03:00
15343be258 show space uuid in analytics devices dropdown. 2025-05-20 14:11:25 +03:00
c21842cc6d removed overflow and fixed sizing and text drawing of PowerClampEnergyStatusWidget. 2025-05-20 13:56:00 +03:00
4326559e14 shows OccupancyHeatMapBox instead of a Placeholder in vertical srcollable AnalyticsOccupancyView. 2025-05-20 13:51:04 +03:00
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
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
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
baaf5111b1 Applied correct business logic in EnergyManagementDataLoadingStrategy. 2025-05-15 12:48:18 +03:00
745205063e added correct behavior to OccupancyDataLoadingStrategy. 2025-05-15 12:46:12 +03:00
73 changed files with 2319 additions and 2413 deletions

1
.gitignore vendored
View File

@ -30,6 +30,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
pubspec.lock
# Symbolication related
app.*.symbols

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -17,7 +17,8 @@ class TagDialogTextfieldDropdown extends StatefulWidget {
}) : super(key: key);
@override
_DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState();
_DialogTextfieldDropdownState createState() =>
_DialogTextfieldDropdownState();
}
class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
@ -36,6 +37,12 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
// Call onSelected when focus is lost
final selectedTag = _filteredItems.firstWhere(
(tag) => tag.tag == _controller.text,
orElse: () => Tag(tag: _controller.text),
);
widget.onSelected(selectedTag);
_closeDropdown();
}
});
@ -43,7 +50,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
void _filterItems() {
setState(() {
_filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList();
_filteredItems = widget.items;
// .where((tag) => tag.product?.uuid == widget.product)
// .toList();
});
}
@ -112,7 +121,9 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: ColorsManager.textPrimaryColor)),
?.copyWith(
color: ColorsManager
.textPrimaryColor)),
onTap: () {
_controller.text = tag.tag ?? '';
widget.onSelected(tag);
@ -156,13 +167,15 @@ class _DialogTextfieldDropdownState extends State<TagDialogTextfieldDropdown> {
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: (value) {
final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value,
final selectedTag = _filteredItems.firstWhere(
(tag) => tag.tag == value,
orElse: () => Tag(tag: value));
widget.onSelected(selectedTag);
_closeDropdown();
},
onTapOutside: (event) {
widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text,
widget.onSelected(_filteredItems.firstWhere(
(tag) => tag.tag == _controller.text,
orElse: () => Tag(tag: _controller.text)));
_closeDropdown();
},

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
lib/core/network/dio.dart Normal file
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;
}
}

View File

@ -0,0 +1,3 @@
class EndPoints {
static const String fetchCommunities = 'projects/{projectUuid}/communities';
}

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,
}

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",
),
};

View File

@ -1,13 +1,71 @@
class AnalyticsDevice {
const AnalyticsDevice({required this.name, required this.uuid});
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? ?? '',
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?,
);
}
}

View File

@ -1,18 +1,32 @@
import 'package:equatable/equatable.dart';
class Occupacy extends Equatable {
final String date;
final DateTime date;
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) {
return Occupacy(
date: json['date'] as String,
occupancy: json['occupancy'] as String,
date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'),
occupancy: (json['occupancy_percentage'] ?? 0).toString(),
spaceUuid: json['space_uuid'] as String? ?? '',
occupiedSeconds: json['occupied_seconds'] as int? ?? 0,
);
}
@override
List<Object?> get props => [date, occupancy];
List<Object?> get props => [
date,
occupancy,
spaceUuid,
occupiedSeconds,
];
}

View File

@ -14,7 +14,6 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
List<SpaceModel> spaces,
) {
// Add to space tree bloc first
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
@ -69,7 +68,9 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel child,
) {
// Do nothing else as per original implementation
if (child.children.isNotEmpty) {
return onSpaceSelected(context, community, child);
}
}
@override

View File

@ -14,23 +14,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
List<SpaceModel> spaces,
) {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces.isNotEmpty ? [spaces.first] : [],
),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
clearData(context);
return;
}
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
// Do Nothing
}
@override
@ -40,26 +24,17 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
SpaceModel space,
) {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces;
final isSpaceSelected = selectedSpacesIds.contains(space.uuid);
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (selectedSpacesIds.isEmpty) {
spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space]));
} else if (isSpaceSelected) {
spaceTreeBloc.add(const SpaceTreeClearSelectionEvent());
} else {
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
}
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
if (isSpaceSelected) {
clearData(context);
return;
}
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
@ -73,7 +48,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel child,
) {
// Do nothing
onSpaceSelected(context, community, child);
}
@override

View File

@ -17,7 +17,7 @@ import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_en
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_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/fake_occupacy_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/realtime_device_service/firebase_realtime_device_service.dart';
@ -75,7 +75,11 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FirebaseRealtimeDeviceService(),
),
),
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
BlocProvider(
create: (context) => OccupancyBloc(
RemoteOccupancyService(_httpService),
),
),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(
RemoteOccupancyHeatMapService(_httpService),

View File

@ -74,6 +74,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??

View File

@ -178,7 +178,6 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
shrinkWrap: true,
itemCount: 12,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
@ -191,6 +190,17 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
return InkWell(
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(
alignment: Alignment.center,
decoration: BoxDecoration(
@ -215,6 +225,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
),
),
),
),
);
},
);

View File

@ -109,7 +109,6 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
shrinkWrap: true,
itemCount: years.length,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
@ -120,6 +119,17 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
final isSelected = _currentYear == years[index];
return InkWell(
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(
alignment: Alignment.center,
decoration: BoxDecoration(
@ -140,6 +150,7 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
),
),
),
),
);
},
);

View File

@ -20,14 +20,14 @@ abstract final class EnergyManagementChartsHelper {
interval: 1,
reservedSize: 32,
showTitles: true,
maxIncluded: false,
minIncluded: false,
maxIncluded: true,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
(value + 1).toString(),
value.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
fontSize: 12,
),
),
@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper {
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: false,
minIncluded: true,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
@ -72,7 +72,7 @@ abstract final class EnergyManagementChartsHelper {
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
getToolTipLabel(spot.x + 1, spot.y),
getToolTipLabel(spot.x, spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
@ -93,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() {
return const FlGridData(
return FlGridData(
show: true,
drawVerticalLine: false,
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() {
return LineTouchData(
handleBuiltInTouches: true,
touchSpotThreshold: 2,
touchSpotThreshold: 16,
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
);
}

View File

@ -26,6 +26,7 @@ abstract final class FetchEnergyManagementDataHelper {
required String communityId,
required String spaceId,
DateTime? selectedDate,
bool shouldFetchAnalyticsDevices = true,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
@ -34,12 +35,16 @@ abstract final class FetchEnergyManagementDataHelper {
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityId,
spaceUuid: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
loadTotalEnergyConsumption(
context,
selectedDate: selectedDate0,
@ -60,8 +65,6 @@ abstract final class FetchEnergyManagementDataHelper {
spaceId: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
static void loadEnergyConsumptionByPhases(

View File

@ -48,6 +48,7 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
}
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
final spaceUuid = state.selectedDevice?.spaceUuid;
return DropdownButton<AnalyticsDevice?>(
value: state.selectedDevice,
isDense: true,
@ -60,10 +61,30 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
),
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: Text(e.name),
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) {

View File

@ -56,7 +56,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
ColorsManager.vividBlue.withValues(alpha: 0.15),
),
],
width: 16,
width: 8,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
@ -66,6 +66,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
);
}).toList(),
),
duration: Duration.zero,
);
}
@ -160,9 +161,9 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final month = DateFormat('dd/MM').format(energyData[value.toInt()].date);
final month = DateFormat('d').format(energyData[value.toInt()].date);
return FittedBox(
alignment: AlignmentDirectional.bottomCenter,
alignment: AlignmentDirectional.center,
fit: BoxFit.scaleDown,
child: RotatedBox(
quarterTurns: 3,
@ -176,7 +177,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
),
);
},
reservedSize: 36,
reservedSize: 18,
),
);

View File

@ -16,6 +16,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
@ -36,7 +37,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
);
}).toList(),
),
duration: Durations.extralong1,
duration: Duration.zero,
curve: Curves.easeIn,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/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/energy_consumption_per_device_chart.dart';
@ -46,6 +47,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
flex: 2,
child: EnergyConsumptionPerDeviceDevicesList(
chartData: state.chartData,
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
),
),
],

View File

@ -1,10 +1,16 @@
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/utils/color_manager.dart';
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;
@override
@ -16,13 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.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) {
return Container(
Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
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,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
@ -43,10 +63,10 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
children: [
CircleAvatar(
radius: 4,
backgroundColor: device.color,
backgroundColor: deviceColor,
),
Text(
device.deviceName,
device.name,
textAlign: TextAlign.center,
style: const TextStyle(
color: ColorsManager.blackColor,
@ -57,6 +77,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
],
),
),
),
);
}
}

View File

@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
fontWeight: FontWeight.w400,
fontSize: 16,
),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text.rich(
TextSpan(

View File

@ -26,7 +26,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: _lineBarsData,
),
duration: Durations.extralong1,
duration: Duration.zero,
curve: Curves.easeIn,
),
);
@ -43,7 +43,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
.entries
.map(
(entry) => FlSpot(
entry.key.toDouble(),
entry.value.date.day.toDouble(),
entry.value.value,
),
)

View File

@ -30,7 +30,6 @@ abstract final class FetchOccupancyDataHelper {
loadOccupancyChartData(
context,
communityUuid: communityId,
spaceUuid: spaceId,
date: datePickerState.monthlyDate,
);
@ -59,16 +58,14 @@ abstract final class FetchOccupancyDataHelper {
static void loadOccupancyChartData(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
required DateTime date,
}) {
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate: '${date.year}-${date.month}',
monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}',
spaceUuid: spaceUuid,
communityUuid: communityUuid,
),
),
);

View File

@ -20,9 +20,9 @@ class AnalyticsOccupancyView extends StatelessWidget {
child: Column(
spacing: 32,
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 Placeholder()),
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
],
),
);

View File

@ -16,10 +16,10 @@ class OccupancyChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
maxY: 1.0,
maxY: 100.0,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 0.25,
horizontalInterval: 20,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
@ -33,20 +33,21 @@ class OccupancyChart extends StatelessWidget {
),
barGroups: List.generate(chartData.length, (index) {
final actual = chartData[index];
final occupancyValue = double.parse(actual.occupancy);
return BarChartGroupData(
x: index,
barsSpace: 0,
groupVertically: true,
barRods: [
BarChartRodData(
toY: 1.0,
fromY: double.parse(actual.occupancy) + 0.025,
toY: 100.0,
fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5,
color: ColorsManager.graysColor,
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
BarChartRodData(
toY: double.parse(actual.occupancy),
toY: occupancyValue,
color: ColorsManager.vividBlue.withValues(alpha: 0.8),
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
@ -88,7 +89,7 @@ class OccupancyChart extends StatelessWidget {
final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue * 100).toStringAsFixed(0)}%';
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
@ -108,14 +109,14 @@ class OccupancyChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 0.25,
interval: 20,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
'${(value).toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,

View File

@ -50,9 +50,6 @@ class OccupancyChartBox extends StatelessWidget {
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadOccupancyChartData(
context,
communityUuid:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceUuid:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
date: value,

View File

@ -1,19 +1,11 @@
class GetOccupancyParam {
final String monthDate;
final String? spaceUuid;
final String communityUuid;
GetOccupancyParam({
required this.monthDate,
required this.spaceUuid,
required this.communityUuid,
});
Map<String, dynamic> toJson() {
return {
'monthDate': monthDate,
'spaceUuid': spaceUuid,
'communityUuid': communityUuid,
};
}
Map<String, dynamic> toJson() => {'monthDate': monthDate};
}

View File

@ -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();
});
}
}

View File

@ -34,10 +34,7 @@ abstract final class _EnergyConsumptionPerDeviceMapper {
static List<DeviceEnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.where((e) {
final deviceData = (e as Map<String, dynamic>)['data'] as List<dynamic>? ?? [];
return deviceData.isNotEmpty;
}).map((e) {
return mappedData.map((e) {
final deviceData = e as Map<String, dynamic>;
final energyData = deviceData['data'] as List<dynamic>;

View File

@ -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(),
),
),
);
}
}

View File

@ -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');
}
}
}

View File

@ -112,8 +112,8 @@ class _DynamicTableState extends State<DynamicTable> {
trackVisibility: true,
child: Scrollbar(
controller: _horizontalScrollController,
thumbVisibility: false,
trackVisibility: false,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController,

View File

@ -51,12 +51,13 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
try {
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
user = await HomeApi().fetchUserInfo(uuid);
if (user != null && user!.project != null) {
await ProjectManager.setProjectUUID(user!.project!.uuid);
NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
// NavigationService.navigatorKey.currentContext!.read<SpaceTreeBloc>().add(InitialEvent());
}
add(FetchTermEvent());
add(FetchPolicyEvent());
@ -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 {
emit(LoadingHome());
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
var uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
policy = await HomeApi().confirmUserAgreements(uuid);
emit(PolicyAgreement());
} catch (e) {

View File

@ -24,7 +24,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
void initState() {
super.initState();
final homeBloc = BlocProvider.of<HomeBloc>(context);
homeBloc.add(const FetchUserInfo());
// homeBloc.add(const FetchUserInfo());
}
@override
@ -38,8 +38,10 @@ class _HomeWebPageState extends State<HomeWebPage> {
child: BlocConsumer<HomeBloc, HomeState>(
listener: (BuildContext context, state) {
if (state is HomeInitial) {
if (homeBloc.user!.hasAcceptedWebAgreement == false && !_dialogShown) {
_dialogShown = true; // Set the flag to true to indicate the dialog is showing.
if (homeBloc.user!.hasAcceptedWebAgreement == false &&
!_dialogShown) {
_dialogShown =
true; // Set the flag to true to indicate the dialog is showing.
Future.delayed(const Duration(seconds: 1), () {
showDialog(
context: context,
@ -54,7 +56,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
_dialogShown = false;
if (v != null) {
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,
child: GridView.builder(
itemCount: homeBloc.homeItems.length,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // Adjust as needed.
crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0,
@ -110,7 +113,8 @@ class _HomeWebPageState extends State<HomeWebPage> {
active: homeBloc.homeItems[index].active!,
name: homeBloc.homeItems[index].title!,
img: homeBloc.homeItems[index].icon!,
onTap: () => homeBloc.homeItems[index].onPress(context),
onTap: () =>
homeBloc.homeItems[index].onPress(context),
);
},
),

View File

@ -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/utils/constants/action_enum.dart' as custom_action;
class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementState> {
class SpaceManagementBloc
extends Bloc<SpaceManagementEvent, SpaceManagementState> {
final CommunitySpaceManagementApi _api;
final ProductApi _productApi;
final SpaceModelManagementApi _spaceModelApi;
@ -62,7 +63,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
int page = 1;
while (hasNext) {
final spaceModels = await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid);
final spaceModels = await _spaceModelApi.listSpaceModels(
page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels);
page++;
@ -75,26 +77,29 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
await fetchTags();
emit(SpaceModelLoaded(
communities:
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [],
communities: state is SpaceManagementLoaded
? (state as SpaceManagementLoaded).communities
: [],
products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? []));
}
void _deleteSpaceModelFromCache(
DeleteSpaceModelFromCache event, Emitter<SpaceManagementState> emit) async {
void _deleteSpaceModelFromCache(DeleteSpaceModelFromCache event,
Emitter<SpaceManagementState> emit) async {
if (_cachedSpaceModels != null) {
_cachedSpaceModels =
_cachedSpaceModels!.where((model) => model.uuid != event.deletedUuid).toList();
_cachedSpaceModels = _cachedSpaceModels!
.where((model) => model.uuid != event.deletedUuid)
.toList();
} else {
_cachedSpaceModels = await fetchSpaceModels();
}
await fetchTags();
emit(SpaceModelLoaded(
communities:
state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [],
communities: state is SpaceManagementLoaded
? (state as SpaceManagementLoaded).communities
: [],
products: _cachedProducts ?? [],
spaceModels: List.from(_cachedSpaceModels ?? []),
allTags: _cachedTags ?? []));
@ -122,8 +127,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
int page = 1;
while (hasNext) {
final spaceModels =
await _spaceModelApi.listSpaceModels(page: page, projectId: projectUuid);
final spaceModels = await _spaceModelApi.listSpaceModels(
page: page, projectId: projectUuid);
if (spaceModels.isNotEmpty) {
allSpaceModels.addAll(spaceModels);
page++;
@ -164,10 +169,12 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
await fetchTags();
emit(SpaceManagementLoading());
final success = await _api.updateCommunity(event.communityUuid, event.name, projectUuid);
final success = await _api.updateCommunity(
event.communityUuid, event.name, projectUuid);
if (success) {
if (previousState is SpaceManagementLoaded) {
final updatedCommunities = List<CommunityModel>.from(previousState.communities);
final updatedCommunities =
List<CommunityModel>.from(previousState.communities);
for (var community in updatedCommunities) {
if (community.uuid == event.communityUuid) {
community.name = event.name;
@ -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() ?? '';
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 {
final previousState = state;
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
var spaceBloc = event.context.read<SpaceTreeBloc>();
var spaceTreeState = event.context.read<SpaceTreeBloc>().state;
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
List<CommunityModel> communities =
await _waitForCommunityList(spaceBloc, spaceTreeState);
await fetchSpaceModels();
await fetchTags();
// await fetchTags();
var prevSpaceModels = await fetchSpaceModels();
if (previousState is SpaceManagementLoaded || previousState is BlankState) {
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final prevCommunities = (previousState as dynamic).communities ?? [];
emit(BlankState(
communities: List<CommunityModel>.from(prevCommunities),
@ -286,7 +297,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
_onloadProducts();
await fetchTags();
// Wait until `communityList` is loaded
List<CommunityModel> communities = await _waitForCommunityList(spaceBloc, spaceTreeState);
List<CommunityModel> communities =
await _waitForCommunityList(spaceBloc, spaceTreeState);
// Fetch space models after communities are available
final prevSpaceModels = await fetchSpaceModels();
@ -310,8 +322,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
final completer = Completer<List<CommunityModel>>();
final subscription = spaceBloc.stream.listen((state) {
if (!completer.isCompleted && state.communityList.isNotEmpty) {
completer
.complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList);
completer.complete(state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList);
}
});
try {
@ -339,7 +352,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
emit(SpaceManagementLoading());
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final success = await _api.deleteCommunity(event.communityUuid, projectUuid);
final success =
await _api.deleteCommunity(event.communityUuid, projectUuid);
if (success) {
// add(LoadCommunityAndSpacesEvent());
} else {
@ -361,12 +375,13 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
await fetchTags();
CommunityModel? newCommunity =
await _api.createCommunity(event.name, event.description, projectUuid);
CommunityModel? newCommunity = await _api.createCommunity(
event.name, event.description, projectUuid);
var prevSpaceModels = await fetchSpaceModels();
if (newCommunity != null) {
if (previousState is SpaceManagementLoaded || previousState is BlankState) {
if (previousState is SpaceManagementLoaded ||
previousState is BlankState) {
final prevCommunities = List<CommunityModel>.from(
(previousState as dynamic).communities,
);
@ -459,12 +474,15 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
try {
final spaceTreeState = event.context.read<SpaceTreeBloc>().state;
final updatedSpaces =
await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid);
final updatedSpaces = await saveSpacesHierarchically(
event.context, event.spaces, event.communityUuid);
final allSpaces = await _fetchSpacesForCommunity(event.communityUuid);
emit(SpaceCreationSuccess(spaces: updatedSpaces));
// emit(SpaceCreationSuccess(spaces: updatedSpaces));
// updatedSpaces.forEach(
// (element) => element.uuid,
// );
// final lastUpdatedSpaced = updatedSpaces..addAll(allSpaces);
if (previousState is SpaceManagementLoaded) {
await _updateLoadedState(
spaceTreeState,
@ -475,7 +493,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
);
}
} catch (e) {
emit(SpaceManagementError('Error saving spaces: $e'));
// emit(SpaceManagementError('Error saving spaces: $e'));
if (previousState is SpaceManagementLoaded) {
emit(previousState);
@ -515,13 +533,15 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return;
}
}
emit(previousState);
} catch (e, stackTrace) {
rethrow;
emit(previousState);
// rethrow;
}
}
Future<List<SpaceModel>> saveSpacesHierarchically(
BuildContext context, List<SpaceModel> spaces, String communityUuid) async {
Future<List<SpaceModel>> saveSpacesHierarchically(BuildContext context,
List<SpaceModel> spaces, String communityUuid) async {
final orderedSpaces = flattenHierarchy(spaces);
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@ -534,6 +554,14 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
selectedCommunity = filteredCommunities.firstWhere(
(community) => community.uuid == communityUuid,
orElse: () => CommunityModel(
uuid: '',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
name: '',
description: '',
spaces: spaces,
),
);
} catch (e) {
return [];
@ -548,9 +576,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (parent.uuid != null) {
await _api.deleteSpace(communityUuid, parent.uuid!, projectUuid);
}
} catch (e) {
rethrow;
}
} catch (e) {}
}
orderedSpaces.removeWhere((space) => parentsToDelete.contains(space));
@ -564,7 +590,7 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
if (matchedSpaces.isEmpty) continue;
final prevSpace = matchedSpaces[0];
final prevSpace = matchedSpaces.elementAtOrNull(0);
final List<UpdateSubspaceTemplateModel> subspaceUpdates = [];
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) {
for (var prevSubspace in prevSubspaces) {
final existsInNew =
newSubspaces.any((subspace) => subspace.uuid == prevSubspace.uuid);
final existsInNew = newSubspaces
.any((subspace) => subspace.uuid == prevSubspace.uuid);
if (!existsInNew) {
subspaceUpdates.add(UpdateSubspaceTemplateModel(
action: custom_action.Action.delete, uuid: prevSubspace.uuid));
action: custom_action.Action.delete,
uuid: prevSubspace.uuid));
}
}
} else if (prevSubspaces != null && newSubspaces == null) {
for (var prevSubspace in prevSubspaces) {
subspaceUpdates.add(UpdateSubspaceTemplateModel(
action: custom_action.Action.delete, uuid: prevSubspace.uuid));
action: custom_action.Action.delete,
uuid: prevSubspace.uuid));
}
}
@ -613,7 +641,9 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
}
if (prevSubspaces != null && newSubspaces != null) {
final newSubspaceMap = {for (var subspace in newSubspaces) subspace.uuid: subspace};
final newSubspaceMap = {
for (var subspace in newSubspaces) subspace.uuid: subspace
};
for (var prevSubspace in prevSubspaces) {
final newSubspace = newSubspaceMap[prevSubspace.uuid];
@ -639,9 +669,10 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
isPrivate: space.isPrivate,
position: space.position,
icon: space.icon,
subspaces: subspaceUpdates,
tags: tagUpdates,
direction: space.incomingConnection?.direction,
subspaces: space.subspaces,
// subspaceUpdates,
tags: space.tags,
// tagUpdates,
spaceModelUuid: space.spaceModel?.uuid,
projectId: projectUuid);
} else {
@ -651,8 +682,10 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
: [];
var createSubspaceBodyModels = space.subspaces?.map((subspace) {
final tagBodyModels =
subspace.tags?.map((tag) => tag.toCreateTagBodyModel()).toList() ?? [];
final tagBodyModels = subspace.tags
?.map((tag) => tag.toCreateTagBodyModel())
.toList() ??
[];
return CreateSubspaceModel()
..subspaceName = subspace.subspaceName
..tags = tagBodyModels;
@ -671,7 +704,6 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
isPrivate: space.isPrivate,
position: space.position,
icon: space.icon,
direction: space.incomingConnection?.direction,
spaceModelUuid: space.spaceModel?.uuid,
tags: tagBodyModels,
subspaces: createSubspaceBodyModels,
@ -679,7 +711,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
space.uuid = response?.uuid;
}
} catch (e) {
rethrow; // Stop further execution on failure
return [];
// Stop further execution on failure
}
}
return spaces;
@ -710,7 +743,8 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return result.toList(); // Convert back to a list
}
void _onLoadSpaceModel(SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
void _onLoadSpaceModel(
SpaceModelLoadEvent event, Emitter<SpaceManagementState> emit) async {
emit(SpaceManagementLoading());
try {
@ -757,14 +791,17 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
// Case 1: Tags deleted
if (prevTags != null && newTags != null) {
for (var prevTag in prevTags) {
final existsInNew = newTags.any((newTag) => newTag.uuid == prevTag.uuid);
final existsInNew =
newTags.any((newTag) => newTag.uuid == prevTag.uuid);
if (!existsInNew) {
tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid));
tagUpdates.add(TagModelUpdate(
action: custom_action.Action.delete, uuid: prevTag.uuid));
}
}
} else if (prevTags != null && newTags == null) {
for (var prevTag in prevTags) {
tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid));
tagUpdates.add(TagModelUpdate(
action: custom_action.Action.delete, uuid: prevTag.uuid));
}
}
@ -807,15 +844,16 @@ class SpaceManagementBloc extends Bloc<SpaceManagementEvent, SpaceManagementStat
return tagUpdates;
}
List<SpaceModel> findMatchingSpaces(List<SpaceModel> spaces, String targetUuid) {
List<SpaceModel> findMatchingSpaces(
List<SpaceModel> spaces, String targetUuid) {
List<SpaceModel> matched = [];
for (var space in spaces) {
if (space.uuid == targetUuid) {
matched.add(space);
}
matched
.addAll(findMatchingSpaces(space.children, targetUuid)); // Recursively search in children
matched.addAll(findMatchingSpaces(
space.children, targetUuid)); // Recursively search in children
}
return matched;

View File

@ -3,23 +3,26 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model
class Connection {
final SpaceModel startSpace;
final SpaceModel endSpace;
final String direction;
Connection({required this.startSpace, required this.endSpace, required this.direction});
Connection({
required this.startSpace,
required this.endSpace,
});
Map<String, dynamic> toMap() {
return {
'startUuid': startSpace.uuid ?? 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces
'endUuid': endSpace.uuid ?? 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces
'direction': direction,
'startUuid': startSpace.uuid ??
'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces
'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(
startSpace: spaces[map['startUuid']]!,
endSpace: spaces[map['endUuid']]!,
direction: map['direction'],
);
}
}

View File

@ -1,5 +1,7 @@
import 'package:syncrow_web/utils/constants/assets.dart';
import 'selected_product_model.dart';
class ProductModel {
final String uuid;
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) {
const iconMapping = {
'1G': Assets.Gang1SwitchIcon,

View File

@ -101,7 +101,7 @@ class SpaceModel {
spaceModel: json['spaceModel'] != null
? SpaceTemplateModel.fromJson(json['spaceModel'])
: null,
tags: (json['tags'] as List<dynamic>?)
tags: (json['productAllocations'] as List<dynamic>?)
?.where((item) => item is Map<String, dynamic>) // Validate type
.map((item) => Tag.fromJson(item as Map<String, dynamic>))
.toList() ??
@ -116,7 +116,6 @@ class SpaceModel {
instance.incomingConnection = Connection(
startSpace: instance.parent ?? instance, // Parent space
endSpace: instance, // This space instance
direction: conn['direction'],
);
}

View File

@ -27,7 +27,7 @@ class SubspaceModel {
subspaceName: json['subspaceName'] ?? '',
disabled: json['disabled'] ?? false,
internalId: internalId,
tags: (json['tags'] as List<dynamic>?)
tags: (json['productAllocations'] as List<dynamic>?)
?.map((item) => Tag.fromJson(item))
.toList() ??
[],
@ -36,7 +36,7 @@ class SubspaceModel {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
if (uuid != null) 'uuid': uuid,
'subspaceName': subspaceName,
'disabled': disabled,
'tags': tags?.map((e) => e.toJson()).toList() ?? [],

View File

@ -23,10 +23,13 @@ class Tag extends BaseTag {
final String internalId = json['internalId'] ?? const Uuid().v4();
return Tag(
uuid: json['uuid'] ?? '',
//TODO:insure UUId for tag or prodAlloc
uuid: json['name'] != null ? json['uuid'] : json['tag']?['uuid'] ?? '',
internalId: internalId,
tag: json['name'] ?? '',
product: json['product'] != null ? ProductModel.fromMap(json['product']) : null,
tag: json['name'] ?? json['tag']?['name'] ?? '',
product: json['product'] != null
? ProductModel.fromMap(json['product'])
: null,
);
}
@ -49,9 +52,10 @@ class Tag extends BaseTag {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'tag': tag,
'product': product?.toMap(),
if (uuid != null) 'uuid': uuid,
'name': tag,
'productUuid': product?.uuid,
// .toMap(),
};
}
}

View File

@ -67,7 +67,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void initState() {
super.initState();
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
_nameController = TextEditingController(
text: widget.selectedCommunity?.name ?? '',
@ -96,13 +97,15 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
if (oldWidget.spaces != widget.spaces) {
setState(() {
spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : [];
connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
connections =
widget.spaces.isNotEmpty ? createConnections(widget.spaces) : [];
_adjustCanvasSizeForSpaces();
realignTree();
});
}
if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) {
if (widget.selectedSpace != oldWidget.selectedSpace &&
widget.selectedSpace != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_moveToSpace(widget.selectedSpace!);
});
@ -185,7 +188,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
connection, widget.selectedSpace)
? 1.0
: 0.3, // Adjust opacity
child: CustomPaint(painter: CurvedLinePainter([connection])),
child: CustomPaint(
painter: CurvedLinePainter([connection])),
),
for (var entry in spaces.asMap().entries)
if (entry.value.status != SpaceStatus.deleted &&
@ -195,11 +199,11 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
top: entry.value.position.dy,
child: SpaceCardWidget(
index: entry.key,
onButtonTap: (int index, Offset newPosition, String direction) {
onButtonTap: (int index, Offset newPosition) {
_showCreateSpaceDialog(screenSize,
position: spaces[index].position + newPosition,
position:
spaces[index].position + newPosition,
parentIndex: index,
direction: direction,
projectTags: widget.projectTags);
},
position: entry.value.position,
@ -210,7 +214,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
_updateNodePosition(entry.value, newPosition);
},
buildSpaceContainer: (int index) {
final bool isHighlighted = SpaceHelper.isHighlightedSpace(
final bool isHighlighted =
SpaceHelper.isHighlightedSpace(
spaces[index], widget.selectedSpace);
return Opacity(
@ -289,7 +294,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _showCreateSpaceDialog(Size screenSize,
{Offset? position,
int? parentIndex,
String? direction,
double? canvasWidth,
double? canvasHeight,
required List<Tag> projectTags}) {
@ -299,19 +303,25 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return CreateSpaceDialog(
products: widget.products,
spaceModels: widget.spaceModels,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
allTags:
TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
parentSpace: parentIndex != null ? spaces[parentIndex] : null,
projectTags: projectTags,
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Set the first space in the center or use passed position
Offset newPosition;
if (parentIndex != null) {
newPosition =
getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position
newPosition = getBalancedChildPosition(
spaces[parentIndex]); // Ensure balanced position
} else {
newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize);
newPosition =
position ?? ConnectionHelper.getCenterPosition(screenSize);
}
SpaceModel newSpace = SpaceModel(
@ -325,14 +335,13 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
subspaces: subspaces,
tags: tags);
if (parentIndex != null && direction != null) {
if (parentIndex != null) {
SpaceModel parentSpace = spaces[parentIndex];
parentSpace.internalId = spaces[parentIndex].internalId;
newSpace.parent = parentSpace;
final newConnection = Connection(
startSpace: parentSpace,
endSpace: newSpace,
direction: direction,
);
connections.add(newConnection);
newSpace.incomingConnection = newConnection;
@ -360,16 +369,21 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
name: widget.selectedSpace!.name,
icon: widget.selectedSpace!.icon,
projectTags: widget.projectTags,
parentSpace:
SpaceHelper.findSpaceByInternalId(widget.selectedSpace?.parent?.internalId, spaces),
parentSpace: SpaceHelper.findSpaceByInternalId(
widget.selectedSpace?.parent?.internalId, spaces),
editSpace: widget.selectedSpace,
currentSpaceModel: widget.selectedSpace?.spaceModel,
tags: widget.selectedSpace?.tags,
subspaces: widget.selectedSpace?.subspaces,
isEdit: true,
allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels),
onCreateSpace: (String name, String icon, List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel, List<SubspaceModel>? subspaces, List<Tag>? tags) {
allTags: TagHelper.getAllTagValues(
widget.communities, widget.spaceModels),
onCreateSpace: (String name,
String icon,
List<SelectedProduct> selectedProducts,
SpaceTemplateModel? spaceModel,
List<SubspaceModel>? subspaces,
List<Tag>? tags) {
setState(() {
// Update the space's properties
widget.selectedSpace!.name = name;
@ -379,7 +393,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
widget.selectedSpace!.tags = tags;
if (widget.selectedSpace!.status != SpaceStatus.newSpace) {
widget.selectedSpace!.status = SpaceStatus.modified; // Mark as modified
widget.selectedSpace!.status =
SpaceStatus.modified; // Mark as modified
}
for (var space in spaces) {
@ -410,7 +425,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Map<String, SpaceModel> idToSpace = {};
void flatten(SpaceModel space) {
if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) {
if (space.status == SpaceStatus.deleted ||
space.status == SpaceStatus.parentDeleted) {
return;
}
result.add(space);
@ -447,7 +463,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
Connection(
startSpace: parent,
endSpace: child,
direction: "down",
),
);
@ -532,13 +547,16 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _selectSpace(BuildContext context, SpaceModel space) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: space),
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity,
selectedSpace: space),
);
}
void _deselectSpace(BuildContext context) {
context.read<SpaceManagementBloc>().add(
SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: null),
SelectSpaceEvent(
selectedCommunity: widget.selectedCommunity, selectedSpace: null),
);
}
@ -708,7 +726,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent);
duplicated.position = Offset(space.position.dx + 300, space.position.dy + 100);
duplicated.position =
Offset(space.position.dx + 300, space.position.dy + 100);
List<SpaceModel> duplicatedSubtree = [];
void collectSubtree(SpaceModel node) {
duplicatedSubtree.add(node);
@ -726,7 +745,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection(
startSpace: parent,
endSpace: duplicated,
direction: "down",
);
connections.add(newConnection);
duplicated.incomingConnection = newConnection;
@ -739,7 +757,8 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
}
SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) {
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final duplicatedName =
SpaceHelper.generateUniqueSpaceName(original.name, spaces);
final newSpace = SpaceModel(
name: duplicatedName,
@ -761,7 +780,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
final newConnection = Connection(
startSpace: newSpace,
endSpace: duplicatedChild,
direction: "down",
);
connections.add(newConnection);

View File

@ -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,
),
)
],
);
}
}

View File

@ -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,
),
),
),
),
],
),
],
);
}
}

View File

@ -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),
],
),
),
],
);
}
}

View File

@ -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),
),
),
],
);
}
}

View File

@ -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,
)
],
),
),
)
],
);
}
}

View File

@ -30,27 +30,12 @@ class CurvedLinePainter extends CustomPainter {
Offset end = connection.endSpace.position +
const Offset(75, 0); // Center top of end space
if (connection.direction == 'down') {
// Curved line for down connections
final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50);
final path = Path()
..moveTo(start.dx, start.dy)
..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint);
} else if (connection.direction == 'right') {
start = connection.startSpace.position +
const Offset(150, 30); // Right center
end = connection.endSpace.position + const Offset(0, 30); // Left center
canvas.drawLine(start, end, paint);
} else if (connection.direction == 'left') {
start =
connection.startSpace.position + const Offset(0, 30); // Left center
end = connection.endSpace.position +
const Offset(150, 30); // Right center
canvas.drawLine(start, end, paint);
}
final dotPaint = Paint()..color = ColorsManager.blackColor;
canvas.drawCircle(start, 5, dotPaint); // Start dot

View File

@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
@ -9,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/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/devices_part_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart';
@ -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/link_space_model/view/link_space_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/space_icon_const.dart';
@ -82,8 +83,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
super.initState();
selectedIcon = widget.icon ?? Assets.location;
nameController = TextEditingController(text: widget.name ?? '');
selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty;
selectedProducts =
widget.selectedProducts.isNotEmpty ? widget.selectedProducts : [];
isOkButtonEnabled =
enteredName.isNotEmpty || nameController.text.isNotEmpty;
if (widget.currentSpaceModel != null) {
subspaces = [];
tags = [];
@ -96,13 +99,15 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
@override
Widget build(BuildContext context) {
bool isSpaceModelDisabled =
(tags != null && tags!.isNotEmpty || subspaces != null && subspaces!.isNotEmpty);
bool isSpaceModelDisabled = (tags != null && tags!.isNotEmpty ||
subspaces != null && subspaces!.isNotEmpty);
bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null);
final screenWidth = MediaQuery.of(context).size.width;
return AlertDialog(
title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'),
title: widget.isEdit
? const Text('Edit Space')
: const Text('Create New Space'),
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
width: screenWidth * 0.5,
@ -112,50 +117,10 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
children: [
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 50),
Stack(
alignment: Alignment.center,
children: [
Container(
width: screenWidth * 0.1,
height: screenWidth * 0.1,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
),
SvgPicture.asset(
selectedIcon,
width: screenWidth * 0.04,
height: screenWidth * 0.04,
),
Positioned(
top: 20,
right: 20,
child: InkWell(
onTap: _showIconSelectionDialog,
child: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
),
),
],
),
],
child: IconChoosePartWidget(
selectedIcon: selectedIcon,
showIconSelection: _showIconSelectionDialog,
screenWidth: screenWidth,
),
),
const SizedBox(width: 20),
@ -164,11 +129,12 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: screenWidth * 0.25,
child: TextField(
controller: nameController,
onChanged: (value) {
SpaceNameTextfieldWidget(
isNameFieldExist: isNameFieldExist,
isNameFieldInvalid: isNameFieldInvalid,
nameController: nameController,
screenWidth: screenWidth,
onChange: (value) {
enteredName = value.trim();
setState(() {
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),
selectedSpaceModel == null
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
// SpaceModelLinkingWidget(
// isSpaceModelDisabled: true,
// // isSpaceModelDisabled,
// onPressed: () {
// isSpaceModelDisabled
// ? null
// : _showLinkSpaceModelDialog(context);
// },
// onDeleted: () => setState(() {
// selectedSpaceModel = null;
// subspaces = widget.subspaces ?? [];
// tags = widget.tags ?? [];
// }),
// screenWidth: screenWidth,
// selectedSpaceModel: selectedSpaceModel,
// ),
const SizedBox(height: 25),
// Row(
// children: [
// const Expanded(
// child: Divider(
// color: ColorsManager.neutralGray,
// thickness: 1.0,
// ),
// ),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
// child: Text(
// 'OR',
// style: Theme.of(context)
// .textTheme
// .bodyMedium
// ?.copyWith(fontWeight: FontWeight.bold),
// ),
// ),
// const Expanded(
// child: Divider(
// color: ColorsManager.neutralGray,
// thickness: 1.0,
// ),
// ),
// ],
// ),
const SizedBox(height: 25),
SubSpacePartWidget(
subspaces: subspaces,
onPressed: () {
isSpaceModelDisabled ? null : _showLinkSpaceModelDialog(context);
},
child: ButtonContentWidget(
svgAssets: Assets.link,
label: 'Link a space model',
disabled: isSpaceModelDisabled,
),
)
: Container(
width: screenWidth * 0.25,
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
Chip(
label: Text(
selectedSpaceModel?.modelName ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(
color: ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const Icon(
Icons.close,
size: 16,
color: ColorsManager.lightGrayColor,
),
),
onDeleted: () => setState(() {
this.selectedSpaceModel = null;
subspaces = widget.subspaces ?? [];
tags = widget.tags ?? [];
})),
],
),
),
const SizedBox(height: 25),
Row(
children: [
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Text(
'OR',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
const Expanded(
child: Divider(
color: ColorsManager.neutralGray,
thickness: 1.0,
),
),
],
),
const SizedBox(height: 25),
subspaces == null || subspaces!.isEmpty
? TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () async {
isTagsAndSubspaceModelDisabled
? null
: _showSubSpaceDialog(
context, enteredName, [], false, widget.products, subspaces);
},
child: ButtonContentWidget(
icon: Icons.add,
label: 'Create Sub Space',
disabled: isTagsAndSubspaceModelDisabled,
),
)
: SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
if (subspaces != null)
...subspaces!.map((subspace) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SubspaceNameDisplayWidget(
text: subspace.subspaceName,
validateName: (updatedName) {
bool nameExists = subspaces!.any((s) {
bool isSameId = s.internalId == subspace.internalId;
bool isSameName =
s.subspaceName.trim().toLowerCase() ==
updatedName.trim().toLowerCase();
return !isSameId && isSameName;
});
return !nameExists;
},
onNameChanged: (updatedName) {
setState(() {
subspace.subspaceName = updatedName;
});
},
),
],
context,
enteredName,
[],
false,
widget.products,
subspaces,
);
},
isTagsAndSubspaceModelDisabled:
isTagsAndSubspaceModelDisabled,
screenWidth: screenWidth,
editChipOnTap: () async {
_showSubSpaceDialog(
context,
enteredName,
[],
true,
widget.products,
subspaces,
);
}),
EditChip(
onTap: () async {
_showSubSpaceDialog(context, enteredName, [], true,
widget.products, subspaces);
},
)
],
),
),
),
const SizedBox(height: 10),
(tags?.isNotEmpty == true ||
subspaces?.any((subspace) => subspace.tags?.isNotEmpty == true) == true)
? SizedBox(
width: screenWidth * 0.25,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0, // Border width
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
// Combine tags from spaceModel and subspaces
...TagHelper.groupTags([
...?tags,
...?subspaces?.expand((subspace) => subspace.tags ?? [])
]).entries.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
entry.key.icon ?? 'assets/icons/gateway.svg',
fit: BoxFit.contain,
),
),
label: Text(
'x${entry.value}', // Show count
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
EditChip(onTap: () async {
DevicesPartWidget(
tags: tags,
subspaces: subspaces,
screenWidth: screenWidth,
isTagsAndSubspaceModelDisabled:
isTagsAndSubspaceModelDisabled,
onEditChip: () async {
await showDialog(
context: context,
builder: (context) => AssignTagDialog(
@ -476,13 +257,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
},
),
);
})
],
),
),
)
: TextButton(
onPressed: () {
},
onTextButtonPressed: () {
isTagsAndSubspaceModelDisabled
? null
: _showTagCreateDialog(
@ -492,14 +268,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
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) {
return;
} else {
String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? '');
String newName = enteredName.isNotEmpty
? enteredName
: (widget.name ?? '');
if (newName.isNotEmpty) {
widget.onCreateSpace(newName, selectedIcon, selectedProducts,
selectedSpaceModel, subspaces, tags);
if (tags != null && tags!.isNotEmpty) {
if (tags!.any(
(tag) => tag.uuid == null || tag.uuid!.isEmpty,
)) {
return;
}
}
widget.onCreateSpace(
newName,
selectedIcon,
selectedProducts,
selectedSpaceModel,
subspaces,
tags);
Navigator.of(context).pop();
}
}
},
borderRadius: 10,
backgroundColor:
isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor,
backgroundColor: isOkButtonEnabled
? ColorsManager.secondaryColor
: ColorsManager.grayColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
@ -550,6 +334,7 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
//dialooogggs
void _showIconSelectionDialog() {
showDialog(
context: context,
@ -586,26 +371,50 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showSubSpaceDialog(BuildContext context, String name, final List<Tag>? spaceTags,
bool isEdit, List<ProductModel>? products, final List<SubspaceModel>? existingSubSpaces) {
void _showSubSpaceDialog(
BuildContext context,
String name,
final List<Tag>? spaceTags,
bool isEdit,
List<ProductModel>? products,
final List<SubspaceModel>? existingSubSpaces,
) {
showDialog(
context: context,
builder: (BuildContext context) {
return CreateSubSpaceDialog(
spaceName: name,
dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space',
dialogTitle: isEdit ? 'Edit Sub-spaces' : 'Create Sub-spaces',
products: products,
existingSubSpaces: existingSubSpaces,
onSave: (slectedSubspaces) {
onSave: (slectedSubspaces, updatedSubSpaces) {
final List<Tag> tagsToAppendToSpace = [];
if (slectedSubspaces != null) {
final updatedIds = slectedSubspaces.map((s) => s.internalId).toSet();
if (slectedSubspaces != null && slectedSubspaces.isNotEmpty) {
final updatedIds =
slectedSubspaces.map((s) => s.internalId).toSet();
if (existingSubSpaces != null) {
final deletedSubspaces =
existingSubSpaces.where((s) => !updatedIds.contains(s.internalId)).toList();
final deletedSubspaces = existingSubSpaces
.where((s) => !updatedIds.contains(s.internalId))
.toList();
for (var s in deletedSubspaces) {
if (s.tags != null) {
s.tags!.forEach(
(tag) => tag.location = null,
);
tagsToAppendToSpace.addAll(s.tags!);
}
}
}
} else {
if (existingSubSpaces != null) {
final deletedSubspaces = existingSubSpaces;
for (var s in deletedSubspaces) {
if (s.tags != null) {
s.tags!.forEach(
(tag) => tag.location = null,
);
tagsToAppendToSpace.addAll(s.tags!);
}
}
@ -623,15 +432,16 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
);
}
void _showTagCreateDialog(
BuildContext context, String name, bool isEdit, List<ProductModel>? products) {
void _showTagCreateDialog(BuildContext context, String name, bool isEdit,
List<ProductModel>? products) {
isEdit
? showDialog(
context: context,
builder: (BuildContext context) {
return AssignTagDialog(
title: 'Edit Device',
addedProducts: TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
addedProducts: TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
spaceName: name,
products: products,
subspaces: subspaces,
@ -646,7 +456,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}
@ -670,7 +481,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
allTags: widget.allTags,
projectTags: widget.projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(tags, subspaces),
TagHelper.createInitialSelectedProductsForTags(
tags, subspaces),
onSave: (selectedSpaceTags, selectedSubspaces) {
setState(() {
tags = selectedSpaceTags;
@ -680,7 +492,8 @@ class CreateSpaceDialogState extends State<CreateSpaceDialog> {
if (subspaces != null) {
for (final subspace in subspaces!) {
for (final selectedSubspace in selectedSubspaces) {
if (subspace.subspaceName == selectedSubspace.subspaceName) {
if (subspace.subspaceName ==
selectedSubspace.subspaceName) {
subspace.tags = selectedSubspace.tags;
}
}

View File

@ -5,7 +5,7 @@ class PlusButtonWidget extends StatelessWidget {
final int index;
final String direction;
final Offset offset;
final Function(int index, Offset newPosition, String direction) onButtonTap;
final Function(int index, Offset newPosition) onButtonTap;
const PlusButtonWidget({
super.key,
@ -17,26 +17,9 @@ class PlusButtonWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Positioned(
left: offset.dx,
top: offset.dy,
child: GestureDetector(
return GestureDetector(
onTap: () {
Offset newPosition;
switch (direction) {
case 'left':
newPosition = const Offset(-200, 0);
break;
case 'right':
newPosition = const Offset(200, 0);
break;
case 'down':
newPosition = const Offset(0, 150);
break;
default:
newPosition = Offset.zero;
}
onButtonTap(index, newPosition, direction);
onButtonTap(index, const Offset(0, 150));
},
child: Container(
width: 30,
@ -45,7 +28,10 @@ class PlusButtonWidget extends StatelessWidget {
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
),
);

View File

@ -7,7 +7,7 @@ class SpaceCardWidget extends StatelessWidget {
final Offset position;
final bool isHovered;
final Function(int index, bool isHovered) onHoverChanged;
final Function(int index, Offset newPosition, String direction) onButtonTap;
final Function(int index, Offset newPosition) onButtonTap;
final Widget Function(int index) buildSpaceContainer;
final ValueChanged<Offset> onPositionChanged;
@ -25,35 +25,34 @@ class SpaceCardWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
// Call the provided callback to update the position
final newPosition = position + details.delta;
onPositionChanged(newPosition);
},
child: MouseRegion(
onEnter: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, true);
},
onExit: (_) {
// Call the provided callback to handle hover state
onHoverChanged(index, false);
},
return MouseRegion(
onEnter: (_) => onHoverChanged(index, true),
onExit: (_) => onHoverChanged(index, false),
child: SizedBox(
width: 140, // Make sure this covers both card and plus button
height: 90,
child: Stack(
clipBehavior: Clip
.none, // Allow hovering elements to be displayed outside the boundary
clipBehavior: Clip.none,
children: [
buildSpaceContainer(index), // Build the space container
if (isHovered) ...[
PlusButtonWidget(
// Main card
Container(
width: 140,
height: 80,
alignment: Alignment.center,
color: Colors.transparent,
child: buildSpaceContainer(index),
),
// Plus button (NO inner Positioned!)
if (isHovered)
Align(
alignment: Alignment.bottomCenter,
child: PlusButtonWidget(
index: index,
direction: 'down',
offset: const Offset(63, 50),
offset: Offset.zero,
onButtonTap: onButtonTap,
),
],
),
],
),
),

View File

@ -13,7 +13,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final existingTagCounts = <String, int>{};
for (var tag in initialTags) {
if (tag.product != null) {
existingTagCounts[tag.product!.uuid] = (existingTagCounts[tag.product!.uuid] ?? 0) + 1;
existingTagCounts[tag.product!.uuid] =
(existingTagCounts[tag.product!.uuid] ?? 0) + 1;
}
}
@ -22,14 +23,17 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
for (var selectedProduct in event.addedProducts) {
final existingCount = existingTagCounts[selectedProduct.productId] ?? 0;
if (selectedProduct.count == 0 || selectedProduct.count <= existingCount) {
tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId));
if (selectedProduct.count == 0 ||
selectedProduct.count <= existingCount) {
tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
continue;
}
final missingCount = selectedProduct.count - existingCount;
tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId));
tags.addAll(initialTags
.where((tag) => tag.product?.uuid == selectedProduct.productId));
if (missingCount > 0) {
tags.addAll(List.generate(
@ -85,7 +89,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final tags = List<Tag>.from(currentState.tags);
// Update the location
tags[event.index] = tags[event.index].copyWith(location: event.location);
tags[event.index] =
tags[event.index].copyWith(location: event.location);
final updatedTags = _calculateAvailableTags(projectTags, tags);
@ -117,7 +122,8 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
final currentState = state;
if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) {
final tags = List<Tag>.from(currentState.tags)..remove(event.tagToDelete);
final tags = List<Tag>.from(currentState.tags)
..remove(event.tagToDelete);
// Recalculate available tags
final updatedTags = _calculateAvailableTags(projectTags, tags);
@ -141,8 +147,10 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
// Get validation error for duplicate tags
String? _getValidationError(List<Tag> tags) {
final nonEmptyTags =
tags.map((tag) => tag.tag?.trim() ?? '').where((tag) => tag.isNotEmpty).toList();
final nonEmptyTags = tags
.map((tag) => tag.tag?.trim() ?? '')
.where((tag) => tag.isNotEmpty)
.toList();
final duplicateTags = nonEmptyTags
.fold<Map<String, int>>({}, (map, tag) {
@ -168,9 +176,11 @@ class AssignTagBloc extends Bloc<AssignTagEvent, AssignTagState> {
.toSet();
final availableTags = allTags
.where((tag) => tag.tag != null && !selectedTagSet.contains(tag.tag!.trim()))
.where((tag) =>
tag.tag != null && !selectedTagSet.contains(tag.tag!.trim()))
.toList();
return availableTags;
return projectTags;
// availableTags;
}
}

View File

@ -1,10 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.dart';
import 'package:syncrow_web/common/tag_dialog_textfield_dropdown.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
@ -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_event.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart';
import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart';
import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.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 {
final List<ProductModel>? products;
@ -29,7 +25,7 @@ class AssignTagDialog extends StatelessWidget {
final List<Tag> projectTags;
const AssignTagDialog(
{Key? key,
{super.key,
required this.products,
required this.subspaces,
required this.addedProducts,
@ -39,13 +35,14 @@ class AssignTagDialog extends StatelessWidget {
required this.spaceName,
required this.title,
this.onSave,
required this.projectTags})
: super(key: key);
required this.projectTags});
@override
Widget build(BuildContext context) {
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space');
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider(
create: (_) => AssignTagBloc(projectTags)
@ -67,131 +64,31 @@ class AssignTagDialog extends StatelessWidget {
content: SingleChildScrollView(
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(state.tags.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label: Text('Tag', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label:
Text('Location', style: Theme.of(context).textTheme.bodyMedium)),
],
rows: state.tags.isEmpty
? [
DataRow(cells: [
DataCell(
Center(
child: Text('No Data Available',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
)),
),
),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
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));
AssignTagsTable(
controllers: controllers,
locations: locations,
tags: state.tags,
updatedTags: state.updatedTags,
onDeleteDevice: ({required index, required tag}) {
context
.read<AssignTagBloc>()
.add(DeleteTag(tagToDelete: tag, tags: state.tags));
controllers.removeAt(index);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment:
Alignment.centerLeft, // Align cell content to the left
child: SizedBox(
width: double.infinity,
child: TagDialogTextfieldDropdown(
key: ValueKey('dropdown_${const Uuid().v4()}_$index'),
items: state.updatedTags,
product: tag.product?.uuid ?? 'Unknown',
initialValue: tag,
onSelected: (value) {
controller.text = value.tag ?? '';
onTagDropDownSelected: ({required index, required tag}) {
context.read<AssignTagBloc>().add(UpdateTagEvent(
index: index,
tag: value,
tag: tag,
));
},
),
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue: tag.location ?? 'Main Space',
onSelected: (value) {
onLocationDropDownSelected: (
{required index, required location}) {
context.read<AssignTagBloc>().add(UpdateLocation(
index: index,
location: value,
location: location,
));
},
)),
),
],
);
}),
),
),
if (state.errorMessage != null)
Text(state.errorMessage!,
@ -203,69 +100,15 @@ class AssignTagDialog extends StatelessWidget {
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
const SizedBox(width: 10),
Expanded(
child: Builder(
builder: (buttonContext) => CancelButton(
label: 'Add New Device',
onPressed: () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceModel>.from(result['subspaces'] as List<dynamic>);
Navigator.of(context).pop();
await showDialog(
context: context,
builder: (context) => AddDeviceTypeWidget(
SaveAddDeviceRowWidget(
subspaces: subspaces,
products: products,
subspaces: processedSubspaces,
projectTags: projectTags,
initialSelectedProducts:
TagHelper.createInitialSelectedProductsForTags(
processedTags, processedSubspaces),
spaceName: spaceName,
spaceTags: processedTags,
isCreate: false,
onSave: onSave,
allTags: allTags,
),
);
},
),
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: state.isSaveEnabled
? ColorsManager.whiteColors
: ColorsManager.whiteColorsWithOpacity,
onPressed: state.isSaveEnabled
? () async {
final updatedTags = List<Tag>.from(state.tags);
final result = TagHelper.processTags(updatedTags, subspaces);
final processedTags = result['updatedTags'] as List<Tag>;
final processedSubspaces =
List<SubspaceModel>.from(result['subspaces'] as List<dynamic>);
onSave?.call(processedTags, processedSubspaces);
Navigator.of(context).pop();
}
: null,
child: const Text('Save'),
),
),
const SizedBox(width: 10),
],
tags: state.tags,
isSaveEnabled: state.isSaveEnabled,
),
],
);

View File

@ -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),
],
);
}
}

View File

@ -1,9 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.dart';
import 'package:syncrow_web/common/tag_dialog_textfield_dropdown.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
@ -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/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import '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 {
final List<ProductModel>? products;
@ -53,8 +48,10 @@ class AssignTagModelsDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final List<String> locations =
(subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space');
final List<String> locations = (subspaces ?? [])
.map((subspace) => subspace.subspaceName)
.toList()
..add('Main Space');
return BlocProvider(
create: (_) => AssignTagModelBloc(projectTags)
@ -78,137 +75,38 @@ class AssignTagModelsDialog extends StatelessWidget {
content: SingleChildScrollView(
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey),
key: ValueKey(state.tags.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
DataColumn(
label: Text('#', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Device',
style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
numeric: false,
label:
Text('Tag', style: Theme.of(context).textTheme.bodyMedium)),
DataColumn(
label: Text('Location',
style: Theme.of(context).textTheme.bodyMedium)),
],
rows: state.tags.isEmpty
? [
DataRow(cells: [
DataCell(
Center(
child: Text('No Devices Available',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
)),
),
),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
const DataCell(SizedBox()),
])
]
: List.generate(state.tags.length, (index) {
final tag = state.tags[index];
final controller = controllers[index];
return DataRow(
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
tag.product?.name ?? 'Unknown',
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.0,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
context.read<AssignTagModelBloc>().add(
DeleteTagModel(
tagToDelete: tag, tags: state.tags));
controllers.removeAt(index);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment
.centerLeft, // Align cell content to the left
child: SizedBox(
width: double.infinity,
child: TagDialogTextfieldDropdown(
key: ValueKey(
'dropdown_${const Uuid().v4()}_$index'),
product: tag.product?.uuid ?? 'Unknown',
items: state.updatedTags,
initialValue: tag,
onSelected: (value) {
controller.text = value.tag ?? '';
context.read<AssignTagModelBloc>().add(UpdateTag(
index: index,
tag: value,
));
},
),
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: locations,
selectedValue: tag.location ?? 'Main Space',
onSelected: (value) {
AssignTagsTable(
controllers: controllers,
locations: locations,
tags: state.tags,
updatedTags: state.updatedTags,
onDeleteDevice: ({required index, required tag}) {
context
.read<AssignTagModelBloc>()
.add(UpdateLocation(
index: index,
location: value,
.add(DeleteTagModel(
tagToDelete: tag,
tags: state.tags,
));
controllers.removeAt(index);
},
)),
onTagDropDownSelected: (
{required index, required tag}) {
context.read<AssignTagModelBloc>().add(
UpdateTag(
index: index,
tag: tag,
),
],
);
}),
},
onLocationDropDownSelected: (
{required index, required location}) {
context.read<AssignTagModelBloc>().add(
UpdateLocation(
index: index,
location: location,
),
);
},
),
if (state.errorMessage != null)
Text(state.errorMessage!,
@ -220,101 +118,16 @@ class AssignTagModelsDialog extends StatelessWidget {
),
),
actions: [
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(
RowOfSaveCancelWidget(
subspaces: subspaces,
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,
spaceModel: spaceModel,
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),
],
),
],
);

View File

@ -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();
}
},
);
}
}

View File

@ -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);
},
)),
),
],
);
}),
),
);
}
}

View File

@ -1,23 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart';
import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'widgets/ok_cancel_sub_space_widget.dart';
import 'widgets/textfield_sub_space_dialog_widget.dart';
class CreateSubSpaceDialog extends StatefulWidget {
final String dialogTitle;
final List<SubspaceModel>? existingSubSpaces;
final String? spaceName;
final List<ProductModel>? products;
final void Function(List<SubspaceModel>?)? onSave;
final void Function(
List<SubspaceModel>?, List<UpdateSubspaceModel> updatedSubSpaces)? onSave;
const CreateSubSpaceDialog({
required this.dialogTitle,
@ -33,14 +33,7 @@ class CreateSubSpaceDialog extends StatefulWidget {
}
class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
late final TextEditingController _subspaceNameController;
@override
void initState() {
_subspaceNameController = TextEditingController();
super.initState();
}
final TextEditingController _subspaceNameController = TextEditingController();
@override
void dispose() {
_subspaceNameController.dispose();
@ -79,83 +72,26 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
color: ColorsManager.blackColor,
),
),
const SizedBox(height: 16),
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,
Row(
children: [
...state.subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.subspaceName.toLowerCase();
final duplicateIndices = state.subSpaces
.asMap()
.entries
.where((e) =>
e.value.subspaceName.toLowerCase() == lowerName)
.map((e) => e.key)
.toList();
final isDuplicate = duplicateIndices.length > 1 &&
duplicateIndices.indexOf(index) != 0;
return SubspaceChip(
subSpace: SubspaceTemplateModel(
subspaceName: entry.value.subspaceName,
disabled: entry.value.disabled,
),
isDuplicate: isDuplicate,
onDeleted: () => context.read<SubSpaceBloc>().add(
RemoveSubSpace(subSpace),
),
);
},
),
SizedBox(
width: 200,
child: TextField(
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: state.subSpaces.isEmpty
? 'Please enter the name'
: null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
Text(
'press Enter to Save',
style: context.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w500,
fontSize: 10,
color: ColorsManager.grayColor,
),
),
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,
),
const SizedBox(
width: 5,
),
const Icon(Icons.save_as_sharp, size: 10),
],
),
const SizedBox(height: 16),
TextFieldSubSpaceDialogWidget(
subspaceNameController: _subspaceNameController,
subSpaces: state.subSpaces,
),
if (state.errorMessage.isNotEmpty)
Padding(
@ -168,36 +104,10 @@ class _CreateSubSpaceDialogState extends State<CreateSubSpaceDialog> {
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () async {
Navigator.of(context).pop();
},
),
),
const SizedBox(width: 10),
Expanded(
child: DefaultButton(
onPressed: state.errorMessage.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'),
),
),
],
OkCancelSubSpaceWidget(
subspaceNameController: _subspaceNameController,
errorMessage: state.errorMessage,
onSave: widget.onSave,
),
],
),

View File

@ -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'),
),
),
],
);
}
}

View File

@ -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,
),
),
],
),
);
}
}

View File

@ -31,15 +31,19 @@ class CenterBodyWidget extends StatelessWidget {
children: [
GestureDetector(
onTap: () {
context.read<CenterBodyBloc>().add(CommunityStructureSelectedEvent());
context
.read<CenterBodyBloc>()
.add(CommunityStructureSelectedEvent());
},
child: Text(
'Community Structure',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is CommunityStructureState || state is CommunitySelectedState
fontWeight: state is CommunityStructureState ||
state is CommunitySelectedState
? FontWeight.bold
: FontWeight.normal,
color: state is CommunityStructureState || state is CommunitySelectedState
color: state is CommunityStructureState ||
state is CommunitySelectedState
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
@ -50,26 +54,26 @@ class CenterBodyWidget extends StatelessWidget {
),
),
const SizedBox(width: 20),
GestureDetector(
onTap: () {
context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
},
child: Text(
'Space Model',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: state is SpaceModelState
? FontWeight.bold
: FontWeight.normal,
color: state is SpaceModelState
? Theme.of(context).textTheme.bodyLarge!.color
: Theme.of(context)
.textTheme
.bodyLarge!
.color!
.withOpacity(0.5),
),
),
),
// GestureDetector(
// onTap: () {
// context.read<CenterBodyBloc>().add(SpaceModelSelectedEvent());
// },
// child: Text(
// 'Space Model',
// style: Theme.of(context).textTheme.bodyLarge!.copyWith(
// fontWeight: state is SpaceModelState
// ? FontWeight.bold
// : FontWeight.normal,
// color: state is SpaceModelState
// ? Theme.of(context).textTheme.bodyLarge!.color
// : Theme.of(context)
// .textTheme
// .bodyLarge!
// .color!
// .withOpacity(0.5),
// ),
// ),
// ),
],
),
],

View File

@ -24,37 +24,22 @@ class AddDeviceTypeModelBloc
if (currentState is AddDeviceModelLoaded) {
final existingProduct = currentState.selectedProducts.firstWhere(
(p) => p.productId == event.productId,
orElse: () => SelectedProduct(
productId: event.productId,
count: 0,
productName: event.productName,
product: event.product,
),
(p) => p.productId == event.selectedProduct.productId,
orElse: () => event.selectedProduct,
);
List<SelectedProduct> updatedProducts;
if (event.count > 0) {
if (event.selectedProduct.count > 0) {
if (!currentState.selectedProducts.contains(existingProduct)) {
updatedProducts = [
...currentState.selectedProducts,
SelectedProduct(
productId: event.productId,
count: event.count,
productName: event.productName,
product: event.product,
),
event.selectedProduct,
];
} else {
updatedProducts = currentState.selectedProducts.map((p) {
if (p.productId == event.productId) {
return SelectedProduct(
productId: p.productId,
count: event.count,
productName: p.productName,
product: p.product,
);
if (p.productId == event.selectedProduct.productId) {
return event.selectedProduct;
}
return p;
}).toList();
@ -62,7 +47,7 @@ class AddDeviceTypeModelBloc
} else {
// Remove the product if the count is 0
updatedProducts = currentState.selectedProducts
.where((p) => p.productId != event.productId)
.where((p) => p.productId != event.selectedProduct.productId)
.toList();
}

View File

@ -10,20 +10,15 @@ abstract class AddDeviceTypeModelEvent extends Equatable {
List<Object> get props => [];
}
class UpdateProductCountEvent extends AddDeviceTypeModelEvent {
final String productId;
final int count;
final String productName;
final ProductModel product;
final SelectedProduct selectedProduct;
UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product});
UpdateProductCountEvent({required this.selectedProduct});
@override
List<Object> get props => [productId, count];
List<Object> get props => [selectedProduct];
}
class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent {
final List<Tag> initialTags;
final List<SelectedProduct> addedProducts;

View File

@ -54,10 +54,10 @@ class DeviceTypeTileWidget extends StatelessWidget {
onCountChanged: (newCount) {
context.read<AddDeviceTypeModelBloc>().add(
UpdateProductCountEvent(
productId: product.uuid,
count: newCount,
productName: product.catName,
product: product),
selectedProduct: product.toSelectedProduct(
newCount,
),
),
);
},
),

View File

@ -22,6 +22,18 @@ class HTTPService {
);
client.interceptors.add(serviceLocator.get<HTTPInterceptor>());
// Add this interceptor for logging requests and responses
// client.interceptors.add(
// LogInterceptor(
// request: true,
// requestHeader: true,
// requestBody: true,
// responseHeader: false,
// responseBody: true,
// error: true,
// logPrint: (object) => print(object),
// ),
// );
return client;
}

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/space_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart';
import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
import '../pages/spaces_management/all_spaces/model/subspace_model.dart';
class CommunitySpaceManagementApi {
// Community Management APIs
Future<List<CommunityModel>> fetchCommunities(String projectId, {int page = 1}) async {
Future<List<CommunityModel>> fetchCommunities(String projectId,
{int page = 1}) async {
try {
List<CommunityModel> allCommunities = [];
bool hasNext = true;
while (hasNext) {
await HTTPService().get(
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
path: ApiEndpoints.getCommunityList
.replaceAll('{projectId}', projectId),
queryParameters: {
'page': page,
},
@ -55,8 +60,14 @@ class CommunitySpaceManagementApi {
try {
bool hasNext = false;
await HTTPService().get(
path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search},
path:
ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId),
queryParameters: {
'page': page,
'includeSpaces': true,
'size': 25,
'search': search
},
expectedResponseModel: (json) {
try {
List<dynamic> jsonData = json['data'] ?? [];
@ -68,7 +79,10 @@ class CommunitySpaceManagementApi {
page = currentPage + 1;
paginationModel = PaginationModel(
pageNum: page, hasNext: hasNext, size: 25, communities: communityList);
pageNum: page,
hasNext: hasNext,
size: 25,
communities: communityList);
return paginationModel;
} catch (_) {
hasNext = false;
@ -83,7 +97,8 @@ class CommunitySpaceManagementApi {
Future<CommunityModel?> getCommunityById(String communityId) async {
try {
final response = await HTTPService().get(
path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId),
path: ApiEndpoints.getCommunityById
.replaceAll('{communityId}', communityId),
expectedResponseModel: (json) {
return CommunityModel.fromJson(json['data']);
},
@ -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 {
final response = await HTTPService().post(
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 {
final response = await HTTPService().put(
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 {
final response = await HTTPService().get(
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 {
final response = await HTTPService().get(
path: ApiEndpoints.getSpace
@ -199,7 +218,6 @@ class CommunitySpaceManagementApi {
{required String communityId,
required String name,
String? parentId,
String? direction,
bool isPrivate = false,
required Offset position,
String? spaceModelUuid,
@ -213,7 +231,6 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate,
'x': position.dx,
'y': position.dy,
'direction': direction,
'icon': icon,
};
if (parentId != null) {
@ -248,11 +265,10 @@ class CommunitySpaceManagementApi {
required String name,
String? parentId,
String? icon,
String? direction,
bool isPrivate = false,
required Offset position,
List<TagModelUpdate>? tags,
List<UpdateSubspaceTemplateModel>? subspaces,
List<Tag>? tags,
List<SubspaceModel>? subspaces,
String? spaceModelUuid,
required String projectId}) async {
try {
@ -261,9 +277,8 @@ class CommunitySpaceManagementApi {
'isPrivate': isPrivate,
'x': position.dx,
'y': position.dy,
'direction': direction,
'icon': icon,
'subspace': subspaces,
'subspaces': subspaces,
'tags': tags,
'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 {
final response = await HTTPService().delete(
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 {
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceHierarchy
.replaceAll('{communityId}', communityId)
.replaceAll('{projectId}', projectId),
expectedResponseModel: (json) {
final spaceModels =
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList();
final spaceModels = (json['data'] as List)
.map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
return spaceModels;
},
@ -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 {
final response = await HTTPService().get(
path: ApiEndpoints.spaceOnlyWithDevices
.replaceAll('{communityId}', communityId!)
.replaceAll('{projectId}', projectId!),
expectedResponseModel: (json) {
final spaceModels =
(json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList();
final spaceModels = (json['data'] as List)
.map((spaceJson) => SpaceModel.fromJson(spaceJson))
.toList();
return spaceModels;
},
);

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/space_model/models/create_space_template_body_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 {
Future<List<SpaceTemplateModel>> listSpaceModels(
{required String projectId, int page = 1}) async {
final response = await HTTPService().get(
path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId),
queryParameters: {'page': page},
expectedResponseModel: (json) {
List<dynamic> jsonData = json['data'];
return jsonData.map((jsonItem) {
return SpaceTemplateModel.fromJson(jsonItem);
}).toList();
},
);
return response;
try {
// final response = await HTTPService().get(
// path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId),
// queryParameters: {'page': page},
// expectedResponseModel: (json) {
// List<dynamic> jsonData = json['data'];
// return jsonData.map((jsonItem) {
// return SpaceTemplateModel.fromJson(jsonItem);
// }).toList();
// },
// );
return [];
// response;
} catch (e) {
log(e.toString());
return [];
}
}
Future<SpaceTemplateModel?> createSpaceModel(
@ -33,8 +41,8 @@ class SpaceModelManagementApi {
return response;
}
Future<String?> updateSpaceModel(
CreateSpaceTemplateBodyModel spaceModel, String spaceModelUuid, String projectId) async {
Future<String?> updateSpaceModel(CreateSpaceTemplateBodyModel spaceModel,
String spaceModelUuid, String projectId) async {
final response = await HTTPService().put(
path: ApiEndpoints.updateSpaceModel
.replaceAll('{projectId}', projectId)
@ -47,7 +55,8 @@ class SpaceModelManagementApi {
return response;
}
Future<SpaceTemplateModel?> getSpaceModel(String spaceModelUuid, String projectId) async {
Future<SpaceTemplateModel?> getSpaceModel(
String spaceModelUuid, String projectId) async {
final response = await HTTPService().get(
path: ApiEndpoints.getSpaceModel
.replaceAll('{projectId}', projectId)

View File

@ -2,6 +2,7 @@ class Assets {
Assets._();
static const String background = "assets/images/Background.png";
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 logo = "assets/images/Logo.svg";
static const String logoHorizontal = "assets/images/logo_horizontal.png";

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/web_layout/web_app_bar.dart';
import 'menu_sidebar.dart';
class WebScaffold extends StatelessWidget with HelperResponsiveLayout {
@ -28,14 +28,11 @@ class WebScaffold extends StatelessWidget with HelperResponsiveLayout {
SizedBox(
width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height,
child: SvgPicture.asset(
Assets.webBackground,
child: Image.asset(
Assets.webBackgroundPng,
fit: BoxFit.cover,
),
),
Container(
color: Colors.white.withOpacity(0.7),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [

View File

@ -1,906 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
url: "https://pub.dev"
source: hosted
version: "1.3.51"
args:
dependency: transitive
description:
name: args
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
bloc:
dependency: "direct main"
description:
name: bloc
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
url: "https://pub.dev"
source: hosted
version: "8.1.4"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
source: hosted
version: "1.19.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
csslib:
dependency: transitive
description:
name: csslib
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
url: "https://pub.dev"
source: hosted
version: "0.17.3"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.8"
data_table_2:
dependency: "direct main"
description:
name: data_table_2
sha256: f02ec9b24f44420816a87370ff4f4e533e15b274f6267e4c9a88a585ad1a0473
url: "https://pub.dev"
source: hosted
version: "2.5.15"
dio:
dependency: "direct main"
description:
name: dio
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev"
source: hosted
version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dropdown_button2:
dependency: "direct main"
description:
name: dropdown_button2
sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1
url: "https://pub.dev"
source: hosted
version: "2.3.9"
dropdown_search:
dependency: "direct main"
description:
name: dropdown_search
sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
equatable:
dependency: "direct main"
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.dev"
source: hosted
version: "2.0.5"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
firebase_analytics:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
url: "https://pub.dev"
source: hosted
version: "11.4.2"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
url: "https://pub.dev"
source: hosted
version: "4.3.2"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
url: "https://pub.dev"
source: hosted
version: "0.5.10+8"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
url: "https://pub.dev"
source: hosted
version: "3.11.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
url: "https://pub.dev"
source: hosted
version: "5.4.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678"
url: "https://pub.dev"
source: hosted
version: "2.20.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "6273ed71bcd8a6fb4d0ca13d3abddbb3301796807efaad8782b5f90156f26f03"
url: "https://pub.dev"
source: hosted
version: "4.3.2"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "94f3986e1a10e5a883f2ad5e3d719aef98a8a0f9a49357f6e45b7d3696ea6a97"
url: "https://pub.dev"
source: hosted
version: "3.8.2"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: cd2354dfef68e52c0713b5efbb7f4e10dfc2aff2f945c7bc8db34d1934170627
url: "https://pub.dev"
source: hosted
version: "11.3.2"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: d430983f4d877c9f72f88b3d715cca9a50021dd7ccd8e3ae6fb79603853317de
url: "https://pub.dev"
source: hosted
version: "0.2.6+2"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: f64edae62c5beaa08e9e611a0736d64ab11a812983a0aa132695d2d191311ea7
url: "https://pub.dev"
source: hosted
version: "0.2.6+8"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
fl_chart:
dependency: "direct main"
description:
name: fl_chart
sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef"
url: "https://pub.dev"
source: hosted
version: "0.69.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bloc:
dependency: "direct main"
description:
name: flutter_bloc
sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
url: "https://pub.dev"
source: hosted
version: "8.1.5"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee"
url: "https://pub.dev"
source: hosted
version: "3.0.0-beta.2"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0"
url: "https://pub.dev"
source: hosted
version: "9.2.2"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
get_it:
dependency: "direct main"
description:
name: get_it
sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1
url: "https://pub.dev"
source: hosted
version: "7.7.0"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
url: "https://pub.dev"
source: hosted
version: "14.2.7"
graphview:
dependency: "direct main"
description:
name: graphview
sha256: bdba183583b23c30c71edea09ad5f0beef612572d3e39e855467a925bd08392f
url: "https://pub.dev"
source: hosted
version: "1.2.0"
html:
dependency: transitive
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http:
dependency: transitive
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
intl_phone_field:
dependency: "direct main"
description:
name: intl_phone_field
sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev"
source: hosted
version: "3.0.0"
list_counter:
dependency: transitive
description:
name: list_counter
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
url: "https://pub.dev"
source: hosted
version: "1.0.2"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.15.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
number_pagination:
dependency: "direct main"
description:
name: number_pagination
sha256: "75d3a28616196e7c8df431d0fb7c48e811e462155f4cf3b5b4167b3408421327"
url: "https://pub.dev"
source: hosted
version: "1.1.6"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
url: "https://pub.dev"
source: hosted
version: "2.1.3"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: e84c8a53fe1510ef4582f118c7b4bdf15b03002b51d7c2b66983c65843d61193
url: "https://pub.dev"
source: hosted
version: "2.2.8"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
url: "https://pub.dev"
source: hosted
version: "2.4.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: transitive
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
time_picker_spinner:
dependency: "direct main"
description:
name: time_picker_spinner
sha256: "53d824801d108890d22756501e7ade9db48b53dac1ec41580499dd4ebd128e3c"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.dev"
source: hosted
version: "1.3.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
url: "https://pub.dev"
source: hosted
version: "6.3.14"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
url: "https://pub.dev"
source: hosted
version: "6.3.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
source: hosted
version: "14.3.0"
web:
dependency: transitive
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.1.0"
win32:
dependency: transitive
description:
name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
url: "https://pub.dev"
source: hosted
version: "5.5.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0"