mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
Compare commits
128 Commits
fix-schedu
...
SP-1703-fe
Author | SHA1 | Date | |
---|---|---|---|
19cdd371f8 | |||
d82a050422 | |||
a1562110d5 | |||
46aa5e2ddb | |||
ec1bb5b609 | |||
5827ba4296 | |||
b96f65d2c2 | |||
c198047165 | |||
b97183fb61 | |||
07dfe6b206 | |||
c4fd90b3bc | |||
bbcb947313 | |||
13e9a808ab | |||
32208c1e81 | |||
1d95915f57 | |||
e365aa3faa | |||
26e8ff7ee2 | |||
3fc6964e15 | |||
0c0bf96c07 | |||
4744009cb6 | |||
1a4ced195a | |||
812c51400b | |||
5beae81596 | |||
f1144762b0 | |||
ca41aa6224 | |||
396ce3dad8 | |||
2d0019200e | |||
475462301f | |||
731ba7a768 | |||
7fda564ee4 | |||
11e2853403 | |||
9c02bed4c0 | |||
4f932b8c35 | |||
44ae8386df | |||
9d4a665547 | |||
f43826a824 | |||
0b372e1ed8 | |||
4e9bcbdcea | |||
eee6a80c50 | |||
03ba506294 | |||
6c268754a9 | |||
8593055923 | |||
18ab9fd24c | |||
3c9494963d | |||
07dd260593 | |||
f38ac58442 | |||
30e940fdfc | |||
520b73717a | |||
e1bb67d7bd | |||
5e0df09cb6 | |||
22070ca04a | |||
6d667af7dc | |||
3b4952db0a | |||
5f59583696 | |||
7397486e7a | |||
487c5a894b | |||
7e0200aad8 | |||
562c67a958 | |||
52b843d514 | |||
423ad6e687 | |||
932e50f518 | |||
c649044a1f | |||
c46cfb04a7 | |||
8754960713 | |||
c6e98fa245 | |||
277a9ce4f0 | |||
f901983aa5 | |||
010403f1fa | |||
ee1ebeae2e | |||
6e6ef79ed0 | |||
7e5825de45 | |||
db9e856bca | |||
07435ec89e | |||
2a2fb7ffca | |||
5a2299ea2f | |||
90f8305aa1 | |||
329b2ba472 | |||
0fb9149613 | |||
87b45fff1d | |||
95ae50d12d | |||
95d6e1ecda | |||
479aa4a091 | |||
75efc595b4 | |||
d14cc785a8 | |||
379ecec789 | |||
ad00cf35ba | |||
5276f4186c | |||
8bc7a3daa2 | |||
e6957d566d | |||
71cf0a636e | |||
1200a809c2 | |||
03a6c5474b | |||
ada7daf179 | |||
4bdb487094 | |||
f8e4c89cdb | |||
7d4cdba0ef | |||
a78b5993a9 | |||
0e7109a19e | |||
ff3d5cd996 | |||
5f30a5a61b | |||
0712e6d64b | |||
a493ae08ce | |||
27349a6cc0 | |||
d17d4184be | |||
41d4fbb555 | |||
fccb5cbbab | |||
48d7ab430f | |||
28ac911f3f | |||
a793cc3967 | |||
09446844b0 | |||
f02788eaa5 | |||
614db4333c | |||
b79ab06d95 | |||
8494f0a8f1 | |||
65ed94eb08 | |||
51c088d998 | |||
2f233db332 | |||
1f82e84115 | |||
23c3bf11f9 | |||
5201a65a97 | |||
e4cc5fce50 | |||
8dea89db0e | |||
ad5ada9d55 | |||
7172a0e3fb | |||
78898968e8 | |||
666c64231f | |||
20d044f2e5 | |||
8caee32822 |
8
assets/icons/close_curtain.svg
Normal file
8
assets/icons/close_curtain.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="23" height="13" viewBox="0 0 23 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.24512 2.00263V11L1.90308 11.278L7.5311 6.94877C7.82484 6.72277 7.82484 6.27987 7.5311 6.05388L1.90308 1.72461L1.24512 2.00263Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M1.90344 1.7231L1.68312 1.55364C1.31186 1.2681 0.774414 1.53272 0.774414 2.00105V10.9984C0.774414 11.4668 1.31186 11.7315 1.68312 11.4459L1.90344 11.2764V1.7231Z" fill="#023DFE"/>
|
||||
<path d="M12.0646 0.855469H11.5001C11.1883 0.855469 10.9355 1.10819 10.9355 1.41998V11.5813H12.0646C12.3764 11.5813 12.6291 11.3285 12.6291 11.0167V1.41998C12.6291 1.10826 12.3764 0.855469 12.0646 0.855469Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M12.6291 11.0168C12.0056 11.0168 11.5001 10.5113 11.5001 9.88779V0.855469H10.9356C10.6238 0.855469 10.3711 1.10819 10.3711 1.41998V11.5813C10.3711 11.893 10.6238 12.1458 10.9356 12.1458H12.0646C12.3764 12.1458 12.6291 11.893 12.6291 11.5813V11.0168Z" fill="#023DFE"/>
|
||||
<path d="M21.4247 2.01953L16.1094 6.50343L21.4247 11.1061L22.226 10.7315V2.27062L21.4247 2.01953Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M17.3084 6.94723C17.0147 6.7213 17.0147 6.27833 17.3084 6.05233L22.2263 2.26933V2.00108C22.2263 1.53275 21.6889 1.26807 21.3176 1.55367L15.4693 6.05233C15.1756 6.27833 15.1756 6.7213 15.4693 6.94723L21.3176 11.4459C21.6889 11.7314 22.2263 11.4668 22.2263 10.9985V10.7302L17.3084 6.94723Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
8
assets/icons/open_curtain.svg
Normal file
8
assets/icons/open_curtain.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2227 1.27411V10.2715L15.8806 10.5495L21.5086 6.22025C21.8024 5.99426 21.8024 5.55136 21.5086 5.32536L15.8806 0.996094L15.2227 1.27411Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M15.881 0.994589L15.6607 0.825126C15.2894 0.539589 14.752 0.804208 14.752 1.27254V10.2699C14.752 10.7383 15.2894 11.0029 15.6607 10.7173L15.881 10.5479V0.994589Z" fill="#023DFE"/>
|
||||
<path d="M12.0646 0.128906H11.5001C11.1883 0.128906 10.9355 0.381631 10.9355 0.693418V10.8547H12.0646C12.3764 10.8547 12.6291 10.602 12.6291 10.2902V0.693418C12.6291 0.381699 12.3764 0.128906 12.0646 0.128906Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M12.6291 10.2903C12.0056 10.2903 11.5001 9.78474 11.5001 9.16123V0.128906H10.9356C10.6238 0.128906 10.3711 0.381631 10.3711 0.693418V10.8547C10.3711 11.1665 10.6238 11.4192 10.9356 11.4192H12.0646C12.3764 11.4192 12.6291 11.1665 12.6291 10.8547V10.2903Z" fill="#023DFE"/>
|
||||
<path d="M6.95005 1.29297L1.63477 5.77687L6.95005 10.3795L7.75136 10.005V1.54405L6.95005 1.29297Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M2.83379 6.21871C2.54005 5.99278 2.54005 5.54981 2.83379 5.32382L7.7517 1.54081V1.27257C7.7517 0.804238 7.21426 0.539551 6.843 0.825156L0.994719 5.32382C0.700979 5.54981 0.700979 5.99278 0.994719 6.21871L6.843 10.7174C7.21426 11.0029 7.7517 10.7383 7.7517 10.27V10.0017L2.83379 6.21871Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
6
assets/icons/pause_curtain.svg
Normal file
6
assets/icons/pause_curtain.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.81262 0.277344H8.24811C7.93632 0.277344 7.68359 0.530068 7.68359 0.841855V11.0031H8.81262C9.1244 11.0031 9.37713 10.7504 9.37713 10.4386V0.841855C9.37713 0.530136 9.1244 0.277344 8.81262 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M9.37719 10.4387C8.75361 10.4387 8.24816 9.93317 8.24816 9.30967V0.277344H7.68365C7.37187 0.277344 7.11914 0.530068 7.11914 0.841855V11.0031C7.11914 11.3149 7.37187 11.5676 7.68365 11.5676H8.81268C9.12446 11.5676 9.37719 11.3149 9.37719 11.0031V10.4387Z" fill="#023DFE"/>
|
||||
<path d="M2.5548 0.277344H1.99029C1.67851 0.277344 1.42578 0.530068 1.42578 0.841855V11.0031H2.5548C2.86659 11.0031 3.11932 10.7504 3.11932 10.4386V0.841855C3.11932 0.530136 2.86659 0.277344 2.5548 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
<path d="M3.11937 10.4387C2.4958 10.4387 1.99035 9.93317 1.99035 9.30967V0.277344H1.42584C1.11405 0.277344 0.861328 0.530068 0.861328 0.841855V11.0031C0.861328 11.3149 1.11405 11.5676 1.42584 11.5676H2.55486C2.86665 11.5676 3.11937 11.3149 3.11937 11.0031V10.4387Z" fill="#023DFE"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
10
assets/icons/reverse_arrows.svg
Normal file
10
assets/icons/reverse_arrows.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10119_2631)">
|
||||
<path d="M16.4229 10.9077V14.4803C16.4229 15.1238 15.9015 15.6453 15.2579 15.6453C14.6143 15.6453 14.0928 15.1238 14.0928 14.4803V14.3684C12.644 15.7134 10.7197 16.5 8.65572 16.5C5.42291 16.5 2.52657 14.573 1.27576 11.5914C1.21425 11.4446 1.18535 11.2917 1.18535 11.1417C1.18535 10.6854 1.45378 10.2539 1.89977 10.0661C2.49302 9.81722 3.17574 10.0959 3.4246 10.6901C4.31098 12.804 6.36475 14.1699 8.65572 14.1699C10.3973 14.1699 11.9999 13.3804 13.0578 12.0728H11.6849C11.0413 12.0728 10.5198 11.5513 10.5198 10.9077C10.5198 10.2641 11.0413 9.74265 11.6849 9.74265H15.2574C15.901 9.74265 16.4229 10.2641 16.4229 10.9077ZM5.31572 7.413C5.9593 7.413 6.48078 6.89105 6.48078 6.24794C6.48078 5.60436 5.9593 5.08288 5.31572 5.08288H4.13342C5.18897 3.68388 6.84661 2.83012 8.65572 2.83012C10.9472 2.83012 13.0005 4.1965 13.8873 6.31039C14.1357 6.90364 14.8184 7.18278 15.4117 6.93439C16.0049 6.68554 16.2841 6.00328 16.0357 5.40956C14.7844 2.42701 11.8881 0.5 8.65572 0.5C6.4421 0.5 4.38554 1.40455 2.90824 2.93218V2.67493C2.90824 2.03135 2.3863 1.50987 1.74318 1.50987C1.09961 1.50987 0.578125 2.03135 0.578125 2.67493V6.24794C0.578125 6.55691 0.701155 6.8533 0.919255 7.07234C1.13782 7.2909 1.43375 7.413 1.74318 7.413H5.31572Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10119_2631">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
10
lib/common/widgets/app_loading_indicator.dart
Normal file
10
lib/common/widgets/app_loading_indicator.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppLoadingIndicator extends StatelessWidget {
|
||||
const AppLoadingIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
@ -25,8 +25,8 @@ class AnalyticsDevice {
|
||||
|
||||
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,
|
||||
@ -39,8 +39,8 @@ class AnalyticsDevice {
|
||||
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
|
||||
: null,
|
||||
spaceUuid: json['spaceUuid'] as String?,
|
||||
latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
|
||||
longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
|
||||
latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null,
|
||||
longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,21 @@ class OccupancyHeatMapModel extends Equatable {
|
||||
});
|
||||
|
||||
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
|
||||
final eventDate = json['event_date'] as String?;
|
||||
final year = eventDate?.split('-')[0];
|
||||
final month = eventDate?.split('-')[1];
|
||||
final day = eventDate?.split('-')[2];
|
||||
|
||||
return OccupancyHeatMapModel(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
eventDate: DateTime.parse(
|
||||
json['event_date'] as String? ?? '${DateTime.now()}',
|
||||
eventDate: DateTime.utc(
|
||||
int.parse(year ?? '2025'),
|
||||
int.parse(month ?? '1'),
|
||||
int.parse(day ?? '1'),
|
||||
),
|
||||
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
|
||||
countTotalPresenceDetected: num.parse(
|
||||
json['count_total_presence_detected']?.toString() ?? '0',
|
||||
).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable {
|
||||
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
|
||||
return RangeOfAqiValue(
|
||||
type: json['type'] as String,
|
||||
min: (json['min'] as num).toDouble(),
|
||||
average: (json['average'] as num).toDouble(),
|
||||
max: (json['max'] as num).toDouble(),
|
||||
min: (json['min'] as num? ?? 0).toDouble(),
|
||||
average: (json['average'] as num? ?? 0).toDouble(),
|
||||
max: (json['max'] as num? ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
|
||||
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
|
||||
@ -22,13 +21,14 @@ abstract final class FetchAirQualityDataHelper {
|
||||
required String spaceUuid,
|
||||
bool shouldFetchAnalyticsDevices = true,
|
||||
}) {
|
||||
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
|
||||
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
if (shouldFetchAnalyticsDevices) {
|
||||
loadAnalyticsDevices(
|
||||
context,
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
);
|
||||
}
|
||||
loadRangeOfAqi(
|
||||
context,
|
||||
spaceUuid: spaceUuid,
|
||||
|
@ -18,11 +18,16 @@ abstract final class RangeOfAqiChartsHelper {
|
||||
(ColorsManager.hazardousPurple, 'Hazardous'),
|
||||
];
|
||||
|
||||
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
|
||||
static FlTitlesData titlesData(
|
||||
BuildContext context,
|
||||
List<RangeOfAqi> data, {
|
||||
double leftSideInterval = 50,
|
||||
}) {
|
||||
final titlesData = EnergyManagementChartsHelper.titlesData(context);
|
||||
return titlesData.copyWith(
|
||||
bottomTitles: titlesData.bottomTitles.copyWith(
|
||||
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
|
||||
reservedSize: 36,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
padding: const EdgeInsetsDirectional.only(top: 20.0),
|
||||
child: Text(
|
||||
@ -38,10 +43,11 @@ abstract final class RangeOfAqiChartsHelper {
|
||||
leftTitles: titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 50,
|
||||
interval: leftSideInterval,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final text = value >= 300 ? '301+' : value.toInt().toString();
|
||||
final text = value.toInt().toString();
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 12),
|
||||
child: FittedBox(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
|
||||
|
||||
class AirQualityView extends StatelessWidget {
|
||||
@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: height * 0.1,
|
||||
child: const AqiLegend(),
|
||||
),
|
||||
SizedBox(
|
||||
height: height * 1.2,
|
||||
child: const AirQualityEndSideWidget(),
|
||||
@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: _padding,
|
||||
height: height * 1.1,
|
||||
height: height * 1.2,
|
||||
child: const Column(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 20,
|
||||
children: [
|
||||
Expanded(child: RangeOfAqiChartBox()),
|
||||
Expanded(child: AqiDistributionChartBox()),
|
||||
Expanded(flex: 2, child: AqiLegend()),
|
||||
Expanded(flex: 12, child: RangeOfAqiChartBox()),
|
||||
Expanded(flex: 12, child: AqiDistributionChartBox()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
|
||||
);
|
||||
final tvocValue = _getValueForStatus(
|
||||
status,
|
||||
'tvoc_value',
|
||||
'voc_value',
|
||||
formatter: (value) => (value / 100).toStringAsFixed(2),
|
||||
);
|
||||
|
||||
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -32,8 +33,13 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
}
|
||||
|
||||
List<BarChartGroupData> _buildBarGroups() {
|
||||
return List.generate(chartData.length, (index) {
|
||||
final data = chartData[index];
|
||||
final groups = <BarChartGroupData>[];
|
||||
for (var i = 0; i < chartData.length; i++) {
|
||||
final data = chartData[i];
|
||||
final isAllZero = data.data.every((d) => d.percentage == 0);
|
||||
if (isAllZero) {
|
||||
continue;
|
||||
}
|
||||
final stackItems = <BarChartRodData>[];
|
||||
double currentY = 0;
|
||||
var isFirstElement = true;
|
||||
@ -56,13 +62,15 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
currentY += percentageData.percentage + _rodStackItemsSpacing;
|
||||
isFirstElement = false;
|
||||
}
|
||||
|
||||
return BarChartGroupData(
|
||||
x: index,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
groups.add(
|
||||
BarChartGroupData(
|
||||
x: i,
|
||||
barRods: stackItems,
|
||||
groupVertically: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
BarTouchData _barTouchData(BuildContext context) {
|
||||
@ -73,6 +81,7 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
color: ColorsManager.semiTransparentBlack,
|
||||
),
|
||||
tooltipRoundedRadius: 16,
|
||||
maxContentWidth: 500,
|
||||
tooltipPadding: const EdgeInsets.all(8),
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
||||
final data = chartData[group.x];
|
||||
@ -81,10 +90,13 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 8,
|
||||
fontSize: 11,
|
||||
);
|
||||
|
||||
for (final percentageData in data.data) {
|
||||
if (percentageData.percentage == 0) {
|
||||
continue;
|
||||
}
|
||||
final percentage = percentageData.percentage.toStringAsFixed(1);
|
||||
final type = percentageData.type[0].toUpperCase() +
|
||||
percentageData.type.substring(1).replaceAll('_', ' ');
|
||||
@ -98,7 +110,7 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
DateFormat('dd/MM/yyyy').format(data.date),
|
||||
context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 9,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.start,
|
||||
@ -118,7 +130,6 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
final leftTitles = titlesData.leftTitles.copyWith(
|
||||
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
|
||||
reservedSize: 70,
|
||||
interval: 20,
|
||||
maxIncluded: false,
|
||||
minIncluded: true,
|
||||
getTitlesWidget: (value, meta) => Padding(
|
||||
@ -139,8 +150,9 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
showTitles: chartData.isNotEmpty,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: BoxFit.scaleDown,
|
||||
@ -148,7 +160,7 @@ class AqiDistributionChart extends StatelessWidget {
|
||||
chartData[value.toInt()].date.day.toString(),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -19,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
|
||||
children: [
|
||||
ChartsLoadingWidget(isLoading: isLoading),
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
@ -28,23 +28,26 @@ class AqiDistributionChartTitle extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
final bloc = context.read<AirQualityDistributionBloc>();
|
||||
try {
|
||||
final param = _makeLoadAqiDistributionParam(context, value);
|
||||
bloc.add(LoadAirQualityDistribution(param));
|
||||
} catch (_) {
|
||||
return;
|
||||
} finally {
|
||||
bloc.add(UpdateAqiTypeEvent(value));
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: AqiTypeDropdown(
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
final bloc = context.read<AirQualityDistributionBloc>();
|
||||
try {
|
||||
final param = _makeLoadAqiDistributionParam(context, value);
|
||||
bloc.add(LoadAirQualityDistribution(param));
|
||||
} catch (_) {
|
||||
return;
|
||||
} finally {
|
||||
bloc.add(UpdateAqiTypeEvent(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class AqiLegend extends StatelessWidget {
|
||||
const AqiLegend({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(20),
|
||||
decoration: subSectionContainerDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: RangeOfAqiChartsHelper.gradientData.map((e) {
|
||||
return Flexible(
|
||||
flex: 4,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: ChartInformativeCell(
|
||||
color: e.$1,
|
||||
title: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Text(e.$2),
|
||||
),
|
||||
height: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: 120,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: SvgPicture.asset(
|
||||
svgPath,
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomEnd,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.all(10),
|
||||
child: Text(
|
||||
value,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.vividBlue.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SizedBox.square(
|
||||
dimension: MediaQuery.sizeOf(context).width * 0.45,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
child: SvgPicture.asset(svgPath),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -6,8 +6,8 @@ enum AqiType {
|
||||
aqi('AQI', '', 'aqi'),
|
||||
pm25('PM2.5', 'µg/m³', 'pm25'),
|
||||
pm10('PM10', 'µg/m³', 'pm10'),
|
||||
hcho('HCHO', 'mg/m³', 'cho2'),
|
||||
tvoc('TVOC', 'µg/m³', 'voc'),
|
||||
hcho('HCHO', 'mg/m³', 'ch2o'),
|
||||
tvoc('TVOC', 'mg/m³', 'voc'),
|
||||
co2('CO2', 'ppm', 'co2');
|
||||
|
||||
const AqiType(this.value, this.unit, this.code);
|
||||
|
@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class RangeOfAqiChart extends StatelessWidget {
|
||||
final List<RangeOfAqi> chartData;
|
||||
final AqiType selectedAqiType;
|
||||
|
||||
const RangeOfAqiChart({
|
||||
super.key,
|
||||
required this.chartData,
|
||||
required this.selectedAqiType,
|
||||
});
|
||||
|
||||
List<(List<double> values, Color color, Color? dotColor)> get _lines {
|
||||
@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
|
||||
(double maxY, double interval) get _maxYForAqiType {
|
||||
const aqiMaxValues = <AqiType, (double maxY, double interval)>{
|
||||
AqiType.aqi: (401, 100),
|
||||
AqiType.pm25: (351, 50),
|
||||
AqiType.pm10: (501, 100),
|
||||
AqiType.hcho: (301, 50),
|
||||
AqiType.tvoc: (501, 50),
|
||||
AqiType.co2: (1251, 250),
|
||||
};
|
||||
|
||||
return aqiMaxValues[selectedAqiType]!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LineChart(
|
||||
LineChartData(
|
||||
minY: 0,
|
||||
maxY: 301,
|
||||
maxY: _maxYForAqiType.$1,
|
||||
clipData: const FlClipData.vertical(),
|
||||
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
|
||||
gridData: EnergyManagementChartsHelper.gridData(
|
||||
horizontalInterval: _maxYForAqiType.$2,
|
||||
),
|
||||
titlesData: RangeOfAqiChartsHelper.titlesData(
|
||||
context,
|
||||
chartData,
|
||||
leftSideInterval: _maxYForAqiType.$2,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
|
||||
betweenBarsData: [
|
||||
|
@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
|
||||
Expanded(
|
||||
child: RangeOfAqiChart(
|
||||
chartData: state.filteredRangeOfAqi,
|
||||
selectedAqiType: state.selectedAqiType,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget {
|
||||
required this.title,
|
||||
required this.color,
|
||||
this.hasBorder = false,
|
||||
this.height,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Color color;
|
||||
final bool hasBorder;
|
||||
final double? height;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: MediaQuery.sizeOf(context).height * 0.0385,
|
||||
height: height ?? MediaQuery.sizeOf(context).height * 0.0385,
|
||||
padding: const EdgeInsetsDirectional.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper {
|
||||
return FlTitlesData(
|
||||
show: true,
|
||||
bottomTitles: AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
drawBelowEverything: true,
|
||||
sideTitles: SideTitles(
|
||||
interval: 1,
|
||||
@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper {
|
||||
);
|
||||
}
|
||||
|
||||
static String getToolTipLabel(num month, double value) {
|
||||
final monthLabel = month.toString();
|
||||
final valueLabel = value.formatNumberToKwh;
|
||||
final labels = [monthLabel, valueLabel];
|
||||
return labels.where((element) => element.isNotEmpty).join(', ');
|
||||
}
|
||||
static String getToolTipLabel(double value) => value.formatNumberToKwh;
|
||||
|
||||
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
|
||||
return touchedSpots.map((spot) {
|
||||
return LineTooltipItem(
|
||||
getToolTipLabel(spot.x, spot.y),
|
||||
getToolTipLabel(spot.y),
|
||||
const TextStyle(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
visible: state.status != AnalyticsDevicesStatus.loading,
|
||||
replacement: _buildLoadingIndicator(),
|
||||
child: Visibility(
|
||||
visible: state.devices.isNotEmpty,
|
||||
replacement: _buildNoDevicesFound(context),
|
||||
child: _buildDevicesDropdown(context, state),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return const Center(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 2,
|
||||
|
@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ChartTitle(
|
||||
title: Text('Energy Consumption per Device'),
|
||||
title: Text('Device energy consumed'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: LineChart(
|
||||
LineChartData(
|
||||
maxY: chartData.isEmpty
|
||||
? null
|
||||
: chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250,
|
||||
clipData: const FlClipData.vertical(),
|
||||
titlesData: EnergyManagementChartsHelper.titlesData(
|
||||
context,
|
||||
leftTitlesInterval: 250,
|
||||
leftTitlesInterval: 500,
|
||||
),
|
||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||
checkToShowHorizontalLine: (value) => true,
|
||||
horizontalInterval: 250,
|
||||
horizontalInterval: 500,
|
||||
),
|
||||
borderData: EnergyManagementChartsHelper.borderData(),
|
||||
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
|
||||
@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
|
||||
),
|
||||
duration: Duration.zero,
|
||||
curve: Curves.easeIn,
|
||||
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
|
||||
child: FittedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ChartTitle(title: Text('Total Energy Consumption')),
|
||||
child: ChartTitle(title: Text('Space energy consumed')),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 4),
|
||||
|
@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper {
|
||||
param: GetAnalyticsDevicesParam(
|
||||
communityUuid: communityUuid,
|
||||
spaceUuid: spaceUuid,
|
||||
deviceTypes: ['WPS', 'CPS'],
|
||||
deviceTypes: ['WPS', 'CPS', 'NCPS'],
|
||||
requestType: AnalyticsDeviceRequestType.occupancy,
|
||||
),
|
||||
onSuccess: (device) {
|
||||
|
@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
|
||||
child: Column(
|
||||
spacing: 32,
|
||||
children: [
|
||||
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
|
||||
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
|
||||
],
|
||||
|
@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 2, thickness: 1),
|
||||
Text(
|
||||
'$value Occupants',
|
||||
'Occupancy detected: $value',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||
color: Colors.transparent,
|
||||
child: Transform.translate(
|
||||
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||
child: HeatMapTooltip(date: item.date.toUtc(), value: item.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
|
||||
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
|
||||
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget {
|
||||
}) {
|
||||
final data = chartData;
|
||||
|
||||
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
|
||||
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
|
||||
final occupancyValue = double.parse(data[group.x].occupancy);
|
||||
final percentage = '${occupancyValue.toStringAsFixed(0)}%';
|
||||
|
||||
return BarTooltipItem(
|
||||
percentage,
|
||||
@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'${(value).toStringAsFixed(0)}%',
|
||||
'${value.toStringAsFixed(0)}%',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.greyColor,
|
||||
@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget {
|
||||
);
|
||||
|
||||
final bottomTitles = AxisTitles(
|
||||
axisNameWidget: const ChartsXAxisTitle(),
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, _) => FittedBox(
|
||||
|
@ -23,10 +23,9 @@ class OccupancyEndSideBar extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
|
||||
const AnalyticsSidebarHeader(title: 'Presence Sensor'),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
// height: MediaQuery.sizeOf(context).height * 0.2,
|
||||
child: PowerClampEnergyStatusWidget(
|
||||
status: [
|
||||
PowerClampEnergyStatus(
|
||||
|
@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class OccupancyHeatMap extends StatelessWidget {
|
||||
const OccupancyHeatMap({required this.heatMapData, super.key});
|
||||
const OccupancyHeatMap({
|
||||
required this.heatMapData,
|
||||
required this.selectedDate,
|
||||
super.key,
|
||||
});
|
||||
final Map<DateTime, int> heatMapData;
|
||||
final DateTime selectedDate;
|
||||
|
||||
static const _cellSize = 16.0;
|
||||
static const _totalWeeks = 53;
|
||||
@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget {
|
||||
: 0;
|
||||
|
||||
DateTime _getStartingDate() {
|
||||
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
||||
final jan1 = DateTime.utc(selectedDate.year, 1, 1);
|
||||
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
|
||||
return startOfWeek;
|
||||
}
|
||||
|
@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: OccupancyHeatMap(
|
||||
selectedDate:
|
||||
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
|
||||
heatMapData: state.heatMapData.asMap().map(
|
||||
(_, value) => MapEntry(
|
||||
value.eventDate,
|
||||
|
23
lib/pages/analytics/widgets/charts_x_axis_title.dart
Normal file
23
lib/pages/analytics/widgets/charts_x_axis_title.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ChartsXAxisTitle extends StatelessWidget {
|
||||
const ChartsXAxisTitle({
|
||||
this.label = 'Day of month',
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
label,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -36,7 +36,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
|
||||
////////////////////////////// forget password //////////////////////////////////
|
||||
final TextEditingController forgetEmailController = TextEditingController();
|
||||
final TextEditingController forgetPasswordController = TextEditingController();
|
||||
final TextEditingController forgetPasswordController =
|
||||
TextEditingController();
|
||||
final TextEditingController forgetOtp = TextEditingController();
|
||||
final forgetFormKey = GlobalKey<FormState>();
|
||||
final forgetEmailKey = GlobalKey<FormState>();
|
||||
@ -53,7 +54,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
return;
|
||||
}
|
||||
_remainingTime = 1;
|
||||
add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
|
||||
add(UpdateTimerEvent(
|
||||
remainingTime: _remainingTime, isButtonEnabled: false));
|
||||
try {
|
||||
forgetEmailValidate = '';
|
||||
_remainingTime = (await AuthenticationAPI.sendOtp(
|
||||
@ -90,7 +92,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
_timer?.cancel();
|
||||
add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true));
|
||||
} else {
|
||||
add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
|
||||
add(UpdateTimerEvent(
|
||||
remainingTime: _remainingTime, isButtonEnabled: false));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -100,7 +103,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
|
||||
}
|
||||
|
||||
Future<void> changePassword(
|
||||
Future<void> changePassword(
|
||||
ChangePasswordEvent event, Emitter<AuthState> emit) async {
|
||||
emit(LoadingForgetState());
|
||||
try {
|
||||
@ -122,7 +125,6 @@ Future<void> changePassword(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String? validateCode(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Code is required';
|
||||
@ -131,7 +133,9 @@ Future<void> changePassword(
|
||||
}
|
||||
|
||||
void _onUpdateTimer(UpdateTimerEvent event, Emitter<AuthState> emit) {
|
||||
emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime));
|
||||
emit(TimerState(
|
||||
isButtonEnabled: event.isButtonEnabled,
|
||||
remainingTime: event.remainingTime));
|
||||
}
|
||||
|
||||
///////////////////////////////////// login /////////////////////////////////////
|
||||
@ -151,7 +155,6 @@ Future<void> changePassword(
|
||||
static UserModel? user;
|
||||
bool showValidationMessage = false;
|
||||
|
||||
|
||||
void _login(LoginButtonPressed event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
if (isChecked) {
|
||||
@ -170,11 +173,11 @@ Future<void> changePassword(
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
validate = e.message;
|
||||
emit(LoginInitial());
|
||||
emit(LoginFailure(error: validate));
|
||||
return;
|
||||
} catch (e) {
|
||||
validate = 'Something went wrong';
|
||||
emit(LoginInitial());
|
||||
emit(LoginFailure(error: validate));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -197,7 +200,6 @@ Future<void> changePassword(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
checkBoxToggle(
|
||||
CheckBoxEvent event,
|
||||
Emitter<AuthState> emit,
|
||||
@ -339,12 +341,14 @@ Future<void> changePassword(
|
||||
static Future<String> getTokenAndValidate() async {
|
||||
try {
|
||||
const storage = FlutterSecureStorage();
|
||||
final firstLaunch =
|
||||
await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
|
||||
final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(
|
||||
StringsManager.firstLaunch) ??
|
||||
true;
|
||||
if (firstLaunch) {
|
||||
storage.deleteAll();
|
||||
}
|
||||
await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false);
|
||||
await SharedPreferencesHelper.saveBoolToSP(
|
||||
StringsManager.firstLaunch, false);
|
||||
final value = await storage.read(key: Token.loginAccessTokenKey) ?? '';
|
||||
if (value.isEmpty) {
|
||||
return 'Token not found';
|
||||
@ -397,7 +401,9 @@ Future<void> changePassword(
|
||||
final String formattedTime = [
|
||||
if (days > 0) '${days}d', // Append 'd' for days
|
||||
if (days > 0 || hours > 0)
|
||||
hours.toString().padLeft(2, '0'), // Show hours if there are days or hours
|
||||
hours
|
||||
.toString()
|
||||
.padLeft(2, '0'), // Show hours if there are days or hours
|
||||
minutes.toString().padLeft(2, '0'),
|
||||
seconds.toString().padLeft(2, '0'),
|
||||
].join(':');
|
||||
|
@ -179,31 +179,36 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SvgPicture.asset(Assets.emptyTable),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
widget.tableName == 'AccessManagement'
|
||||
? 'No Password '
|
||||
: 'No Devices',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.grayColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
Widget _buildEmptyState() => Container(
|
||||
height: widget.size.height,
|
||||
color: ColorsManager.whiteColors,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SvgPicture.asset(Assets.emptyTable),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
widget.tableName == 'AccessManagement'
|
||||
? 'No Password '
|
||||
: 'No Devices',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: ColorsManager.grayColor),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: widget.size.height * 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget _buildSelectAllCheckbox() {
|
||||
return Container(
|
||||
|
@ -68,24 +68,30 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToChanges(deviceId) {
|
||||
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
final stream = ref.onValue;
|
||||
|
||||
stream.listen((DatabaseEvent event) async {
|
||||
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||
if (event.snapshot.value == null) return;
|
||||
|
||||
Map<dynamic, dynamic> usersMap =
|
||||
event.snapshot.value as Map<dynamic, dynamic>;
|
||||
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
final statusList = <Status>[];
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
|
||||
deviceStatus =
|
||||
AcStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
|
||||
deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
print('Device status updated: ${deviceStatus.acSwitch}');
|
||||
|
||||
|
||||
if (!isClosed) {
|
||||
add(AcStatusUpdated(deviceStatus));
|
||||
}
|
||||
@ -105,22 +111,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
AcControlEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
_updateDeviceFunctionFromCode(event.code, event.value);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
|
||||
try {
|
||||
final success = await controlDeviceService.controlDevice(
|
||||
_updateDeviceFunctionFromCode(event.code, event.value);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: event.code, value: event.value),
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
emit(const AcsFailedState(error: 'Failed to control device'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
FutureOr<void> _onFetchAcBatchStatus(
|
||||
@ -141,23 +139,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
AcBatchControlEvent event,
|
||||
Emitter<AcsState> emit,
|
||||
) async {
|
||||
emit(AcsLoadingState());
|
||||
_updateDeviceFunctionFromCode(event.code, event.value);
|
||||
emit(ACStatusLoaded(status: deviceStatus));
|
||||
|
||||
try {
|
||||
final success = await batchControlDevicesService.batchControlDevices(
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesIds,
|
||||
code: event.code,
|
||||
value: event.value,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
emit(const AcsFailedState(error: 'Failed to control devices'));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AcsFailedState(error: e.toString()));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
Future<void> _onFactoryReset(
|
||||
@ -190,8 +181,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter<AcsState> emit) {
|
||||
if (state is! ACStatusLoaded) return;
|
||||
final currentState = state as ACStatusLoaded;
|
||||
int newHours = scheduledHours;
|
||||
int newMinutes = scheduledMinutes + 30;
|
||||
var newHours = scheduledHours;
|
||||
var newMinutes = scheduledMinutes + 30;
|
||||
newHours += newMinutes ~/ 60;
|
||||
newMinutes = newMinutes % 60;
|
||||
if (newHours > 23) {
|
||||
@ -213,7 +204,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
) {
|
||||
if (state is! ACStatusLoaded) return;
|
||||
final currentState = state as ACStatusLoaded;
|
||||
int totalMinutes = (scheduledHours * 60) + scheduledMinutes;
|
||||
var totalMinutes = (scheduledHours * 60) + scheduledMinutes;
|
||||
totalMinutes = (totalMinutes - 30).clamp(0, 1440);
|
||||
scheduledHours = totalMinutes ~/ 60;
|
||||
scheduledMinutes = totalMinutes % 60;
|
||||
@ -286,20 +277,24 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
|
||||
void _startCountdownTimer(Emitter<AcsState> emit) {
|
||||
_countdownTimer?.cancel();
|
||||
int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
|
||||
var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
|
||||
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (totalSeconds > 0) {
|
||||
totalSeconds--;
|
||||
scheduledHours = totalSeconds ~/ 3600;
|
||||
scheduledMinutes = (totalSeconds % 3600) ~/ 60;
|
||||
add(UpdateTimerEvent());
|
||||
if (!isClosed) {
|
||||
add(UpdateTimerEvent());
|
||||
}
|
||||
} else {
|
||||
_countdownTimer?.cancel();
|
||||
timerActive = false;
|
||||
scheduledHours = 0;
|
||||
scheduledMinutes = 0;
|
||||
add(TimerCompletedEvent());
|
||||
if (!isClosed) {
|
||||
add(TimerCompletedEvent());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -326,7 +321,9 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
_startCountdownTimer(
|
||||
emit,
|
||||
);
|
||||
add(UpdateTimerEvent());
|
||||
if (!isClosed) {
|
||||
add(UpdateTimerEvent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -370,6 +367,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
|
||||
@override
|
||||
Future<void> close() {
|
||||
add(OnClose());
|
||||
_countdownTimer?.cancel();
|
||||
_deviceStatusSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
@ -40,17 +40,18 @@ class DeviceManagementBloc
|
||||
List<AllDevicesModel> devices = [];
|
||||
_devices.clear();
|
||||
var spaceBloc = event.context.read<SpaceTreeBloc>();
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
|
||||
|
||||
if (spaceBloc.state.selectedCommunities.isEmpty) {
|
||||
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
|
||||
devices =
|
||||
await DevicesManagementApi().fetchDevices('', '', projectUuid);
|
||||
} else {
|
||||
for (var community in spaceBloc.state.selectedCommunities) {
|
||||
List<String> spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
|
||||
for (var space in spacesList) {
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(
|
||||
community, space, projectUuid));
|
||||
devices.addAll(await DevicesManagementApi()
|
||||
.fetchDevices(community, space, projectUuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -100,7 +101,7 @@ class DeviceManagementBloc
|
||||
));
|
||||
|
||||
if (currentProductName.isNotEmpty) {
|
||||
add(SearchDevices(productName: currentProductName));
|
||||
add(SearchDevices(deviceNameOrProductName: currentProductName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,34 +270,41 @@ class DeviceManagementBloc
|
||||
return 'All';
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchDevices(
|
||||
SearchDevices event, Emitter<DeviceManagementState> emit) {
|
||||
if ((event.community == null || event.community!.isEmpty) &&
|
||||
(event.unitName == null || event.unitName!.isEmpty) &&
|
||||
(event.productName == null || event.productName!.isEmpty)) {
|
||||
(event.deviceNameOrProductName == null ||
|
||||
event.deviceNameOrProductName!.isEmpty)) {
|
||||
currentProductName = '';
|
||||
if (state is DeviceManagementFiltered) {
|
||||
add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
_filteredDevices = List.from(_devices);
|
||||
emit(DeviceManagementLoaded(
|
||||
devices: _devices,
|
||||
selectedIndex: _selectedIndex,
|
||||
onlineCount: _onlineCount,
|
||||
offlineCount: _offlineCount,
|
||||
lowBatteryCount: _lowBatteryCount,
|
||||
selectedDevice: null,
|
||||
isControlButtonEnabled: false,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.productName == currentProductName &&
|
||||
if (event.deviceNameOrProductName == currentProductName &&
|
||||
event.community == currentCommunity &&
|
||||
event.unitName == currentUnitName &&
|
||||
event.searchField) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentProductName = event.productName ?? '';
|
||||
currentProductName = event.deviceNameOrProductName ?? '';
|
||||
currentCommunity = event.community;
|
||||
currentUnitName = event.unitName;
|
||||
|
||||
List<AllDevicesModel> devicesToSearch = _filteredDevices;
|
||||
List<AllDevicesModel> devicesToSearch = _devices;
|
||||
|
||||
if (devicesToSearch.isNotEmpty) {
|
||||
final searchText = event.deviceNameOrProductName?.toLowerCase() ?? '';
|
||||
|
||||
final filteredDevices = devicesToSearch.where((device) {
|
||||
final matchesCommunity = event.community == null ||
|
||||
event.community!.isEmpty ||
|
||||
@ -304,31 +312,25 @@ class DeviceManagementBloc
|
||||
?.toLowerCase()
|
||||
.contains(event.community!.toLowerCase()) ??
|
||||
false);
|
||||
|
||||
final matchesUnit = event.unitName == null ||
|
||||
event.unitName!.isEmpty ||
|
||||
(device.spaces != null &&
|
||||
device.spaces!.isNotEmpty &&
|
||||
device.spaces![0].spaceName!
|
||||
.toLowerCase()
|
||||
.contains(event.unitName!.toLowerCase()));
|
||||
final matchesProductName = event.productName == null ||
|
||||
event.productName!.isEmpty ||
|
||||
(device.name
|
||||
?.toLowerCase()
|
||||
.contains(event.productName!.toLowerCase()) ??
|
||||
false);
|
||||
final matchesDeviceName = event.productName == null ||
|
||||
event.productName!.isEmpty ||
|
||||
(device.categoryName
|
||||
?.toLowerCase()
|
||||
.contains(event.productName!.toLowerCase()) ??
|
||||
false);
|
||||
device.spaces!.any((space) =>
|
||||
space.spaceName != null &&
|
||||
space.spaceName!
|
||||
.toLowerCase()
|
||||
.contains(event.unitName!.toLowerCase())));
|
||||
|
||||
return matchesCommunity &&
|
||||
matchesUnit &&
|
||||
(matchesProductName || matchesDeviceName);
|
||||
final matchesSearchText = searchText.isEmpty ||
|
||||
(device.name?.toLowerCase().contains(searchText) ?? false) ||
|
||||
(device.productName?.toLowerCase().contains(searchText) ?? false);
|
||||
|
||||
return matchesCommunity && matchesUnit && matchesSearchText;
|
||||
}).toList();
|
||||
|
||||
_filteredDevices = filteredDevices;
|
||||
|
||||
emit(DeviceManagementFiltered(
|
||||
filteredDevices: filteredDevices,
|
||||
selectedIndex: _selectedIndex,
|
||||
|
@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent {
|
||||
class SearchDevices extends DeviceManagementEvent {
|
||||
final String? community;
|
||||
final String? unitName;
|
||||
final String? productName;
|
||||
final String? deviceNameOrProductName;
|
||||
final bool searchField;
|
||||
|
||||
const SearchDevices({
|
||||
this.community,
|
||||
this.unitName,
|
||||
this.productName,
|
||||
this.deviceNameOrProductName,
|
||||
this.searchField = false,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [community, unitName, productName];
|
||||
List<Object?> get props => [community, unitName, deviceNameOrProductName];
|
||||
}
|
||||
|
||||
class SelectDevice extends DeviceManagementEvent {
|
||||
|
@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s
|
||||
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart';
|
||||
@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar
|
||||
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart';
|
||||
@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate
|
||||
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart';
|
||||
|
||||
import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
|
||||
|
||||
mixin RouteControlsBasedCode {
|
||||
Widget routeControlsWidgets({required AllDevicesModel device}) {
|
||||
switch (device.productType) {
|
||||
@ -84,6 +85,10 @@ mixin RouteControlsBasedCode {
|
||||
return CurtainStatusControlsView(
|
||||
deviceId: device.uuid!,
|
||||
);
|
||||
case 'CUR_2':
|
||||
return CurtainModuleItems(
|
||||
deviceId: device.uuid!,
|
||||
);
|
||||
case 'AC':
|
||||
return AcDeviceControlsView(device: device);
|
||||
case 'WH':
|
||||
@ -107,7 +112,7 @@ mixin RouteControlsBasedCode {
|
||||
case 'SOS':
|
||||
return SosDeviceControlsView(device: device);
|
||||
|
||||
case 'NCPS':
|
||||
case 'NCPS':
|
||||
return FlushMountedPresenceSensorControlView(device: device);
|
||||
default:
|
||||
return const SizedBox();
|
||||
@ -132,76 +137,140 @@ mixin RouteControlsBasedCode {
|
||||
switch (devices.first.productType) {
|
||||
case '1G':
|
||||
return WallLightBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '1G')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case '2G':
|
||||
return TwoGangBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '2G')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case '3G':
|
||||
return LivingRoomBatchControlsView(
|
||||
deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '3G')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case '1GT':
|
||||
return OneGangGlassSwitchBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '1GT')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case '2GT':
|
||||
return TwoGangGlassSwitchBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '2GT')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case '3GT':
|
||||
return ThreeGangGlassSwitchBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == '3GT')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'GW':
|
||||
return GatewayBatchControlView(
|
||||
gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(),
|
||||
gatewayIds: devices
|
||||
.where((e) => e.productType == 'GW')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'DL':
|
||||
return DoorLockBatchControlView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList());
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'DL')
|
||||
.map((e) => e.uuid!)
|
||||
.toList());
|
||||
case 'WPS':
|
||||
return WallSensorBatchControlView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList());
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'WPS')
|
||||
.map((e) => e.uuid!)
|
||||
.toList());
|
||||
case 'CPS':
|
||||
return CeilingSensorBatchControlView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(),
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'CPS')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'CUR':
|
||||
return CurtainBatchStatusView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(),
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'CUR')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'CUR_2':
|
||||
return CurtainModuleBatchView(
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'CUR_2')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'AC':
|
||||
return AcDeviceBatchControlView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList());
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'AC')
|
||||
.map((e) => e.uuid!)
|
||||
.toList());
|
||||
case 'WH':
|
||||
return WaterHEaterBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == 'WH')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'DS':
|
||||
return MainDoorSensorBatchView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(),
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'DS')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'GD':
|
||||
return GarageDoorBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == 'GD')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'WL':
|
||||
return WaterLeakBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == 'WL')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'PC':
|
||||
return PowerClampBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == 'PC')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'SOS':
|
||||
return SOSBatchControlView(
|
||||
deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(),
|
||||
deviceIds: devices
|
||||
.where((e) => e.productType == 'SOS')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
case 'NCPS':
|
||||
return FlushMountedPresenceSensorBatchControlView(
|
||||
devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(),
|
||||
devicesIds: devices
|
||||
.where((e) => e.productType == 'NCPS')
|
||||
.map((e) => e.uuid!)
|
||||
.toList(),
|
||||
);
|
||||
default:
|
||||
return const SizedBox();
|
||||
|
@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
|
||||
final buttonLabel =
|
||||
(selectedDevices.length > 1) ? 'Batch Control' : 'Control';
|
||||
|
||||
final isAnyDeviceOffline =
|
||||
selectedDevices.any((element) => !(element.online ?? false));
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: SpaceTreeView(
|
||||
@ -103,8 +104,26 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
||||
decoration: containerDecoration,
|
||||
child: Center(
|
||||
child: DefaultButton(
|
||||
backgroundColor: isAnyDeviceOffline
|
||||
? ColorsManager.primaryColor
|
||||
.withValues(alpha: 0.1)
|
||||
: null,
|
||||
onPressed: isControlButtonEnabled
|
||||
? () {
|
||||
if (isAnyDeviceOffline) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'This Device is Offline',
|
||||
),
|
||||
duration:
|
||||
Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedDevices.length == 1) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
|
||||
controller: controller,
|
||||
onSubmitted: () {
|
||||
final searchDevicesEvent = SearchDevices(
|
||||
productName: _productNameController.text,
|
||||
deviceNameOrProductName: _productNameController.text,
|
||||
unitName: _unitNameController.text,
|
||||
searchField: true,
|
||||
);
|
||||
@ -68,7 +68,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
|
||||
onSearch: () => context.read<DeviceManagementBloc>().add(
|
||||
SearchDevices(
|
||||
unitName: _unitNameController.text,
|
||||
productName: _productNameController.text,
|
||||
deviceNameOrProductName: _productNameController.text,
|
||||
searchField: true,
|
||||
),
|
||||
),
|
||||
|
@ -0,0 +1,379 @@
|
||||
import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/services/devices_mang_api.dart';
|
||||
|
||||
part 'curtain_module_event.dart';
|
||||
part 'curtain_module_state.dart';
|
||||
|
||||
class CurtainModuleBloc extends Bloc<CurtainModuleEvent, CurtainModuleState> {
|
||||
final ControlDeviceService controlDeviceService;
|
||||
final BatchControlDevicesService batchControlDevicesService;
|
||||
StreamSubscription<DatabaseEvent>? _firebaseSubscription;
|
||||
|
||||
CurtainModuleBloc({
|
||||
required this.controlDeviceService,
|
||||
required this.batchControlDevicesService,
|
||||
}) : super(CurtainModuleInitial()) {
|
||||
on<FetchCurtainModuleStatusEvent>(_onFetchCurtainModuleStatusEvent);
|
||||
on<SendCurtainPercentToApiEvent>(_onSendCurtainPercentToApiEvent);
|
||||
on<OpenCurtainEvent>(_onOpenCurtainEvent);
|
||||
on<CloseCurtainEvent>(_onCloseCurtainEvent);
|
||||
on<StopCurtainEvent>(_onStopCurtainEvent);
|
||||
on<ChangeTimerControlEvent>(_onChangeTimerControlEvent);
|
||||
on<CurCalibrationEvent>(_onChageCurCalibrationEvent);
|
||||
on<ChangeElecMachineryModeEvent>(_onChangeElecMachineryModeEvent);
|
||||
on<ChangeControlBackEvent>(_onChangeControlBackEvent);
|
||||
on<ChangeControlBackModeEvent>(_onChangeControlBackModeEvent);
|
||||
on<ChangeCurtainModuleStatusEvent>(_onChangeCurtainModuleStatusEvent);
|
||||
//batch
|
||||
on<CurtainModuleFetchBatchStatusEvent>(_onFetchCurtainModuleBatchStatus);
|
||||
on<SendCurtainBatchPercentToApiEvent>(_onSendCurtainBatchPercentToApiEvent);
|
||||
on<OpenCurtainBatchEvent>(_onOpenCurtainBatchEvent);
|
||||
on<CloseCurtainBatchEvent>(_onCloseCurtainBatchEvent);
|
||||
on<StopCurtainBatchEvent>(_onStopCurtainBatchEvent);
|
||||
on<CurtainModuleFactoryReset>(_onFactoryReset);
|
||||
}
|
||||
|
||||
Future<void> _onFetchCurtainModuleStatusEvent(
|
||||
FetchCurtainModuleStatusEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
emit(CurtainModuleLoading());
|
||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||
final result = Map.fromEntries(
|
||||
status.status.map((element) => MapEntry(element.code, element.value)),
|
||||
);
|
||||
|
||||
emit(CurtainModuleStatusLoaded(
|
||||
curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
|
||||
));
|
||||
Map<String, dynamic> statusMap = {};
|
||||
final ref =
|
||||
FirebaseDatabase.instance.ref('device-status/${event.deviceId}');
|
||||
final stream = ref.onValue;
|
||||
|
||||
stream.listen((DatabaseEvent DatabaseEvent) async {
|
||||
if (DatabaseEvent.snapshot.value == null) return;
|
||||
|
||||
Map<dynamic, dynamic> usersMap =
|
||||
DatabaseEvent.snapshot.value as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
statusMap = {
|
||||
for (final element in statusList) element.code: element.value,
|
||||
};
|
||||
if (!isClosed) {
|
||||
add(
|
||||
ChangeCurtainModuleStatusEvent(
|
||||
deviceId: event.deviceId,
|
||||
status: CurtainModuleStatusModel.fromJson(statusMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onChangeCurtainModuleStatusEvent(
|
||||
ChangeCurtainModuleStatusEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
emit(CurtainModuleLoading());
|
||||
emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status));
|
||||
}
|
||||
|
||||
Future<void> _onSendCurtainPercentToApiEvent(
|
||||
SendCurtainPercentToApiEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: event.status,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to send control command: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOpenCurtainEvent(
|
||||
OpenCurtainEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: 'control', value: 'open'),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCloseCurtainEvent(
|
||||
CloseCurtainEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: 'control', value: 'close'),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStopCurtainEvent(
|
||||
StopCurtainEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: 'control', value: 'stop'),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangeTimerControlEvent(
|
||||
ChangeTimerControlEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
if (event.timControl < 10 || event.timControl > 120) {
|
||||
emit(const CurtainModuleError(
|
||||
message: 'Timer control value must be between 10 and 120'));
|
||||
return;
|
||||
}
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(
|
||||
code: 'tr_timecon',
|
||||
value: event.timControl,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to change timer control: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChageCurCalibrationEvent(
|
||||
CurCalibrationEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(code: 'cur_calibration', value: 'start'),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to start calibration: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangeElecMachineryModeEvent(
|
||||
ChangeElecMachineryModeEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(
|
||||
code: 'elec_machinery_mode',
|
||||
value: event.elecMachineryMode,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to change mode: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangeControlBackEvent(
|
||||
ChangeControlBackEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(
|
||||
code: 'control_back',
|
||||
value: event.controlBack,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to change control back: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onChangeControlBackModeEvent(
|
||||
ChangeControlBackModeEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await controlDeviceService.controlDevice(
|
||||
deviceUuid: event.deviceId,
|
||||
status: Status(
|
||||
code: 'control_back_mode',
|
||||
value: event.controlBackMode,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(
|
||||
message: 'Failed to change control back mode: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onFetchCurtainModuleBatchStatus(
|
||||
CurtainModuleFetchBatchStatusEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
emit(CurtainModuleLoading());
|
||||
try {
|
||||
final status =
|
||||
await DevicesManagementApi().getBatchStatus(event.devicesIds);
|
||||
|
||||
final result = Map.fromEntries(
|
||||
status.status.map((element) => MapEntry(element.code, element.value)),
|
||||
);
|
||||
|
||||
emit(CurtainModuleStatusLoaded(
|
||||
curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
|
||||
));
|
||||
|
||||
Map<String, dynamic> statusMap = {};
|
||||
final ref = FirebaseDatabase.instance
|
||||
.ref('device-status/${event.devicesIds.first}');
|
||||
final stream = ref.onValue;
|
||||
|
||||
stream.listen((DatabaseEvent DatabaseEvent) async {
|
||||
if (DatabaseEvent.snapshot.value == null) return;
|
||||
|
||||
Map<dynamic, dynamic> usersMap =
|
||||
DatabaseEvent.snapshot.value as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList
|
||||
.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
statusMap = {
|
||||
for (final element in statusList) element.code: element.value,
|
||||
};
|
||||
if (!isClosed) {
|
||||
add(
|
||||
ChangeCurtainModuleStatusEvent(
|
||||
deviceId: event.devicesIds.first,
|
||||
status: CurtainModuleStatusModel.fromJson(statusMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSendCurtainBatchPercentToApiEvent(
|
||||
SendCurtainBatchPercentToApiEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesId,
|
||||
code: event.status.code,
|
||||
value: event.status.value,
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to send control command: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOpenCurtainBatchEvent(
|
||||
OpenCurtainBatchEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesId,
|
||||
code: 'control',
|
||||
value: 'open',
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCloseCurtainBatchEvent(
|
||||
CloseCurtainBatchEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesId,
|
||||
code: 'control',
|
||||
value: 'close',
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStopCurtainBatchEvent(
|
||||
StopCurtainBatchEvent event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
try {
|
||||
await batchControlDevicesService.batchControlDevices(
|
||||
uuids: event.devicesId,
|
||||
code: 'control',
|
||||
value: 'stop',
|
||||
);
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onFactoryReset(
|
||||
CurtainModuleFactoryReset event,
|
||||
Emitter<CurtainModuleState> emit,
|
||||
) async {
|
||||
emit(CurtainModuleLoading());
|
||||
try {
|
||||
final response = await DevicesManagementApi().factoryReset(
|
||||
event.factoryReset,
|
||||
event.deviceId,
|
||||
);
|
||||
if (!response) {
|
||||
emit(const CurtainModuleError(message: 'Failed'));
|
||||
} else {
|
||||
add(
|
||||
FetchCurtainModuleStatusEvent(deviceId: event.deviceId),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(CurtainModuleError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _firebaseSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,193 @@
|
||||
part of 'curtain_module_bloc.dart';
|
||||
|
||||
sealed class CurtainModuleEvent extends Equatable {
|
||||
const CurtainModuleEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class FetchCurtainModuleStatusEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
const FetchCurtainModuleStatusEvent({required this.deviceId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
class SendCurtainPercentToApiEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final Status status;
|
||||
|
||||
const SendCurtainPercentToApiEvent({
|
||||
required this.deviceId,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, status];
|
||||
}
|
||||
|
||||
class OpenCurtainEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
|
||||
const OpenCurtainEvent({required this.deviceId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
class CloseCurtainEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
|
||||
const CloseCurtainEvent({required this.deviceId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
class StopCurtainEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
|
||||
const StopCurtainEvent({required this.deviceId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
class ChangeTimerControlEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final int timControl;
|
||||
|
||||
const ChangeTimerControlEvent({
|
||||
required this.deviceId,
|
||||
required this.timControl,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, timControl];
|
||||
}
|
||||
|
||||
class CurCalibrationEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
|
||||
const CurCalibrationEvent({
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId];
|
||||
}
|
||||
|
||||
class ChangeElecMachineryModeEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final String elecMachineryMode;
|
||||
|
||||
const ChangeElecMachineryModeEvent({
|
||||
required this.deviceId,
|
||||
required this.elecMachineryMode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, elecMachineryMode];
|
||||
}
|
||||
|
||||
class ChangeControlBackEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final String controlBack;
|
||||
|
||||
const ChangeControlBackEvent({
|
||||
required this.deviceId,
|
||||
required this.controlBack,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, controlBack];
|
||||
}
|
||||
|
||||
class ChangeControlBackModeEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final String controlBackMode;
|
||||
|
||||
const ChangeControlBackModeEvent({
|
||||
required this.deviceId,
|
||||
required this.controlBackMode,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, controlBackMode];
|
||||
}
|
||||
|
||||
class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final CurtainModuleStatusModel status;
|
||||
|
||||
const ChangeCurtainModuleStatusEvent({
|
||||
required this.deviceId,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, status];
|
||||
}
|
||||
|
||||
///batch
|
||||
class CurtainModuleFetchBatchStatusEvent extends CurtainModuleEvent {
|
||||
final List<String> devicesIds;
|
||||
|
||||
const CurtainModuleFetchBatchStatusEvent(this.devicesIds);
|
||||
|
||||
@override
|
||||
List<Object> get props => [devicesIds];
|
||||
}
|
||||
|
||||
class SendCurtainBatchPercentToApiEvent extends CurtainModuleEvent {
|
||||
final List<String> devicesId;
|
||||
final Status status;
|
||||
|
||||
const SendCurtainBatchPercentToApiEvent({
|
||||
required this.devicesId,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [devicesId, status];
|
||||
}
|
||||
|
||||
class OpenCurtainBatchEvent extends CurtainModuleEvent {
|
||||
final List<String> devicesId;
|
||||
|
||||
const OpenCurtainBatchEvent({required this.devicesId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [devicesId];
|
||||
}
|
||||
|
||||
class CloseCurtainBatchEvent extends CurtainModuleEvent {
|
||||
final List<String> devicesId;
|
||||
|
||||
const CloseCurtainBatchEvent({required this.devicesId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [devicesId];
|
||||
}
|
||||
|
||||
class StopCurtainBatchEvent extends CurtainModuleEvent {
|
||||
final List<String> devicesId;
|
||||
|
||||
const StopCurtainBatchEvent({required this.devicesId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [devicesId];
|
||||
}
|
||||
|
||||
class CurtainModuleFactoryReset extends CurtainModuleEvent {
|
||||
final String deviceId;
|
||||
final FactoryResetModel factoryReset;
|
||||
|
||||
const CurtainModuleFactoryReset(
|
||||
{required this.deviceId, required this.factoryReset});
|
||||
|
||||
@override
|
||||
List<Object> get props => [deviceId, factoryReset];
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
part of 'curtain_module_bloc.dart';
|
||||
|
||||
sealed class CurtainModuleState extends Equatable {
|
||||
const CurtainModuleState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CurtainModuleInitial extends CurtainModuleState {}
|
||||
|
||||
class CurtainModuleLoading extends CurtainModuleState {}
|
||||
|
||||
class CurtainModuleError extends CurtainModuleState {
|
||||
final String message;
|
||||
const CurtainModuleError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
|
||||
class CurtainModuleStatusLoaded extends CurtainModuleState {
|
||||
final CurtainModuleStatusModel curtainModuleStatus;
|
||||
|
||||
const CurtainModuleStatusLoaded({required this.curtainModuleStatus});
|
||||
|
||||
@override
|
||||
List<Object> get props => [curtainModuleStatus];
|
||||
}
|
||||
class CurtainModuleStatusUpdated extends CurtainModuleState {
|
||||
final CurtainModuleStatusModel curtainModuleStatus;
|
||||
|
||||
const CurtainModuleStatusUpdated({required this.curtainModuleStatus});
|
||||
|
||||
@override
|
||||
List<Object> get props => [curtainModuleStatus];
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
|
||||
enum CurtainModuleControl {
|
||||
open,
|
||||
close,
|
||||
stop,
|
||||
}
|
||||
|
||||
// enum CurtainControlBackMode {
|
||||
// foward,
|
||||
// backward,
|
||||
// }
|
||||
|
||||
class CurtainModuleStatusModel {
|
||||
CurtainModuleControl control;
|
||||
int percentControl;
|
||||
String curCalibration;
|
||||
// CurtainControlBackMode controlBackmode;
|
||||
int trTimeControl;
|
||||
String elecMachineryMode;
|
||||
String controlBack;
|
||||
CurtainModuleStatusModel({
|
||||
required this.control,
|
||||
required this.percentControl,
|
||||
required this.curCalibration,
|
||||
// required this.controlBackmode,
|
||||
required this.trTimeControl,
|
||||
required this.controlBack,
|
||||
required this.elecMachineryMode,
|
||||
});
|
||||
factory CurtainModuleStatusModel.zero() => CurtainModuleStatusModel(
|
||||
control: CurtainModuleControl.stop,
|
||||
percentControl: 0,
|
||||
// controlBackmode: CurtainControlBackMode.foward,
|
||||
curCalibration: '',
|
||||
trTimeControl: 0,
|
||||
controlBack: '',
|
||||
elecMachineryMode: '',
|
||||
);
|
||||
|
||||
factory CurtainModuleStatusModel.fromJson(Map<String, dynamic> json) {
|
||||
return CurtainModuleStatusModel(
|
||||
control: CurtainModuleControl.values.firstWhere(
|
||||
(e) => e.toString() == json['control'] as String,
|
||||
orElse: () => CurtainModuleControl.stop,
|
||||
),
|
||||
percentControl: json['percent_control'] as int? ?? 0,
|
||||
curCalibration: json['cur_calibration'] as String? ?? '',
|
||||
trTimeControl: json['tr_timecon'] as int? ?? 0,
|
||||
elecMachineryMode: json['elec_machinery_mode'] as String? ?? '',
|
||||
controlBack: json['control_back'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class CurtainModuleBatchView extends StatelessWidget {
|
||||
final List<String> devicesIds;
|
||||
const CurtainModuleBatchView({
|
||||
super.key,
|
||||
required this.devicesIds,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CurtainModuleBloc(
|
||||
controlDeviceService: RemoteControlDeviceService(),
|
||||
batchControlDevicesService: RemoteBatchControlDevicesService())
|
||||
..add(CurtainModuleFetchBatchStatusEvent(devicesIds)),
|
||||
child: _buildStatusControls(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusControls(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 30),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ControlCurtainMovementWidget(
|
||||
devicesId: devicesIds,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
height: 120,
|
||||
// width: 350,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Expanded(
|
||||
// child:
|
||||
FactoryResetWidget(
|
||||
callFactoryReset: () {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
CurtainModuleFactoryReset(
|
||||
deviceId: devicesIds.first,
|
||||
factoryReset:
|
||||
FactoryResetModel(devicesUuid: devicesIds),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: IconNameStatusContainer(
|
||||
// isFullIcon: false,
|
||||
// name: 'Firmware Update',
|
||||
// icon: Assets.firmware,
|
||||
// onTap: () {},
|
||||
// status: false,
|
||||
// textColor: ColorsManager.blackColor,
|
||||
// ),
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
|
||||
import 'package:syncrow_web/services/batch_control_devices_service.dart';
|
||||
import 'package:syncrow_web/services/control_device_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||
|
||||
class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
|
||||
final String deviceId;
|
||||
const CurtainModuleItems({
|
||||
super.key,
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CurtainModuleBloc(
|
||||
controlDeviceService: RemoteControlDeviceService(),
|
||||
batchControlDevicesService: RemoteBatchControlDevicesService())
|
||||
..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)),
|
||||
child: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
|
||||
builder: (context, state) {
|
||||
return _buildStatusControls(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusControls(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 30),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ControlCurtainMovementWidget(
|
||||
devicesId: [deviceId],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
SizedBox(
|
||||
height: 140,
|
||||
width: 350,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ScheduleControlButton(
|
||||
onTap: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value:
|
||||
BlocProvider.of<CurtainModuleBloc>(context),
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'CUR_2',
|
||||
code: 'control',
|
||||
|
||||
),
|
||||
));
|
||||
},
|
||||
mainText: '',
|
||||
subtitle: 'Scheduling',
|
||||
iconPath: Assets.scheduling,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
|
||||
builder: (context, state) {
|
||||
if (state is CurtainModuleLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (state is CurtainModuleStatusLoaded) {
|
||||
return IconNameStatusContainer(
|
||||
isFullIcon: false,
|
||||
name: 'Preferences',
|
||||
icon: Assets.preferences,
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider.value(
|
||||
value: context.read<CurtainModuleBloc>(),
|
||||
child: CurtainModulePrefrencesDialog(
|
||||
curtainModuleBloc:
|
||||
context.watch<CurtainModuleBloc>(),
|
||||
deviceId: deviceId,
|
||||
curtainModuleStatusModel:
|
||||
state.curtainModuleStatus,
|
||||
),
|
||||
),
|
||||
),
|
||||
status: false,
|
||||
textColor: ColorsManager.blackColor,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
|
||||
class AccurteCalibratingDialog extends StatelessWidget {
|
||||
final String deviceId;
|
||||
final BuildContext parentContext;
|
||||
const AccurteCalibratingDialog({
|
||||
super.key,
|
||||
required this.deviceId,
|
||||
required this.parentContext,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Calibrating',
|
||||
body: const NormalTextBodyForDialog(
|
||||
title: '',
|
||||
step1:
|
||||
'1. Click Close Button to make the Curtain run to Full Close and Position.',
|
||||
step2: '2. click Next to complete the Calibration.',
|
||||
),
|
||||
leftOnTap: () => Navigator.of(parentContext).pop(),
|
||||
rightOnTap: () {
|
||||
parentContext.read<CurtainModuleBloc>().add(
|
||||
CurCalibrationEvent(
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
Navigator.of(parentContext).pop();
|
||||
showDialog(
|
||||
context: parentContext,
|
||||
builder: (_) => CalibrateCompletedDialog(
|
||||
parentContext: parentContext,
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
|
||||
class AccurateCalibrationDialog extends StatelessWidget {
|
||||
final String deviceId;
|
||||
final BuildContext parentContext;
|
||||
const AccurateCalibrationDialog({
|
||||
super.key,
|
||||
required this.deviceId,
|
||||
required this.parentContext,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Accurate Calibration',
|
||||
body: const NormalTextBodyForDialog(
|
||||
title: 'Prepare Calibration:',
|
||||
step1: '1. Run The Curtain to the Fully Open Position,and pause.',
|
||||
step2: '2. click Next to Start accurate calibration.',
|
||||
),
|
||||
leftOnTap: () => Navigator.of(parentContext).pop(),
|
||||
rightOnTap: () {
|
||||
Navigator.of(parentContext).pop();
|
||||
showDialog(
|
||||
context: parentContext,
|
||||
builder: (_) => AccurteCalibratingDialog(
|
||||
deviceId: deviceId,
|
||||
parentContext: parentContext,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class AccurateDialogWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget body;
|
||||
final void Function() leftOnTap;
|
||||
final void Function() rightOnTap;
|
||||
const AccurateDialogWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.leftOnTap,
|
||||
required this.rightOnTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
width: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Divider(
|
||||
indent: 10,
|
||||
endIndent: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: body,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Spacer(),
|
||||
const Divider(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: leftOnTap,
|
||||
child: Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: ColorsManager.grayBorder),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: rightOnTap,
|
||||
child: Container(
|
||||
height: 60,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Next',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CalibrateCompletedDialog extends StatelessWidget {
|
||||
final BuildContext parentContext;
|
||||
final String deviceId;
|
||||
const CalibrateCompletedDialog({
|
||||
super.key,
|
||||
required this.parentContext,
|
||||
required this.deviceId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: SizedBox(
|
||||
height: 250,
|
||||
width: 400,
|
||||
child: Column(
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Calibration Completed',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Divider(
|
||||
indent: 10,
|
||||
endIndent: 10,
|
||||
),
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
size: 100,
|
||||
color: ColorsManager.blueColor,
|
||||
),
|
||||
const Spacer(),
|
||||
const Divider(
|
||||
indent: 10,
|
||||
endIndent: 10,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
parentContext.read<CurtainModuleBloc>().add(
|
||||
FetchCurtainModuleStatusEvent(
|
||||
deviceId: deviceId,
|
||||
),
|
||||
);
|
||||
Navigator.of(parentContext).pop();
|
||||
Navigator.of(parentContext).pop();
|
||||
},
|
||||
child: Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'Close',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CurtainActionWidget extends StatelessWidget {
|
||||
final String icon;
|
||||
final void Function() onTap;
|
||||
const CurtainActionWidget({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: ColorsManager.whiteColors,
|
||||
child: ClipOval(
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: ColorsManager.graysColor,
|
||||
child: SvgPicture.asset(
|
||||
icon,
|
||||
width: 35,
|
||||
height: 35,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class ControlCurtainMovementWidget extends StatelessWidget {
|
||||
final List<String> devicesId;
|
||||
const ControlCurtainMovementWidget({
|
||||
super.key,
|
||||
required this.devicesId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 550,
|
||||
child: DeviceControlsContainer(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CurtainActionWidget(
|
||||
icon: Assets.openCurtain,
|
||||
onTap: () {
|
||||
if (devicesId.length == 1) {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
OpenCurtainEvent(deviceId: devicesId.first),
|
||||
);
|
||||
} else {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
OpenCurtainBatchEvent(devicesId: devicesId),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 30,
|
||||
),
|
||||
CurtainActionWidget(
|
||||
icon: Assets.pauseCurtain,
|
||||
onTap: () {
|
||||
if (devicesId.length == 1) {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
StopCurtainEvent(deviceId: devicesId.first),
|
||||
);
|
||||
} else {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
StopCurtainBatchEvent(devicesId: devicesId),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
width: 30,
|
||||
),
|
||||
CurtainActionWidget(
|
||||
icon: Assets.closeCurtain,
|
||||
onTap: () {
|
||||
if (devicesId.length == 1) {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
CloseCurtainEvent(deviceId: devicesId.first),
|
||||
);
|
||||
} else {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
CloseCurtainBatchEvent(devicesId: devicesId),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
|
||||
builder: (context, state) {
|
||||
if (state is CurtainModuleError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.minBlueDot,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is CurtainModuleLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: ColorsManager.minBlueDot,
|
||||
),
|
||||
);
|
||||
} else if (state is CurtainModuleInitial) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'No data available',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.minBlueDot,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (state is CurtainModuleStatusLoaded) {
|
||||
return CurtainSliderWidget(
|
||||
status: state.curtainModuleStatus,
|
||||
devicesId: devicesId,
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Unknown state',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.minBlueDot,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CurtainSliderWidget extends StatefulWidget {
|
||||
final CurtainModuleStatusModel status;
|
||||
final List<String> devicesId;
|
||||
|
||||
const CurtainSliderWidget({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.devicesId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CurtainSliderWidget> createState() => _CurtainSliderWidgetState();
|
||||
}
|
||||
|
||||
class _CurtainSliderWidgetState extends State<CurtainSliderWidget> {
|
||||
double? _localValue; // For temporary drag state
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If user is dragging, use local value. Otherwise, use Firebase-synced state
|
||||
final double currentSliderValue =
|
||||
_localValue ?? widget.status.percentControl / 100;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${(currentSliderValue * 100).round()}%',
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.minBlueDot,
|
||||
fontSize: 25,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: currentSliderValue,
|
||||
min: 0,
|
||||
max: 1,
|
||||
divisions: 10, // 10% step
|
||||
activeColor: ColorsManager.minBlueDot,
|
||||
thumbColor: ColorsManager.primaryColor,
|
||||
inactiveColor: ColorsManager.whiteColors,
|
||||
|
||||
// Start dragging — use local control
|
||||
onChangeStart: (_) {
|
||||
setState(() {
|
||||
_localValue = currentSliderValue;
|
||||
});
|
||||
},
|
||||
|
||||
// While dragging — update temporary value
|
||||
onChanged: (value) {
|
||||
final steppedValue = (value * 10).roundToDouble() / 10;
|
||||
setState(() {
|
||||
_localValue = steppedValue;
|
||||
});
|
||||
},
|
||||
|
||||
// On release — send API and return to Firebase-controlled state
|
||||
onChangeEnd: (value) {
|
||||
final int targetPercent = (value * 100).round();
|
||||
|
||||
if (widget.devicesId.length == 1) {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
SendCurtainPercentToApiEvent(
|
||||
deviceId: widget.devicesId.first,
|
||||
status: Status(
|
||||
code: 'percent_control',
|
||||
value: targetPercent,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
SendCurtainBatchPercentToApiEvent(
|
||||
devicesId: widget.devicesId,
|
||||
status: Status(
|
||||
code: 'percent_control',
|
||||
value: targetPercent,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Revert back to Firebase-synced stream
|
||||
setState(() {
|
||||
_localValue = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class NormalTextBodyForDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final String step1;
|
||||
final String step2;
|
||||
|
||||
const NormalTextBodyForDialog({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.step1,
|
||||
required this.step2,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
step1,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
step2,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class NumberInputField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
|
||||
const NumberInputField({super.key, required this.controller});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/web_layout/default_container.dart';
|
||||
|
||||
class PrefReversCardWidget extends StatelessWidget {
|
||||
final void Function() onTap;
|
||||
final String title;
|
||||
final String body;
|
||||
const PrefReversCardWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultContainer(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayBorder,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(10),
|
||||
right: Radius.circular(10)),
|
||||
border: Border.all(color: ColorsManager.grayBorder)),
|
||||
child: SvgPicture.asset(
|
||||
Assets.reverseArrows,
|
||||
height: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
body,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/web_layout/default_container.dart';
|
||||
|
||||
class CurtainModulePrefrencesDialog extends StatelessWidget {
|
||||
final CurtainModuleStatusModel curtainModuleStatusModel;
|
||||
final String deviceId;
|
||||
final CurtainModuleBloc curtainModuleBloc;
|
||||
const CurtainModulePrefrencesDialog({
|
||||
super.key,
|
||||
required this.curtainModuleStatusModel,
|
||||
required this.deviceId,
|
||||
required this.curtainModuleBloc,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
backgroundColor: ColorsManager.CircleImageBackground,
|
||||
contentPadding: const EdgeInsets.all(30),
|
||||
title: const Center(
|
||||
child: Text(
|
||||
'Preferences',
|
||||
style: TextStyle(
|
||||
color: ColorsManager.blueColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)),
|
||||
content: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
|
||||
bloc: curtainModuleBloc,
|
||||
builder: (context, state) {
|
||||
if (state is CurtainModuleLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else if (state is CurtainModuleStatusLoaded) {
|
||||
return SizedBox(
|
||||
height: 300,
|
||||
width: 400,
|
||||
child: GridView(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 1.5,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
),
|
||||
children: [
|
||||
PrefReversCardWidget(
|
||||
title: state.curtainModuleStatus.controlBack,
|
||||
body: 'Motor Steering',
|
||||
onTap: () {
|
||||
context.read<CurtainModuleBloc>().add(
|
||||
ChangeControlBackEvent(
|
||||
deviceId: deviceId,
|
||||
controlBack:
|
||||
state.curtainModuleStatus.controlBack ==
|
||||
'forward'
|
||||
? 'back'
|
||||
: 'forward',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PrefReversCardWidget(
|
||||
title: formatDeviceType(
|
||||
state.curtainModuleStatus.elecMachineryMode),
|
||||
body: 'Motor Mode',
|
||||
onTap: () => context.read<CurtainModuleBloc>().add(
|
||||
ChangeElecMachineryModeEvent(
|
||||
deviceId: deviceId,
|
||||
elecMachineryMode:
|
||||
state.curtainModuleStatus.elecMachineryMode ==
|
||||
'dry_contact'
|
||||
? 'strong_power'
|
||||
: 'dry_contact',
|
||||
),
|
||||
),
|
||||
),
|
||||
DefaultContainer(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: InkWell(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => AccurateCalibrationDialog(
|
||||
deviceId: deviceId,
|
||||
parentContext: context,
|
||||
),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('Accurte Calibration',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: ColorsManager.blackColor,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
DefaultContainer(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: InkWell(
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => QuickCalibrationDialog(
|
||||
timControl: state.curtainModuleStatus.trTimeControl,
|
||||
deviceId: deviceId,
|
||||
parentContext: context),
|
||||
),
|
||||
child: const Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text('Quick Calibration',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: ColorsManager.blackColor,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String formatDeviceType(String raw) {
|
||||
return raw
|
||||
.split('_')
|
||||
.map((word) => word.isNotEmpty
|
||||
? '${word[0].toUpperCase()}${word.substring(1)}'
|
||||
: '')
|
||||
.join(' ');
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/number_input_textfield.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class QuickCalibratingDialog extends StatefulWidget {
|
||||
final int timControl;
|
||||
final String deviceId;
|
||||
final BuildContext parentContext;
|
||||
const QuickCalibratingDialog({
|
||||
super.key,
|
||||
required this.timControl,
|
||||
required this.deviceId,
|
||||
required this.parentContext,
|
||||
});
|
||||
|
||||
@override
|
||||
State<QuickCalibratingDialog> createState() => _QuickCalibratingDialogState();
|
||||
}
|
||||
|
||||
class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
|
||||
late TextEditingController _controller;
|
||||
String? _errorText;
|
||||
|
||||
void _onRightTap() {
|
||||
final value = int.tryParse(_controller.text);
|
||||
|
||||
if (value == null || value < 10 || value > 120) {
|
||||
setState(() {
|
||||
_errorText = 'Number should be between 10 and 120';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_errorText = null;
|
||||
});
|
||||
widget.parentContext.read<CurtainModuleBloc>().add(
|
||||
ChangeTimerControlEvent(
|
||||
deviceId: widget.deviceId,
|
||||
timControl: value,
|
||||
),
|
||||
);
|
||||
Navigator.of(widget.parentContext).pop();
|
||||
showDialog(
|
||||
context: widget.parentContext,
|
||||
builder: (_) => CalibrateCompletedDialog(
|
||||
parentContext: widget.parentContext,
|
||||
deviceId: widget.deviceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = TextEditingController(text: widget.timControl.toString());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Calibrating',
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'1. please Enter the Travel Time:',
|
||||
style: TextStyle(color: ColorsManager.grayBorder),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: 150,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: NumberInputField(controller: _controller),
|
||||
),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'seconds',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: ColorsManager.blueColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
_errorText!,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.red,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
leftOnTap: () => Navigator.of(widget.parentContext).pop(),
|
||||
rightOnTap: _onRightTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart';
|
||||
|
||||
class QuickCalibrationDialog extends StatelessWidget {
|
||||
final int timControl;
|
||||
final String deviceId;
|
||||
final BuildContext parentContext;
|
||||
const QuickCalibrationDialog({
|
||||
super.key,
|
||||
required this.timControl,
|
||||
required this.deviceId,
|
||||
required this.parentContext,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(_) {
|
||||
return AlertDialog(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: AccurateDialogWidget(
|
||||
title: 'Quick Calibration',
|
||||
body: const NormalTextBodyForDialog(
|
||||
title: 'Prepare Calibration:',
|
||||
step1:
|
||||
'1. Confirm that the curtain is in the fully closed and suspended state.',
|
||||
step2: '2. click Next to Start calibration.',
|
||||
),
|
||||
leftOnTap: () => Navigator.of(parentContext).pop(),
|
||||
rightOnTap: () {
|
||||
Navigator.of(parentContext).pop();
|
||||
showDialog(
|
||||
context: parentContext,
|
||||
builder: (_) => QuickCalibratingDialog(
|
||||
timControl: timControl,
|
||||
deviceId: deviceId,
|
||||
parentContext: parentContext,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:firebase_database/firebase_database.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -16,45 +14,38 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
|
||||
|
||||
DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) {
|
||||
on<DoorLockFetchStatus>(_onFetchDeviceStatus);
|
||||
//on<DoorLockControl>(_onDoorLockControl);
|
||||
on<UpdateLockEvent>(_updateLock);
|
||||
on<DoorLockFactoryReset>(_onFactoryReset);
|
||||
on<StatusUpdated>(_onStatusUpdated);
|
||||
}
|
||||
|
||||
_listenToChanges(deviceId) {
|
||||
void _listenToChanges(String deviceId) {
|
||||
try {
|
||||
DatabaseReference ref =
|
||||
FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
Stream<DatabaseEvent> stream = ref.onValue;
|
||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||
ref.onValue.listen((event) {
|
||||
final data = event.snapshot.value;
|
||||
if (data is Map) {
|
||||
final statusData = data['status'] as List<dynamic>? ?? [];
|
||||
final statusList = statusData.map((item) {
|
||||
return Status(code: item['code'], value: item['value']);
|
||||
}).toList();
|
||||
|
||||
stream.listen((DatabaseEvent event) {
|
||||
Map<dynamic, dynamic> usersMap =
|
||||
event.snapshot.value as Map<dynamic, dynamic>;
|
||||
|
||||
List<Status> statusList = [];
|
||||
usersMap['status'].forEach((element) {
|
||||
statusList
|
||||
.add(Status(code: element['code'], value: element['value']));
|
||||
});
|
||||
|
||||
deviceStatus =
|
||||
DoorLockStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||
if (!isClosed) {
|
||||
add(StatusUpdated(deviceStatus));
|
||||
final model =
|
||||
DoorLockStatusModel.fromJson(data['productUuid'], statusList);
|
||||
if (!isClosed) {
|
||||
add(StatusUpdated(model));
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
void _onStatusUpdated(StatusUpdated event, Emitter<DoorLockState> emit) {
|
||||
emit(DoorLockStatusLoading());
|
||||
|
||||
deviceStatus = event.deviceStatus;
|
||||
emit(DoorLockStatusLoaded(deviceStatus));
|
||||
}
|
||||
|
||||
FutureOr<void> _onFetchDeviceStatus(
|
||||
Future<void> _onFetchDeviceStatus(
|
||||
DoorLockFetchStatus event, Emitter<DoorLockState> emit) async {
|
||||
emit(DoorLockStatusLoading());
|
||||
try {
|
||||
@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
|
||||
deviceStatus =
|
||||
DoorLockStatusModel.fromJson(event.deviceId, status.status);
|
||||
_listenToChanges(event.deviceId);
|
||||
|
||||
emit(DoorLockStatusLoaded(deviceStatus));
|
||||
} catch (e) {
|
||||
emit(DoorLockControlError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _updateLock(
|
||||
Future<void> _updateLock(
|
||||
UpdateLockEvent event, Emitter<DoorLockState> emit) async {
|
||||
final oldValue = deviceStatus.normalOpenSwitch;
|
||||
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue);
|
||||
@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
|
||||
|
||||
try {
|
||||
final response = await DevicesManagementApi.openDoorLock(deviceId);
|
||||
|
||||
if (!response) {
|
||||
_revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit);
|
||||
}
|
||||
@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runDebounce({
|
||||
required String deviceId,
|
||||
required String code,
|
||||
required dynamic value,
|
||||
required dynamic oldValue,
|
||||
required Emitter<DoorLockState> emit,
|
||||
}) async {
|
||||
if (_timer != null) {
|
||||
_timer!.cancel();
|
||||
}
|
||||
_timer = Timer(const Duration(seconds: 1), () async {
|
||||
try {
|
||||
final response = await DevicesManagementApi()
|
||||
.deviceControl(deviceId, Status(code: code, value: value));
|
||||
if (!response) {
|
||||
_revertValueAndEmit(deviceId, code, oldValue, emit);
|
||||
}
|
||||
} catch (e) {
|
||||
_revertValueAndEmit(deviceId, code, oldValue, emit);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _revertValueAndEmit(
|
||||
String deviceId,
|
||||
String code,
|
||||
dynamic oldValue,
|
||||
Emitter<DoorLockState> emit,
|
||||
) {
|
||||
void _revertValueAndEmit(String deviceId, String code, dynamic oldValue,
|
||||
Emitter<DoorLockState> emit) {
|
||||
_updateLocalValue(code, oldValue);
|
||||
emit(DoorLockStatusLoaded(deviceStatus));
|
||||
emit(const DoorLockControlError('Failed to control the device.'));
|
||||
@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
|
||||
|
||||
void _updateLocalValue(String code, dynamic value) {
|
||||
switch (code) {
|
||||
case 'reverse_lock':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(reverseLock: value);
|
||||
}
|
||||
break;
|
||||
case 'normal_open_switch':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: value);
|
||||
}
|
||||
break;
|
||||
case 'reverse_lock':
|
||||
if (value is bool) {
|
||||
deviceStatus = deviceStatus.copyWith(reverseLock: value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
emit(DoorLockStatusLoaded(deviceStatus));
|
||||
}
|
||||
|
||||
dynamic _getValueByCode(String code) {
|
||||
switch (code) {
|
||||
case 'reverse_lock':
|
||||
return deviceStatus.reverseLock;
|
||||
case 'normal_open_switch':
|
||||
return deviceStatus.normalOpenSwitch;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onFactoryReset(
|
||||
Future<void> _onFactoryReset(
|
||||
DoorLockFactoryReset event, Emitter<DoorLockState> emit) async {
|
||||
emit(DoorLockStatusLoading());
|
||||
try {
|
||||
|
@ -8,7 +8,7 @@ import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_st
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class DoorLockButton extends StatefulWidget {
|
||||
class DoorLockButton extends StatelessWidget {
|
||||
const DoorLockButton({
|
||||
super.key,
|
||||
required this.doorLock,
|
||||
@ -18,70 +18,28 @@ class DoorLockButton extends StatefulWidget {
|
||||
final AllDevicesModel doorLock;
|
||||
final DoorLockStatusModel smartDoorModel;
|
||||
|
||||
@override
|
||||
State<DoorLockButton> createState() =>
|
||||
_DoorLockButtonState(smartDoorModel: smartDoorModel);
|
||||
}
|
||||
|
||||
class _DoorLockButtonState extends State<DoorLockButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _animation;
|
||||
DoorLockStatusModel smartDoorModel;
|
||||
|
||||
_DoorLockButtonState({required this.smartDoorModel});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
_animation = Tween<double>(begin: 0, end: 1).animate(_animationController)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
|
||||
if (smartDoorModel.unlockRequest > 0) {
|
||||
_animationController.reverse(from: 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DoorLockButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.smartDoorModel.normalOpenSwitch !=
|
||||
widget.smartDoorModel.normalOpenSwitch) {
|
||||
setState(() {
|
||||
smartDoorModel = widget.smartDoorModel;
|
||||
});
|
||||
|
||||
if (smartDoorModel.unlockRequest > 0) {
|
||||
_animationController.forward(from: 0);
|
||||
} else {
|
||||
_animationController.reverse(from: 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
double _calculateProgress() {
|
||||
final value = smartDoorModel.unlockRequest;
|
||||
if (value <= 0 || value > 30) return 0;
|
||||
return value / 30.0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final progress = _calculateProgress();
|
||||
final isEnabled = smartDoorModel.unlockRequest > 0;
|
||||
|
||||
return SizedBox(
|
||||
width: 255,
|
||||
height: 255,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_animationController.forward(from: 0);
|
||||
BlocProvider.of<DoorLockBloc>(context)
|
||||
.add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch));
|
||||
},
|
||||
onTap: isEnabled
|
||||
? () {
|
||||
BlocProvider.of<DoorLockBloc>(context).add(
|
||||
UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
width: 255,
|
||||
height: 255,
|
||||
@ -115,15 +73,16 @@ class _DoorLockButtonState extends State<DoorLockButton>
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox.expand(
|
||||
child: CircularProgressIndicator(
|
||||
value: _animation.value,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
ColorsManager.primaryColor),
|
||||
if (progress > 0)
|
||||
SizedBox.expand(
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: Colors.transparent,
|
||||
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||
ColorsManager.primaryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||
|
||||
//Smart Power Clamp
|
||||
class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout {
|
||||
class SmartPowerDeviceControl extends StatelessWidget
|
||||
with HelperResponsiveLayout {
|
||||
final String deviceId;
|
||||
|
||||
const SmartPowerDeviceControl({super.key, required this.deviceId});
|
||||
@ -145,13 +146,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_left),
|
||||
onPressed: () {
|
||||
blocProvider.add(SmartPowerArrowPressedEvent(-1));
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
onPressed: blocProvider.currentPage <= 0
|
||||
? null
|
||||
: () {
|
||||
blocProvider
|
||||
.add(SmartPowerArrowPressedEvent(-1));
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
currentPage == 0
|
||||
@ -165,13 +169,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_right),
|
||||
onPressed: () {
|
||||
blocProvider.add(SmartPowerArrowPressedEvent(1));
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
onPressed: blocProvider.currentPage >= 3
|
||||
? null
|
||||
: () {
|
||||
blocProvider
|
||||
.add(SmartPowerArrowPressedEvent(1));
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
|
||||
blocProvider.add(SelectDateEvent(context: context));
|
||||
blocProvider.add(FilterRecordsByDateEvent(
|
||||
selectedDate: blocProvider.dateTime!,
|
||||
viewType:
|
||||
blocProvider.views[blocProvider.currentIndex]));
|
||||
viewType: blocProvider
|
||||
.views[blocProvider.currentIndex]));
|
||||
},
|
||||
widget: blocProvider.dateSwitcher(),
|
||||
chartData: blocProvider.energyDataList.isNotEmpty
|
||||
|
@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
emit(currentState.copyWith(
|
||||
scheduleMode: event.scheduleMode,
|
||||
countdownRemaining: Duration.zero,
|
||||
countdownHours: 0,
|
||||
countdownMinutes: 0,
|
||||
inchingHours: 0,
|
||||
inchingMinutes: 0,
|
||||
isCountdownActive: false,
|
||||
isInchingActive: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
if (state is ScheduleLoaded) {
|
||||
final currentState = state as ScheduleLoaded;
|
||||
emit(currentState.copyWith(
|
||||
countdownSeconds: event.seconds,
|
||||
countdownHours: event.hours,
|
||||
countdownMinutes: event.minutes,
|
||||
inchingHours: 0,
|
||||
@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
inchingHours: event.hours,
|
||||
inchingMinutes: event.minutes,
|
||||
countdownRemaining: Duration.zero,
|
||||
inchingSeconds: 0, // Add this
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -257,7 +265,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
category: event.category,
|
||||
deviceId: deviceId,
|
||||
time: getTimeStampWithoutSeconds(dateTime).toString(),
|
||||
code: event.category,
|
||||
code: event.code ?? event.category,
|
||||
value: event.functionOn,
|
||||
days: event.selectedDays);
|
||||
if (success) {
|
||||
@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
countdownMinutes: countdownDuration.inMinutes % 60,
|
||||
countdownRemaining: countdownDuration,
|
||||
isCountdownActive: true,
|
||||
countdownSeconds: countdownDuration.inSeconds,
|
||||
),
|
||||
);
|
||||
|
||||
@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
countdownMinutes: 0,
|
||||
countdownRemaining: Duration.zero,
|
||||
isCountdownActive: false,
|
||||
countdownSeconds: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
inchingMinutes: inchingDuration.inMinutes % 60,
|
||||
isInchingActive: true,
|
||||
countdownRemaining: inchingDuration,
|
||||
countdownSeconds: inchingDuration.inSeconds,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
}
|
||||
|
||||
String extractTime(String isoDateTime) {
|
||||
// Example input: "2025-06-19T15:45:00.000"
|
||||
return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00"
|
||||
return isoDateTime.split('T')[1].split('.')[0];
|
||||
}
|
||||
|
||||
int? getTimeStampWithoutSeconds(DateTime? dateTime) {
|
||||
|
@ -70,17 +70,19 @@ class ScheduleAddEvent extends ScheduleEvent {
|
||||
final String category;
|
||||
final String time;
|
||||
final List<String> selectedDays;
|
||||
final bool functionOn;
|
||||
final dynamic functionOn;
|
||||
final String? code;
|
||||
|
||||
const ScheduleAddEvent({
|
||||
required this.category,
|
||||
required this.time,
|
||||
required this.selectedDays,
|
||||
required this.functionOn,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [category, time, selectedDays, functionOn];
|
||||
List<Object?> get props => [category, time, selectedDays, functionOn, code];
|
||||
}
|
||||
|
||||
class ScheduleEditEvent extends ScheduleEvent {
|
||||
@ -146,14 +148,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent {
|
||||
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
||||
final int hours;
|
||||
final int minutes;
|
||||
final int seconds;
|
||||
|
||||
const UpdateCountdownTimeEvent({
|
||||
required this.hours,
|
||||
required this.minutes,
|
||||
required this.seconds,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [hours, minutes];
|
||||
List<Object> get props => [hours, minutes, seconds];
|
||||
}
|
||||
|
||||
class UpdateInchingTimeEvent extends ScheduleEvent {
|
||||
|
@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState {
|
||||
final bool isCountdownActive;
|
||||
final int inchingHours;
|
||||
final int inchingMinutes;
|
||||
final int inchingSeconds;
|
||||
final bool isInchingActive;
|
||||
final ScheduleModes scheduleMode;
|
||||
final Duration? countdownRemaining;
|
||||
final int? countdownSeconds;
|
||||
|
||||
const ScheduleLoaded({
|
||||
this.countdownSeconds = 0,
|
||||
this.inchingSeconds = 0,
|
||||
required this.schedules,
|
||||
this.selectedTime,
|
||||
required this.selectedDays,
|
||||
@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState {
|
||||
bool? isInchingActive,
|
||||
ScheduleModes? scheduleMode,
|
||||
Duration? countdownRemaining,
|
||||
String? deviceId,
|
||||
int? countdownSeconds,
|
||||
int? inchingSeconds,
|
||||
}) {
|
||||
return ScheduleLoaded(
|
||||
schedules: schedules ?? this.schedules,
|
||||
@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState {
|
||||
selectedDays: selectedDays ?? this.selectedDays,
|
||||
functionOn: functionOn ?? this.functionOn,
|
||||
isEditing: isEditing ?? this.isEditing,
|
||||
deviceId: deviceId,
|
||||
deviceId: deviceId ?? this.deviceId,
|
||||
countdownHours: countdownHours ?? this.countdownHours,
|
||||
countdownMinutes: countdownMinutes ?? this.countdownMinutes,
|
||||
isCountdownActive: isCountdownActive ?? this.isCountdownActive,
|
||||
@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState {
|
||||
isInchingActive: isInchingActive ?? this.isInchingActive,
|
||||
scheduleMode: scheduleMode ?? this.scheduleMode,
|
||||
countdownRemaining: countdownRemaining ?? this.countdownRemaining,
|
||||
countdownSeconds: countdownSeconds ?? this.countdownSeconds,
|
||||
inchingSeconds: inchingSeconds ?? this.inchingSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState {
|
||||
isInchingActive,
|
||||
scheduleMode,
|
||||
countdownRemaining,
|
||||
countdownSeconds,
|
||||
inchingSeconds,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CountdownInchingView extends StatefulWidget {
|
||||
const CountdownInchingView({super.key});
|
||||
final String deviceId;
|
||||
const CountdownInchingView({super.key, required this.deviceId});
|
||||
|
||||
@override
|
||||
State<CountdownInchingView> createState() => _CountdownInchingViewState();
|
||||
@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget {
|
||||
class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
late FixedExtentScrollController _hoursController;
|
||||
late FixedExtentScrollController _minutesController;
|
||||
late FixedExtentScrollController _secondsController;
|
||||
|
||||
int _lastHours = -1;
|
||||
int _lastMinutes = -1;
|
||||
int _lastSeconds = -1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hoursController = FixedExtentScrollController();
|
||||
_minutesController = FixedExtentScrollController();
|
||||
_secondsController = FixedExtentScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoursController.dispose();
|
||||
_minutesController.dispose();
|
||||
_secondsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateControllers(int displayHours, int displayMinutes) {
|
||||
void _updateControllers(
|
||||
int displayHours, int displayMinutes, int displaySeconds) {
|
||||
if (_lastHours != displayHours) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_hoursController.hasClients) {
|
||||
@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
});
|
||||
_lastMinutes = displayMinutes;
|
||||
}
|
||||
// Update seconds controller
|
||||
if (_lastSeconds != displaySeconds) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_secondsController.hasClients) {
|
||||
_secondsController.jumpToItem(displaySeconds);
|
||||
}
|
||||
});
|
||||
_lastSeconds = displaySeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
return BlocBuilder<ScheduleBloc, ScheduleState>(
|
||||
builder: (context, state) {
|
||||
if (state is! ScheduleLoaded) return const SizedBox.shrink();
|
||||
|
||||
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
||||
final isActive =
|
||||
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
||||
@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
final displayMinutes = isActive && state.countdownRemaining != null
|
||||
? state.countdownRemaining!.inMinutes.remainder(60)
|
||||
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
||||
final displaySeconds = isActive && state.countdownRemaining != null
|
||||
? state.countdownRemaining!.inSeconds.remainder(60)
|
||||
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
|
||||
|
||||
_updateControllers(displayHours, displayMinutes, displaySeconds!);
|
||||
|
||||
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
StopScheduleEvent(
|
||||
mode: ScheduleModes.countdown,
|
||||
deviceId: widget.deviceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_updateControllers(displayHours, displayMinutes);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
(value) {
|
||||
if (!isActive) {
|
||||
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
|
||||
hours: value, minutes: displayMinutes));
|
||||
hours: value,
|
||||
minutes: displayMinutes,
|
||||
seconds: displaySeconds,
|
||||
));
|
||||
}
|
||||
},
|
||||
isActive: isActive,
|
||||
@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
||||
(value) {
|
||||
if (!isActive) {
|
||||
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
|
||||
hours: displayHours, minutes: value));
|
||||
hours: displayHours,
|
||||
minutes: value,
|
||||
seconds: displaySeconds,
|
||||
));
|
||||
}
|
||||
},
|
||||
isActive: isActive,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
if (isActive)
|
||||
_buildPickerColumn(
|
||||
context,
|
||||
's',
|
||||
displaySeconds,
|
||||
60,
|
||||
_secondsController,
|
||||
(value) {
|
||||
if (!isActive) {
|
||||
context
|
||||
.read<ScheduleBloc>()
|
||||
.add(UpdateCountdownTimeEvent(
|
||||
hours: displayHours,
|
||||
minutes: displayMinutes,
|
||||
seconds: value,
|
||||
));
|
||||
}
|
||||
},
|
||||
isActive: isActive,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart';
|
||||
@ -9,13 +10,19 @@ import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widg
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
|
||||
|
||||
class BuildScheduleView extends StatelessWidget {
|
||||
const BuildScheduleView(
|
||||
{super.key, required this.deviceUuid, required this.category});
|
||||
const BuildScheduleView({
|
||||
super.key,
|
||||
required this.deviceUuid,
|
||||
required this.category,
|
||||
this.code,
|
||||
});
|
||||
final String deviceUuid;
|
||||
final String category;
|
||||
final String? code;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -57,13 +64,21 @@ class BuildScheduleView extends StatelessWidget {
|
||||
final entry = await ScheduleDialogHelper
|
||||
.showAddScheduleDialog(
|
||||
context,
|
||||
schedule: null,
|
||||
schedule: ScheduleEntry(
|
||||
category: category,
|
||||
time: '',
|
||||
function: Status(
|
||||
code: code.toString(), value: null),
|
||||
days: [],
|
||||
),
|
||||
isEdit: false,
|
||||
code: code,
|
||||
);
|
||||
if (entry != null) {
|
||||
context.read<ScheduleBloc>().add(
|
||||
ScheduleAddEvent(
|
||||
category: entry.category,
|
||||
category: category,
|
||||
code: entry.function.code,
|
||||
time: entry.time,
|
||||
functionOn: entry.function.value,
|
||||
selectedDays: entry.days,
|
||||
@ -74,7 +89,9 @@ class BuildScheduleView extends StatelessWidget {
|
||||
),
|
||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||
state.scheduleMode == ScheduleModes.inching)
|
||||
const CountdownInchingView(),
|
||||
CountdownInchingView(
|
||||
deviceId: deviceUuid,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (state.scheduleMode == ScheduleModes.countdown)
|
||||
CountdownModeButtons(
|
||||
|
@ -162,11 +162,18 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
bool temp;
|
||||
if (schedule.category == 'CUR_2') {
|
||||
temp = schedule.function.value == 'open' ? true : false;
|
||||
} else {
|
||||
temp = schedule.function.value as bool;
|
||||
}
|
||||
context.read<ScheduleBloc>().add(
|
||||
ScheduleUpdateEntryEvent(
|
||||
category: schedule.category,
|
||||
scheduleId: schedule.scheduleId,
|
||||
functionOn: schedule.function.value,
|
||||
functionOn: temp,
|
||||
// schedule.function.value,
|
||||
enable: !schedule.enable,
|
||||
),
|
||||
);
|
||||
@ -188,7 +195,10 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
child: Text(_getSelectedDays(
|
||||
ScheduleModel.parseSelectedDays(schedule.days)))),
|
||||
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
||||
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
||||
if (schedule.category == 'CUR_2')
|
||||
Center(child: Text(schedule.function.value))
|
||||
else
|
||||
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
||||
Center(
|
||||
child: Wrap(
|
||||
runAlignment: WrapAlignment.center,
|
||||
|
@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
class DeviceBatchControlDialog extends StatelessWidget
|
||||
with RouteControlsBasedCode {
|
||||
final List<AllDevicesModel> devices;
|
||||
|
||||
const DeviceBatchControlDialog({super.key, required this.devices});
|
||||
@ -18,7 +19,7 @@ class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCo
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: devices.length < 2 ? 500 : 800,
|
||||
width: devices.length < 2 ? 600 : 800,
|
||||
// height: context.screenHeight * 0.7,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
|
@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
}
|
||||
|
||||
Widget _buildDeviceInfoSection() {
|
||||
final isOnlineDevice = device.online != null && device.online!;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50),
|
||||
child: Table(
|
||||
@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
'Installation Date and Time:',
|
||||
formatDateTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
((device.createTime ?? 0) * 1000),
|
||||
(device.createTime ?? 0) * 1000,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
|
||||
),
|
||||
TableRow(
|
||||
children: [
|
||||
_buildInfoRow('Status:', 'Online', statusColor: Colors.green),
|
||||
_buildInfoRow(
|
||||
'Status:',
|
||||
isOnlineDevice ? 'Online' : 'offline',
|
||||
statusColor: isOnlineDevice ? Colors.green : Colors.red,
|
||||
),
|
||||
_buildInfoRow(
|
||||
'Last Offline Date and Time:',
|
||||
formatDateTime(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
((device.updateTime ?? 0) * 1000),
|
||||
(device.updateTime ?? 0) * 1000,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -17,14 +17,21 @@ class ScheduleDialogHelper {
|
||||
BuildContext context, {
|
||||
ScheduleEntry? schedule,
|
||||
bool isEdit = false,
|
||||
String? code,
|
||||
}) {
|
||||
bool temp;
|
||||
if (schedule?.category == 'CUR_2') {
|
||||
temp = schedule!.function.value == 'open' ? true : false;
|
||||
} else {
|
||||
temp = schedule!.function.value;
|
||||
}
|
||||
final initialTime = schedule != null
|
||||
? _convertStringToTimeOfDay(schedule.time)
|
||||
: TimeOfDay.now();
|
||||
final initialDays = schedule != null
|
||||
? _convertDaysStringToBooleans(schedule.days)
|
||||
: List.filled(7, false);
|
||||
bool? functionOn = schedule?.function.value ?? true;
|
||||
bool? functionOn = temp;
|
||||
TimeOfDay selectedTime = initialTime;
|
||||
List<bool> selectedDays = List.of(initialDays);
|
||||
|
||||
@ -96,7 +103,8 @@ class ScheduleDialogHelper {
|
||||
setState(() => selectedDays[i] = v);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
_buildFunctionSwitch(ctx, functionOn!, (v) {
|
||||
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
|
||||
(v) {
|
||||
setState(() => functionOn = v);
|
||||
}),
|
||||
],
|
||||
@ -115,10 +123,21 @@ class ScheduleDialogHelper {
|
||||
width: 100,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
dynamic temp;
|
||||
if (schedule?.category == 'CUR_2') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
}
|
||||
print(temp);
|
||||
final entry = ScheduleEntry(
|
||||
category: schedule?.category ?? 'switch_1',
|
||||
time: _formatTimeOfDayToISO(selectedTime),
|
||||
function: Status(code: 'switch_1', value: functionOn),
|
||||
function: Status(
|
||||
code: code ?? 'switch_1',
|
||||
value: temp,
|
||||
// functionOn,
|
||||
),
|
||||
days: _convertSelectedDaysToStrings(selectedDays),
|
||||
scheduleId: schedule?.scheduleId,
|
||||
);
|
||||
@ -185,7 +204,7 @@ class ScheduleDialogHelper {
|
||||
}
|
||||
|
||||
static Widget _buildFunctionSwitch(
|
||||
BuildContext ctx, bool isOn, Function(bool) onChanged) {
|
||||
String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
@ -199,14 +218,14 @@ class ScheduleDialogHelper {
|
||||
groupValue: isOn,
|
||||
onChanged: (val) => onChanged(true),
|
||||
),
|
||||
const Text('On'),
|
||||
Text(categor == 'CUR_2' ? 'open' : 'On'),
|
||||
const SizedBox(width: 10),
|
||||
Radio<bool>(
|
||||
value: false,
|
||||
groupValue: isOn,
|
||||
onChanged: (val) => onChanged(false),
|
||||
),
|
||||
const Text('Off'),
|
||||
Text(categor == 'CUR_2' ? 'close' : 'Off'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/auth/model/user_model.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
|
||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
||||
@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class EditUserDialog extends StatefulWidget {
|
||||
final String? userId;
|
||||
const EditUserDialog({super.key, this.userId});
|
||||
final RolesUserModel? user;
|
||||
const EditUserDialog({
|
||||
super.key,
|
||||
this.user,
|
||||
});
|
||||
|
||||
@override
|
||||
_EditUserDialogState createState() => _EditUserDialogState();
|
||||
@ -28,10 +33,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
create: (BuildContext context) => UsersBloc()
|
||||
// ..add(const LoadCommunityAndSpacesEvent())
|
||||
..add(const RoleEvent())
|
||||
..add(GetUserByIdEvent(uuid: widget.userId)),
|
||||
..add(GetUserByIdEvent(uuid: widget.user!.uuid)),
|
||||
child: BlocConsumer<UsersBloc, UsersState>(listener: (context, state) {
|
||||
if (state is SpacesLoadedState) {
|
||||
BlocProvider.of<UsersBloc>(context).add(GetUserByIdEvent(uuid: widget.userId));
|
||||
BlocProvider.of<UsersBloc>(context)
|
||||
.add(GetUserByIdEvent(uuid: widget.user!.uuid));
|
||||
}
|
||||
}, builder: (context, state) {
|
||||
final _blocRole = BlocProvider.of<UsersBloc>(context);
|
||||
@ -39,7 +45,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
return Dialog(
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||
width: 900,
|
||||
child: Column(
|
||||
children: [
|
||||
@ -68,7 +75,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
children: [
|
||||
_buildStep1Indicator(1, "Basics", _blocRole),
|
||||
_buildStep2Indicator(2, "Spaces", _blocRole),
|
||||
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
|
||||
_buildStep3Indicator(
|
||||
3, "Role & Permissions", _blocRole),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -86,7 +94,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: _getFormContent(widget.userId),
|
||||
child: _getFormContent(widget.user!),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@ -116,13 +124,14 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
if (currentStep < 3) {
|
||||
currentStep++;
|
||||
if (currentStep == 2) {
|
||||
_blocRole.add(CheckStepStatus(isEditUser: true));
|
||||
_blocRole
|
||||
.add(CheckStepStatus(isEditUser: true));
|
||||
} else if (currentStep == 3) {
|
||||
_blocRole.add(const CheckSpacesStepStatus());
|
||||
}
|
||||
} else {
|
||||
_blocRole
|
||||
.add(EditInviteUsers(context: context, userId: widget.userId!));
|
||||
_blocRole.add(EditInviteUsers(
|
||||
context: context, userId: widget.user!.uuid));
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -131,7 +140,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
style: TextStyle(
|
||||
color: (_blocRole.isCompleteSpaces == false ||
|
||||
_blocRole.isCompleteBasics == false ||
|
||||
_blocRole.isCompleteRolePermissions == false) &&
|
||||
_blocRole.isCompleteRolePermissions ==
|
||||
false) &&
|
||||
currentStep == 3
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.secondaryColor),
|
||||
@ -146,15 +156,15 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
}));
|
||||
}
|
||||
|
||||
Widget _getFormContent(userid) {
|
||||
Widget _getFormContent(RolesUserModel user) {
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
return BasicsView(
|
||||
userId: userid,
|
||||
userId: user.uuid,
|
||||
);
|
||||
case 2:
|
||||
return SpacesAccessView(
|
||||
userId: userid,
|
||||
userId: user.uuid,
|
||||
);
|
||||
case 3:
|
||||
return const RolesAndPermission();
|
||||
@ -166,6 +176,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
int step3 = 0;
|
||||
|
||||
Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) {
|
||||
final isCurrentStep = currentStep == step;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@ -189,7 +200,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
currentStep == step
|
||||
isCurrentStep
|
||||
? Assets.currentProcessIcon
|
||||
: bloc.isCompleteBasics == false
|
||||
? Assets.wrongProcessIcon
|
||||
@ -204,8 +215,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: isCurrentStep
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight:
|
||||
isCurrentStep ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -229,6 +243,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
}
|
||||
|
||||
Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) {
|
||||
final isCurrentStep = currentStep == step;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@ -248,7 +263,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
currentStep == step
|
||||
isCurrentStep
|
||||
? Assets.currentProcessIcon
|
||||
: bloc.isCompleteSpaces == false
|
||||
? Assets.wrongProcessIcon
|
||||
@ -263,8 +278,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: isCurrentStep
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight:
|
||||
isCurrentStep ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -288,6 +306,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
}
|
||||
|
||||
Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) {
|
||||
final isCurrentStep = currentStep == step;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
@ -306,7 +325,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
child: Row(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
currentStep == step
|
||||
isCurrentStep
|
||||
? Assets.currentProcessIcon
|
||||
: bloc.isCompleteRolePermissions == false
|
||||
? Assets.wrongProcessIcon
|
||||
@ -321,8 +340,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
||||
color: isCurrentStep
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.greyColor,
|
||||
fontWeight:
|
||||
isCurrentStep ? FontWeight.bold : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -19,6 +19,7 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:syncrow_web/utils/style.dart';
|
||||
|
||||
class UsersPage extends StatelessWidget {
|
||||
UsersPage({super.key});
|
||||
|
||||
@ -451,33 +452,31 @@ class UsersPage extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
user.isEnabled != false
|
||||
? actionButton(
|
||||
isActive: true,
|
||||
title: "Edit",
|
||||
onTap: () {
|
||||
context
|
||||
.read<SpaceTreeBloc>()
|
||||
.add(ClearCachedData());
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return EditUserDialog(
|
||||
userId: user.uuid);
|
||||
},
|
||||
).then((v) {
|
||||
if (v != null) {
|
||||
if (v != null) {
|
||||
_blocRole.add(const GetUsers());
|
||||
}
|
||||
}
|
||||
});
|
||||
if (user.isEnabled != false)
|
||||
actionButton(
|
||||
isActive: true,
|
||||
title: "Edit",
|
||||
onTap: () {
|
||||
context
|
||||
.read<SpaceTreeBloc>()
|
||||
.add(ClearCachedData());
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return EditUserDialog(user: user);
|
||||
},
|
||||
)
|
||||
: actionButton(
|
||||
title: "Edit",
|
||||
),
|
||||
).then((v) {
|
||||
if (v != null) {
|
||||
_blocRole.add(const GetUsers());
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
else
|
||||
actionButton(
|
||||
title: "Edit",
|
||||
),
|
||||
actionButton(
|
||||
title: "Delete",
|
||||
onTap: () {
|
||||
|
@ -0,0 +1,6 @@
|
||||
class SpaceConnectionModel {
|
||||
final String from;
|
||||
final String to;
|
||||
|
||||
const SpaceConnectionModel({required this.from, required this.to});
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
final List<SpaceConnectionModel> connections;
|
||||
final Map<String, Offset> positions;
|
||||
final double cardWidth = 150.0;
|
||||
final double cardHeight = 90.0;
|
||||
final Set<String> highlightedUuids;
|
||||
|
||||
SpacesConnectionsArrowPainter({
|
||||
required this.connections,
|
||||
required this.positions,
|
||||
required this.highlightedUuids,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
for (final connection in connections) {
|
||||
final isSelected = highlightedUuids.contains(connection.from) ||
|
||||
highlightedUuids.contains(connection.to);
|
||||
final paint = Paint()
|
||||
..color = isSelected
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||
..strokeWidth = 2.0
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final from = positions[connection.from];
|
||||
final to = positions[connection.to];
|
||||
|
||||
if (from != null && to != null) {
|
||||
final startPoint =
|
||||
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
|
||||
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
|
||||
|
||||
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
|
||||
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
|
||||
|
||||
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
|
||||
controlPoint2.dy, endPoint.dx, endPoint.dy);
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
final circlePaint = Paint()
|
||||
..color = isSelected
|
||||
? ColorsManager.blackColor
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.srcIn;
|
||||
canvas.drawCircle(endPoint, 4, circlePaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
||||
|
||||
abstract final class SpaceManagementCommunityDialogHelper {
|
||||
static void showCreateDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => CreateCommunityDialog(
|
||||
title: const SelectableText('Community Name'),
|
||||
onCreateCommunity: (community) {
|
||||
context.read<CommunitiesBloc>().add(
|
||||
InsertCommunity(community),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectCommunityEvent(community: community),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PaginatedDataModel<T> extends Equatable {
|
||||
const PaginatedDataModel({
|
||||
required this.data,
|
||||
required this.page,
|
||||
required this.size,
|
||||
required this.hasNext,
|
||||
required this.totalItems,
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
final List<T> data;
|
||||
final int page;
|
||||
final int size;
|
||||
final bool hasNext;
|
||||
final int totalItems;
|
||||
final int totalPages;
|
||||
|
||||
factory PaginatedDataModel.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
List<T> Function(List<dynamic>) fromJsonList,
|
||||
) {
|
||||
return PaginatedDataModel<T>(
|
||||
data: fromJsonList(json['data'] as List<dynamic>),
|
||||
page: json['page'] as int? ?? 1,
|
||||
size: json['size'] as int? ?? 25,
|
||||
hasNext: json['hasNext'] as bool? ?? false,
|
||||
totalItems: json['totalItem'] as int? ?? 0,
|
||||
totalPages: json['totalPage'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
data,
|
||||
page,
|
||||
size,
|
||||
hasNext,
|
||||
totalItems,
|
||||
totalPages,
|
||||
];
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatelessWidget {
|
||||
const SpaceManagementPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||
),
|
||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text(
|
||||
'Space Management',
|
||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||
),
|
||||
enableMenuSidebar: false,
|
||||
centerBody: Text(
|
||||
'Community Structure',
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
rightBody: const NavigateHomeGridView(),
|
||||
scaffoldBody: const SpaceManagementBody(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
|
||||
class CommunityStructureCanvas extends StatefulWidget {
|
||||
const CommunityStructureCanvas({
|
||||
required this.community,
|
||||
required this.selectedSpace,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final CommunityModel community;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
@override
|
||||
State<CommunityStructureCanvas> createState() => _CommunityStructureCanvasState();
|
||||
}
|
||||
|
||||
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Map<String, Offset> _positions = {};
|
||||
final double _cardWidth = 150.0;
|
||||
final double _cardHeight = 90.0;
|
||||
final double _horizontalSpacing = 150.0;
|
||||
final double _verticalSpacing = 120.0;
|
||||
|
||||
late TransformationController _transformationController;
|
||||
late AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_transformationController = TransformationController();
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_animateToSpace(widget.selectedSpace);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_transformationController.dispose();
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Set<String> _getAllDescendantUuids(SpaceModel space) {
|
||||
final uuids = <String>{};
|
||||
for (final child in space.children) {
|
||||
uuids.add(child.uuid);
|
||||
uuids.addAll(_getAllDescendantUuids(child));
|
||||
}
|
||||
return uuids;
|
||||
}
|
||||
|
||||
void _runAnimation(Matrix4 target) {
|
||||
final animation = Matrix4Tween(
|
||||
begin: _transformationController.value,
|
||||
end: target,
|
||||
).animate(_animationController);
|
||||
|
||||
void listener() {
|
||||
_transformationController.value = animation.value;
|
||||
}
|
||||
|
||||
animation.addListener(listener);
|
||||
_animationController.forward(from: 0).whenCompleteOrCancel(() {
|
||||
animation.removeListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
void _animateToSpace(SpaceModel? space) {
|
||||
if (space == null) {
|
||||
_runAnimation(Matrix4.identity());
|
||||
return;
|
||||
}
|
||||
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
const scale = 1.5;
|
||||
final viewSize = context.size;
|
||||
if (viewSize == null) return;
|
||||
|
||||
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
|
||||
final y =
|
||||
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
|
||||
|
||||
final matrix = Matrix4.identity()
|
||||
..translate(x, y)
|
||||
..scale(scale);
|
||||
|
||||
_runAnimation(matrix);
|
||||
}
|
||||
|
||||
void _onSpaceTapped(SpaceModel? space) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(community: widget.community, space: space),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSelectionAndZoom() {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(
|
||||
community: widget.community,
|
||||
space: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _calculateLayout(
|
||||
List<SpaceModel> spaces,
|
||||
int depth,
|
||||
Map<int, double> levelXOffset,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
double childSubtreeWidth = 0;
|
||||
if (space.children.isNotEmpty) {
|
||||
_calculateLayout(space.children, depth + 1, levelXOffset);
|
||||
final firstChildPos = _positions[space.children.first.uuid];
|
||||
final lastChildPos = _positions[space.children.last.uuid];
|
||||
if (firstChildPos != null && lastChildPos != null) {
|
||||
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
|
||||
}
|
||||
}
|
||||
|
||||
final currentX = levelXOffset.putIfAbsent(depth, () => 0.0);
|
||||
double? x;
|
||||
|
||||
if (space.children.isNotEmpty) {
|
||||
final firstChildPos = _positions[space.children.first.uuid]!;
|
||||
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
|
||||
} else {
|
||||
x = currentX;
|
||||
}
|
||||
|
||||
if (x < currentX) {
|
||||
final shiftX = currentX - x;
|
||||
_shiftSubtree(space, shiftX);
|
||||
final keysToShift = levelXOffset.keys.where((d) => d > depth).toList();
|
||||
for (final key in keysToShift) {
|
||||
levelXOffset[key] = levelXOffset[key]! + shiftX;
|
||||
}
|
||||
x += shiftX;
|
||||
}
|
||||
|
||||
final y = depth * (_verticalSpacing + _cardHeight);
|
||||
_positions[space.uuid] = Offset(x, y);
|
||||
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
void _shiftSubtree(SpaceModel space, double shiftX) {
|
||||
if (_positions.containsKey(space.uuid)) {
|
||||
_positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0);
|
||||
}
|
||||
for (final child in space.children) {
|
||||
_shiftSubtree(child, shiftX);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> _buildTreeWidgets() {
|
||||
_positions.clear();
|
||||
final community = widget.community;
|
||||
|
||||
_calculateLayout(community.spaces, 0, {});
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
if (selectedSpace != null) {
|
||||
highlightedUuids.add(selectedSpace.uuid);
|
||||
highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace));
|
||||
}
|
||||
|
||||
final widgets = <Widget>[];
|
||||
final connections = <SpaceConnectionModel>[];
|
||||
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
|
||||
|
||||
return [
|
||||
CustomPaint(
|
||||
painter: SpacesConnectionsArrowPainter(
|
||||
connections: connections,
|
||||
positions: _positions,
|
||||
highlightedUuids: highlightedUuids,
|
||||
),
|
||||
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _generateWidgets(
|
||||
List<SpaceModel> spaces,
|
||||
List<Widget> widgets,
|
||||
List<SpaceConnectionModel> connections,
|
||||
Set<String> highlightedUuids,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) continue;
|
||||
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
child: SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
for (final child in space.children) {
|
||||
connections.add(
|
||||
SpaceConnectionModel(from: space.uuid, to: child.uuid),
|
||||
);
|
||||
}
|
||||
_generateWidgets(space.children, widgets, connections, highlightedUuids);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final treeWidgets = _buildTreeWidgets();
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
||||
vertical: MediaQuery.sizeOf(context).height * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
constrained: false,
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 5,
|
||||
height: MediaQuery.sizeOf(context).height * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CommunityTemplateCell extends StatelessWidget {
|
||||
const CommunityTemplateCell({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
final void Function() onTap;
|
||||
final Widget title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 2.0,
|
||||
child: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
width: 4,
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
color: ColorsManager.borderColor,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DefaultTextStyle(
|
||||
style: context.textTheme.bodyLarge!.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
child: title,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceButton extends StatelessWidget {
|
||||
const CreateSpaceButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.5),
|
||||
spreadRadius: 5,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PlusButtonWidget extends StatelessWidget {
|
||||
final Offset offset;
|
||||
final void Function() onButtonTap;
|
||||
|
||||
const PlusButtonWidget({
|
||||
super.key,
|
||||
required this.offset,
|
||||
required this.onButtonTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onButtonTap,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.spaceColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
|
||||
|
||||
class SpaceCardWidget extends StatefulWidget {
|
||||
final void Function() onTap;
|
||||
final Widget Function() buildSpaceContainer;
|
||||
|
||||
const SpaceCardWidget({
|
||||
required this.onTap,
|
||||
required this.buildSpaceContainer,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SpaceCardWidget> createState() => _SpaceCardWidgetState();
|
||||
}
|
||||
|
||||
class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
||||
bool isHovered = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: SizedBox(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceCell extends StatelessWidget {
|
||||
final String icon;
|
||||
final String name;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const SpaceCell({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 70,
|
||||
decoration: _containerDecoration(),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildIconContainer(),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIconContainer() {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.spaceColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(15),
|
||||
bottomLeft: Radius.circular(15),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: SvgPicture.asset(
|
||||
icon,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
ColorsManager.whiteColors,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _containerDecoration() {
|
||||
return BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
|
||||
|
||||
class SpaceManagementBody extends StatelessWidget {
|
||||
const SpaceManagementBody({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const SpaceManagementCommunitiesTree(),
|
||||
Expanded(
|
||||
child: BlocBuilder<CommunitiesTreeSelectionBloc,
|
||||
CommunitiesTreeSelectionState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.selectedCommunity != current.selectedCommunity,
|
||||
builder: (context, state) => Visibility(
|
||||
visible: state.selectedCommunity == null,
|
||||
replacement: const SpaceManagementCommunityStructure(),
|
||||
child: const SpaceManagementTemplatesView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
|
||||
class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
const SpaceManagementCommunityStructure({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
|
||||
final selectedCommunity = selectionBloc.selectedCommunity;
|
||||
final selectedSpace = selectionBloc.selectedSpace;
|
||||
const spacer = Spacer(flex: 10);
|
||||
return Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
),
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceManagementTemplatesView extends StatelessWidget {
|
||||
const SpaceManagementTemplatesView({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: ColorsManager.whiteColors,
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 400,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisSpacing: 10,
|
||||
childAspectRatio: 2.0,
|
||||
),
|
||||
itemCount: _gridItems(context).length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = _gridItems(context)[index];
|
||||
return CommunityTemplateCell(
|
||||
onTap: model.onTap,
|
||||
title: model.title,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<_CommunityTemplateModel> _gridItems(BuildContext context) {
|
||||
return [
|
||||
_CommunityTemplateModel(
|
||||
title: const Text('Blank'),
|
||||
onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _CommunityTemplateModel {
|
||||
final Widget title;
|
||||
final void Function() onTap;
|
||||
|
||||
_CommunityTemplateModel({
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
});
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
|
||||
|
||||
final class DebouncedCommunitiesService implements CommunitiesService {
|
||||
DebouncedCommunitiesService(
|
||||
this._decoratee, {
|
||||
this.debounceDuration = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
final CommunitiesService _decoratee;
|
||||
final Duration debounceDuration;
|
||||
|
||||
Timer? _debounceTimer;
|
||||
late Completer<CommunitiesPaginationModel>? _completer;
|
||||
|
||||
@override
|
||||
Future<CommunitiesPaginationModel> getCommunity(
|
||||
LoadCommunitiesParam param,
|
||||
) async {
|
||||
_debounceTimer?.cancel();
|
||||
|
||||
_completer = Completer<CommunitiesPaginationModel>();
|
||||
final currentCompleter = _completer!;
|
||||
|
||||
_debounceTimer = Timer(debounceDuration, () async {
|
||||
try {
|
||||
final result = await _decoratee.getCommunity(param);
|
||||
if (!currentCompleter.isCompleted) {
|
||||
currentCompleter.complete(result);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!currentCompleter.isCompleted) {
|
||||
currentCompleter.completeError(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return currentCompleter.future;
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||
|
||||
class RemoteCommunitiesService implements CommunitiesService {
|
||||
const RemoteCommunitiesService(this._httpService);
|
||||
@ -13,14 +15,26 @@ class RemoteCommunitiesService implements CommunitiesService {
|
||||
static const _defaultErrorMessage = 'Failed to load communities';
|
||||
|
||||
@override
|
||||
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param) async {
|
||||
Future<CommunitiesPaginationModel> getCommunity(
|
||||
LoadCommunitiesParam param,
|
||||
) async {
|
||||
try {
|
||||
return _httpService.get(
|
||||
path: '/api/communities/',
|
||||
expectedResponseModel: (json) => (json as List<dynamic>)
|
||||
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
final response = await _httpService.get(
|
||||
path: await _makeUrl(),
|
||||
queryParameters: {
|
||||
'page': param.page,
|
||||
'size': param.size,
|
||||
'includeSpaces': param.includeSpaces,
|
||||
if (param.search.isNotEmpty && param.search != 'null')
|
||||
'search': param.search,
|
||||
},
|
||||
expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
CommunityModel.fromJsonList,
|
||||
),
|
||||
);
|
||||
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
@ -31,4 +45,13 @@ class RemoteCommunitiesService implements CommunitiesService {
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _makeUrl() async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null) throw APIException('Project UUID is required');
|
||||
return ApiEndpoints.getCommunityListv2.replaceAll(
|
||||
'{projectId}',
|
||||
projectUuid,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
|
||||
class CommunityModel extends Equatable {
|
||||
final String uuid;
|
||||
final String name;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String description;
|
||||
final String externalId;
|
||||
final List<SpaceModel> spaces;
|
||||
|
||||
const CommunityModel({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.description,
|
||||
required this.externalId,
|
||||
required this.spaces,
|
||||
});
|
||||
|
||||
@ -16,11 +24,20 @@ class CommunityModel extends Equatable {
|
||||
return CommunityModel(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
spaces: (json['spaces'] as List<dynamic>)
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
description: json['description'] as String,
|
||||
externalId: json['externalId']?.toString() ?? '',
|
||||
spaces: (json['spaces'] as List<dynamic>? ?? <dynamic>[])
|
||||
.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
static List<CommunityModel> fromJsonList(List<dynamic> json) {
|
||||
return json
|
||||
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name, spaces];
|
||||
|
@ -2,26 +2,37 @@ import 'package:equatable/equatable.dart';
|
||||
|
||||
class SpaceModel extends Equatable {
|
||||
final String uuid;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String spaceName;
|
||||
final String icon;
|
||||
final List<SpaceModel> children;
|
||||
final SpaceModel? parent;
|
||||
|
||||
const SpaceModel({
|
||||
required this.uuid,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.spaceName,
|
||||
required this.icon,
|
||||
required this.children,
|
||||
required this.parent,
|
||||
});
|
||||
|
||||
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return SpaceModel(
|
||||
uuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
icon: json['icon'] as String,
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
|
||||
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
|
||||
spaceName: json['spaceName'] as String? ?? '',
|
||||
icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg',
|
||||
children: (json['children'] as List<dynamic>?)
|
||||
?.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
parent: json['parent'] != null
|
||||
? SpaceModel.fromJson(json['parent'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,32 @@
|
||||
class LoadCommunitiesParam {
|
||||
const LoadCommunitiesParam();
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class LoadCommunitiesParam extends Equatable {
|
||||
const LoadCommunitiesParam({
|
||||
this.page = 1,
|
||||
this.size = 25,
|
||||
this.search = '',
|
||||
this.includeSpaces = true,
|
||||
});
|
||||
|
||||
final int page;
|
||||
final int size;
|
||||
final String search;
|
||||
final bool includeSpaces;
|
||||
|
||||
LoadCommunitiesParam copyWith({
|
||||
int? page,
|
||||
int? size,
|
||||
String? search,
|
||||
bool? includeSpaces,
|
||||
}) {
|
||||
return LoadCommunitiesParam(
|
||||
page: page ?? this.page,
|
||||
size: size ?? this.size,
|
||||
search: search ?? this.search,
|
||||
includeSpaces: includeSpaces ?? this.includeSpaces,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [page, size, search, includeSpaces];
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||
|
||||
typedef CommunitiesPaginationModel = PaginatedDataModel<CommunityModel>;
|
||||
|
||||
abstract class CommunitiesService {
|
||||
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param);
|
||||
Future<CommunitiesPaginationModel> getCommunity(LoadCommunitiesParam param);
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
||||
}) : _communitiesService = communitiesService,
|
||||
super(const CommunitiesState()) {
|
||||
on<LoadCommunities>(_onLoadCommunities);
|
||||
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
||||
on<InsertCommunity>(_onInsertCommunity);
|
||||
}
|
||||
|
||||
final CommunitiesService _communitiesService;
|
||||
@ -23,28 +25,93 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
||||
Emitter<CommunitiesState> emit,
|
||||
) async {
|
||||
try {
|
||||
emit(const CommunitiesState(status: CommunitiesStatus.loading));
|
||||
final communities = await _communitiesService.getCommunity(event.param);
|
||||
emit(
|
||||
state.copyWith(status: CommunitiesStatus.loading),
|
||||
);
|
||||
|
||||
final paginationResponse = await _communitiesService.getCommunity(
|
||||
event.param,
|
||||
);
|
||||
|
||||
emit(
|
||||
CommunitiesState(
|
||||
status: CommunitiesStatus.success,
|
||||
communities: communities,
|
||||
communities: paginationResponse.data,
|
||||
hasNext: paginationResponse.hasNext,
|
||||
currentPage: paginationResponse.page,
|
||||
searchQuery: event.param.search,
|
||||
isLoadingMore: false,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
emit(
|
||||
CommunitiesState(
|
||||
status: CommunitiesStatus.failure,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
_onApiException(e, emit);
|
||||
} catch (e) {
|
||||
emit(
|
||||
CommunitiesState(
|
||||
status: CommunitiesStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
_onError(e, emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLoadMoreCommunities(
|
||||
LoadMoreCommunities event,
|
||||
Emitter<CommunitiesState> emit,
|
||||
) async {
|
||||
if (!state.hasNext || state.isLoadingMore) return;
|
||||
|
||||
try {
|
||||
emit(state.copyWith(isLoadingMore: true));
|
||||
|
||||
final param = LoadCommunitiesParam(
|
||||
page: state.currentPage + 1,
|
||||
search: state.searchQuery,
|
||||
);
|
||||
|
||||
final paginationResponse = await _communitiesService.getCommunity(param);
|
||||
|
||||
final updatedCommunities = List<CommunityModel>.from(state.communities)
|
||||
..addAll(paginationResponse.data);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommunitiesStatus.success,
|
||||
communities: updatedCommunities,
|
||||
hasNext: paginationResponse.hasNext,
|
||||
currentPage: paginationResponse.page,
|
||||
isLoadingMore: false,
|
||||
),
|
||||
);
|
||||
} on APIException catch (e) {
|
||||
_onApiException(e, emit);
|
||||
} catch (e) {
|
||||
_onError(e, emit);
|
||||
}
|
||||
}
|
||||
|
||||
void _onApiException(
|
||||
APIException e,
|
||||
Emitter<CommunitiesState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommunitiesStatus.failure,
|
||||
isLoadingMore: false,
|
||||
errorMessage: e.message,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onError(Object e, Emitter<CommunitiesState> emit) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: CommunitiesStatus.failure,
|
||||
isLoadingMore: false,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onInsertCommunity(
|
||||
InsertCommunity event,
|
||||
Emitter<CommunitiesState> emit,
|
||||
) {
|
||||
emit(state.copyWith(communities: [event.community, ...state.communities]));
|
||||
}
|
||||
}
|
||||
|
@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent {
|
||||
@override
|
||||
List<Object?> get props => [param];
|
||||
}
|
||||
|
||||
class LoadMoreCommunities extends CommunitiesEvent {
|
||||
const LoadMoreCommunities();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class InsertCommunity extends CommunitiesEvent {
|
||||
const InsertCommunity(this.community);
|
||||
|
||||
final CommunityModel community;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [community];
|
||||
}
|
||||
|
@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable {
|
||||
this.status = CommunitiesStatus.initial,
|
||||
this.communities = const [],
|
||||
this.errorMessage,
|
||||
this.isLoadingMore = false,
|
||||
this.hasNext = false,
|
||||
this.currentPage = 1,
|
||||
this.searchQuery = '',
|
||||
});
|
||||
|
||||
final CommunitiesStatus status;
|
||||
final List<CommunityModel> communities;
|
||||
final String? errorMessage;
|
||||
final bool isLoadingMore;
|
||||
final bool hasNext;
|
||||
final int currentPage;
|
||||
final String searchQuery;
|
||||
|
||||
CommunitiesState copyWith({
|
||||
CommunitiesStatus? status,
|
||||
List<CommunityModel>? communities,
|
||||
String? errorMessage,
|
||||
bool? isLoadingMore,
|
||||
bool? hasNext,
|
||||
int? currentPage,
|
||||
String? searchQuery,
|
||||
}) {
|
||||
return CommunitiesState(
|
||||
status: status ?? this.status,
|
||||
communities: communities ?? this.communities,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||
hasNext: hasNext ?? this.hasNext,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, communities, errorMessage];
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
communities,
|
||||
errorMessage,
|
||||
isLoadingMore,
|
||||
hasNext,
|
||||
currentPage,
|
||||
searchQuery,
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
|
||||
part 'communities_tree_selection_event.dart';
|
||||
part 'communities_tree_selection_state.dart';
|
||||
|
||||
class CommunitiesTreeSelectionBloc
|
||||
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
|
||||
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
|
||||
on<SelectCommunityEvent>(_onSelectCommunity);
|
||||
on<SelectSpaceEvent>(_onSelectSpace);
|
||||
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
|
||||
}
|
||||
|
||||
void _onSelectCommunity(
|
||||
SelectCommunityEvent event,
|
||||
Emitter<CommunitiesTreeSelectionState> emit,
|
||||
) {
|
||||
emit(
|
||||
CommunitiesTreeSelectionState(
|
||||
selectedCommunity: event.community,
|
||||
selectedSpace: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectSpace(
|
||||
SelectSpaceEvent event,
|
||||
Emitter<CommunitiesTreeSelectionState> emit,
|
||||
) {
|
||||
emit(
|
||||
CommunitiesTreeSelectionState(
|
||||
selectedCommunity: event.community,
|
||||
selectedSpace: event.space,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onClearSelection(
|
||||
ClearCommunitiesTreeSelectionEvent event,
|
||||
Emitter<CommunitiesTreeSelectionState> emit,
|
||||
) {
|
||||
emit(const CommunitiesTreeSelectionState());
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
part of 'communities_tree_selection_bloc.dart';
|
||||
|
||||
sealed class CommunitiesTreeSelectionEvent extends Equatable {
|
||||
const CommunitiesTreeSelectionEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
|
||||
final CommunityModel community;
|
||||
|
||||
const SelectCommunityEvent({required this.community});
|
||||
@override
|
||||
List<Object?> get props => [community];
|
||||
}
|
||||
|
||||
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
|
||||
final SpaceModel? space;
|
||||
final CommunityModel community;
|
||||
|
||||
const SelectSpaceEvent({required this.space, required this.community});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [space];
|
||||
}
|
||||
|
||||
final class ClearCommunitiesTreeSelectionEvent
|
||||
extends CommunitiesTreeSelectionEvent {
|
||||
const ClearCommunitiesTreeSelectionEvent();
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
part of 'communities_tree_selection_bloc.dart';
|
||||
|
||||
final class CommunitiesTreeSelectionState extends Equatable {
|
||||
const CommunitiesTreeSelectionState({
|
||||
this.selectedCommunity,
|
||||
this.selectedSpace,
|
||||
});
|
||||
|
||||
final CommunityModel? selectedCommunity;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
CommunitiesTreeSelectionState copyWith({
|
||||
CommunityModel? selectedCommunity,
|
||||
SpaceModel? selectedSpace,
|
||||
List<CommunityModel>? expandedCommunities,
|
||||
List<SpaceModel>? expandedSpaces,
|
||||
}) {
|
||||
return CommunitiesTreeSelectionState(
|
||||
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
|
||||
selectedSpace: selectedSpace ?? this.selectedSpace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
selectedCommunity,
|
||||
selectedSpace,
|
||||
];
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user