MVP release (#306)

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Description

MVP Release

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
This commit is contained in:
Faris Armoush
2025-06-29 10:56:48 +03:00
committed by GitHub
164 changed files with 7745 additions and 1967 deletions

View 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

View 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

View 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

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
);
final tvocValue = _getValueForStatus(
status,
'tvoc_value',
'voc_value',
formatter: (value) => (value / 100).toStringAsFixed(2),
);

View File

@ -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,
),
),
),

View File

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

View File

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

View File

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

View File

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

View File

@ -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: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),

View File

@ -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) {

View File

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

View File

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

View File

@ -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),
),
),
),

View File

@ -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(

View File

@ -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(

View File

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

View File

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

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

View File

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

View File

@ -50,20 +50,11 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
late ScrollController _horizontalHeaderScrollController;
late ScrollController _horizontalBodyScrollController;
@override
void initState() {
super.initState();
_initializeSelection();
_horizontalHeaderScrollController = ScrollController();
_horizontalBodyScrollController = ScrollController();
// Synchronize horizontal scrolling
_horizontalBodyScrollController.addListener(() {
_horizontalHeaderScrollController
.jumpTo(_horizontalBodyScrollController.offset);
});
}
@override
@ -113,94 +104,112 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
@override
void dispose() {
_horizontalHeaderScrollController.dispose();
_horizontalBodyScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: widget.cellDecoration,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: Scrollbar(
//fixed the horizontal scrollbar issue
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
child: Row(
child: Column(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
}),
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(
color: ColorsManager.boxColor,
),
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
})
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
],
),
),
SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex, widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
],
),
),
),
),
Expanded(
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: Scrollbar(
controller: _horizontalBodyScrollController,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _horizontalBodyScrollController,
child: Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children: List.generate(widget.data.length,
(rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(rowIndex,
widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
),
),
),
),
),
),
],
),
),
);
}
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(
width: 50,
@ -218,32 +227,6 @@ 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 _buildRowCheckbox(int index, double size) {
return Container(
width: 50,
@ -298,12 +281,8 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableCell(
String content,
double size, {
required int rowIndex,
required int columnIndex,
}) {
Widget _buildTableCell(String content, double size,
{required int rowIndex, required int columnIndex}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@ -311,7 +290,6 @@ class _DynamicTableState extends State<DynamicTable> {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
@ -416,11 +394,10 @@ class _DynamicTableState extends State<DynamicTable> {
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings, // ضع المسار الصحيح هنا
Assets.settings,
width: 40,
height: 22,
color: ColorsManager
.primaryColor, // نفس لون الأيقونة في الصورة
color: ColorsManager.primaryColor,
),
),
),

View File

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

View File

@ -40,18 +40,15 @@ 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(projectUuid, spacesId: spacesList));
}
}
@ -100,7 +97,7 @@ class DeviceManagementBloc
));
if (currentProductName.isNotEmpty) {
add(SearchDevices(productName: currentProductName));
add(SearchDevices(deviceNameOrProductName: currentProductName));
}
}
}
@ -274,29 +271,37 @@ class DeviceManagementBloc
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 +309,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,

View File

@ -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 {

View File

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

View File

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

View File

@ -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,
),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_event.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';

View File

@ -1,14 +1,13 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_bloc.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/seconds_picker.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/seconds_picker.dart';
class OpeningAndClosingTimeDialogBody extends StatefulWidget {
final ValueChanged<int> onDurationChanged;
final GarageDoorBloc bloc;
OpeningAndClosingTimeDialogBody({
const OpeningAndClosingTimeDialogBody({
required this.onDurationChanged,
required this.bloc,
});

View File

@ -26,7 +26,7 @@ class ScheduleGarageTableWidget extends StatelessWidget {
Table(
border: TableBorder.all(
color: ColorsManager.graysColor,
borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
children: [
TableRow(
@ -50,17 +50,20 @@ class ScheduleGarageTableWidget extends StatelessWidget {
BlocBuilder<GarageDoorBloc, GarageDoorState>(
builder: (context, state) {
if (state is ScheduleGarageLoadingState) {
return const SizedBox(height: 200, child: Center(child: CircularProgressIndicator()));
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()));
}
if (state is GarageDoorLoadedState && state.status.schedules?.isEmpty == true) {
if (state is GarageDoorLoadedState &&
state.status.schedules!.isEmpty) {
return _buildEmptyState(context);
} else if (state is GarageDoorLoadedState) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius:
const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(
bottom: Radius.circular(20)),
),
child: _buildTableBody(state, context));
}
@ -78,7 +81,7 @@ class ScheduleGarageTableWidget extends StatelessWidget {
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Center(
child: Column(
@ -112,7 +115,8 @@ class ScheduleGarageTableWidget extends StatelessWidget {
children: [
if (state.status.schedules != null)
for (int i = 0; i < state.status.schedules!.length; i++)
_buildScheduleRow(state.status.schedules![i], i, context, state),
_buildScheduleRow(
state.status.schedules![i], i, context, state),
],
),
),
@ -134,7 +138,8 @@ class ScheduleGarageTableWidget extends StatelessWidget {
);
}
TableRow _buildScheduleRow(ScheduleModel schedule, int index, BuildContext context, GarageDoorLoadedState state) {
TableRow _buildScheduleRow(ScheduleModel schedule, int index,
BuildContext context, GarageDoorLoadedState state) {
return TableRow(
children: [
Center(
@ -152,7 +157,8 @@ class ScheduleGarageTableWidget extends StatelessWidget {
width: 24,
height: 24,
child: schedule.enable
? const Icon(Icons.radio_button_checked, color: ColorsManager.blueColor)
? const Icon(Icons.radio_button_checked,
color: ColorsManager.blueColor)
: const Icon(
Icons.radio_button_unchecked,
color: ColorsManager.grayColor,
@ -160,7 +166,9 @@ class ScheduleGarageTableWidget extends StatelessWidget {
),
),
),
Center(child: Text(_getSelectedDays(ScheduleModel.parseSelectedDays(schedule.days)))),
Center(
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
@ -170,18 +178,24 @@ class ScheduleGarageTableWidget extends StatelessWidget {
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
GarageDoorDialogHelper.showAddGarageDoorScheduleDialog(context,
schedule: schedule, index: index, isEdit: true);
GarageDoorDialogHelper.showAddGarageDoorScheduleDialog(
context,
schedule: schedule,
index: index,
isEdit: true);
},
child: Text(
'Edit',
style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor),
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
context.read<GarageDoorBloc>().add(DeleteGarageDoorScheduleEvent(
context
.read<GarageDoorBloc>()
.add(DeleteGarageDoorScheduleEvent(
index: index,
scheduleId: schedule.scheduleId,
deviceId: state.status.uuid,
@ -189,7 +203,8 @@ class ScheduleGarageTableWidget extends StatelessWidget {
},
child: Text(
'Delete',
style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.blueColor),
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
],

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule__garage_table.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';

View File

@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_header.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart';
class BuildGarageDoorScheduleView extends StatefulWidget {
const BuildGarageDoorScheduleView({super.key, required this.status});

View File

@ -5,7 +5,7 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_
import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_state.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/widgets/schedule_garage_view.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart';
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';

View File

@ -3,11 +3,14 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.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/toggle_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout {
class OneGangGlassSwitchControlView extends StatelessWidget
with HelperResponsiveLayout {
final String deviceId;
const OneGangGlassSwitchControlView({required this.deviceId, super.key});
@ -16,7 +19,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)),
OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)
..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<OneGangGlassSwitchBloc, OneGangGlassSwitchState>(
builder: (context, state) {
if (state is OneGangGlassSwitchLoading) {
@ -33,7 +37,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv
);
}
Widget _buildStatusControls(BuildContext context, OneGangGlassStatusModel status) {
Widget _buildStatusControls(
BuildContext context, OneGangGlassStatusModel status) {
final isExtraLarge = isExtraLargeScreenSize(context);
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
@ -76,14 +81,21 @@ class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiv
onChange: (value) {},
showToggle: false,
),
ToggleWidget(
value: false,
code: '',
deviceId: deviceId,
label: 'Scheduling',
icon: Assets.scheduling,
onChange: (value) {},
showToggle: false,
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<OneGangGlassSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
),
));
},
mainText: '',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
],
);

View File

@ -5,7 +5,10 @@ import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_lig
import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.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/toggle_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class WallLightDeviceControl extends StatelessWidget
@ -55,7 +58,6 @@ class WallLightDeviceControl extends StatelessWidget
mainAxisSpacing: 12,
),
children: [
const SizedBox(),
ToggleWidget(
value: status.switch1,
code: 'switch_1',
@ -69,7 +71,22 @@ class WallLightDeviceControl extends StatelessWidget
));
},
),
const SizedBox(),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<WallLightSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
),
));
},
mainText: '',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
],
);
}

View File

@ -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

View File

@ -0,0 +1,597 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'schedule_event.dart';
part 'schedule_state.dart';
class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
final String deviceId;
ScheduleBloc({
required this.deviceId,
}) : super(ScheduleInitial()) {
on<ScheduleInitializeAddEvent>(_initializeAddSchedule);
on<ScheduleUpdateSelectedTimeEvent>(_updateSelectedTime);
on<ScheduleUpdateSelectedDayEvent>(_updateSelectedDay);
on<ScheduleUpdateFunctionOnEvent>(_updateFunctionOn);
on<ScheduleGetEvent>(_getSchedule);
on<ScheduleAddEvent>(_onAddSchedule);
on<ScheduleEditEvent>(_onEditSchedule);
on<ScheduleUpdateEntryEvent>(_onUpdateSchedule);
on<UpdateScheduleModeEvent>(_onUpdateScheduleMode);
on<UpdateCountdownTimeEvent>(_onUpdateCountdownTime);
on<UpdateInchingTimeEvent>(_onUpdateInchingTime);
on<StartScheduleEvent>(_onStartScheduleEvent);
on<StopScheduleEvent>(_onStopScheduleEvent);
on<ScheduleDecrementCountdownEvent>(_onDecrementCountdown);
on<ScheduleFetchStatusEvent>(_fetchStatus);
on<ScheduleDeleteEvent>(_onDeleteSchedule);
}
Timer? _countdownTimer;
Duration countdownRemaining = Duration.zero;
Future<void> _onStopScheduleEvent(
StopScheduleEvent event,
Emitter<ScheduleState> emit,
) async {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
final success = await RemoteControlDeviceService().controlDevice(
deviceUuid: deviceId,
status: Status(
code: 'countdown_1',
value: 0,
),
);
if (success) {
_countdownTimer?.cancel();
if (event.mode == ScheduleModes.countdown) {
emit(currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
isCountdownActive: false,
countdownRemaining: Duration.zero,
));
} else if (event.mode == ScheduleModes.inching) {
emit(currentState.copyWith(
inchingHours: 0,
inchingMinutes: 0,
isInchingActive: false,
countdownRemaining: Duration.zero,
));
}
} else {
emit(const ScheduleError('Failed to stop schedule'));
}
}
}
void _onUpdateScheduleMode(
UpdateScheduleModeEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
scheduleMode: event.scheduleMode,
countdownRemaining: Duration.zero,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
));
}
}
void _onUpdateCountdownTime(
UpdateCountdownTimeEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
countdownSeconds: event.seconds,
countdownHours: event.hours,
countdownMinutes: event.minutes,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
}
}
void _onUpdateInchingTime(
UpdateInchingTimeEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
inchingHours: event.hours,
inchingMinutes: event.minutes,
countdownRemaining: Duration.zero,
inchingSeconds: 0, // Add this
));
}
}
void _initializeAddSchedule(
ScheduleInitializeAddEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
selectedTime: event.selectedTime,
selectedDays: event.selectedDays ?? List.filled(7, false),
functionOn: event.functionOn ?? false,
isEditing: event.isEditing,
scheduleMode: event.scheduleMode,
countdownRemaining: Duration.zero,
));
} else {
emit(ScheduleLoaded(
schedules: const [],
selectedTime: event.selectedTime,
selectedDays: event.selectedDays ?? List.filled(7, false),
functionOn: event.functionOn ?? false,
isEditing: event.isEditing,
deviceId: deviceId,
scheduleMode: event.scheduleMode,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
));
}
}
void _updateSelectedTime(
ScheduleUpdateSelectedTimeEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
selectedTime: event.selectedTime,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
}
}
void _updateSelectedDay(
ScheduleUpdateSelectedDayEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
final updatedDays = List<bool>.from(currentState.selectedDays);
updatedDays[event.index] = event.value;
emit(currentState.copyWith(
selectedDays: updatedDays,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
}
}
void _updateFunctionOn(
ScheduleUpdateFunctionOnEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
functionOn: event.isOn,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
}
}
Future<void> _getSchedule(
ScheduleGetEvent event,
Emitter<ScheduleState> emit,
) async {
try {
emit(ScheduleLoading());
final schedules = await DevicesManagementApi().getDeviceSchedules(
deviceId,
event.category,
);
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
schedules: schedules,
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
countdownRemaining: Duration.zero,
));
} else {
emit(ScheduleLoaded(
schedules: schedules,
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
deviceId: deviceId,
scheduleMode: ScheduleModes.schedule,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
));
}
} catch (e) {
emit(ScheduleError('Failed to load schedules: $e'));
}
}
Future<void> _onAddSchedule(
ScheduleAddEvent event,
Emitter<ScheduleState> emit,
) async {
try {
if (state is ScheduleLoaded) {
final dateTime = DateTime.parse(event.time);
final success = await DevicesManagementApi().postSchedule(
category: event.category,
deviceId: deviceId,
time: getTimeStampWithoutSeconds(dateTime).toString(),
code: event.code ?? event.category,
value: event.functionOn,
days: event.selectedDays);
if (success) {
add(ScheduleGetEvent(category: event.category));
} else {
emit(const ScheduleError('Failed to add schedule'));
}
}
} catch (e) {
emit(ScheduleError('Failed to add schedule: $e'));
}
}
Future<void> _onEditSchedule(
ScheduleEditEvent event,
Emitter<ScheduleState> emit,
) async {
try {
if (state is ScheduleLoaded) {
final dateTime = DateTime.parse(event.time);
final updatedSchedule = ScheduleEntry(
scheduleId: event.scheduleId,
category: event.category,
time: getTimeStampWithoutSeconds(dateTime).toString(),
function: Status(code: event.category, value: event.functionOn),
days: event.selectedDays,
);
final success = await DevicesManagementApi().editScheduleRecord(
deviceId,
updatedSchedule,
);
if (success) {
add(ScheduleGetEvent(
category: event.category,
));
} else {
emit(const ScheduleError('Failed to update schedule'));
}
}
} catch (e) {
emit(ScheduleError('Failed to update schedule: $e'));
}
}
Future<void> _onUpdateSchedule(
ScheduleUpdateEntryEvent event,
Emitter<ScheduleState> emit,
) async {
try {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
final updatedSchedules = currentState.schedules.map((schedule) {
if (schedule.scheduleId == event.scheduleId) {
return schedule.copyWith(
function: Status(code: event.category, value: event.functionOn),
enable: event.enable,
);
}
return schedule;
}).toList();
final success = await DevicesManagementApi().updateScheduleRecord(
enable: event.enable,
uuid: deviceId,
scheduleId: event.scheduleId,
);
if (success) {
emit(currentState.copyWith(
schedules: updatedSchedules,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
} else {
emit(const ScheduleError('Failed to update schedule status'));
}
}
} catch (e) {
emit(ScheduleError('Failed to update schedule: $e'));
}
}
Future<void> _onDeleteSchedule(
ScheduleDeleteEvent event,
Emitter<ScheduleState> emit,
) async {
try {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
final success = await DevicesManagementApi().deleteScheduleRecord(
deviceId,
event.scheduleId,
);
if (success) {
final updatedSchedules = currentState.schedules
.where((s) => s.scheduleId != event.scheduleId)
.toList();
emit(currentState.copyWith(
schedules: updatedSchedules,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
countdownRemaining: Duration.zero,
));
} else {
emit(const ScheduleError('Failed to delete schedule'));
}
}
} catch (e) {
emit(ScheduleError('Failed to delete schedule: $e'));
}
}
Duration? _currentCountdown;
Future<void> _onStartScheduleEvent(
StartScheduleEvent event,
Emitter<ScheduleState> emit,
) async {
if (state is ScheduleLoaded) {
final totalSeconds =
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
final code = event.mode == ScheduleModes.countdown
? 'countdown_1'
: 'switch_inching';
final currentState = state as ScheduleLoaded;
final duration = Duration(seconds: totalSeconds);
_currentCountdown = duration;
emit(currentState.copyWith(
countdownRemaining: duration,
schedules: currentState.schedules.map((schedule) {
if (schedule.function.code == code) {
return schedule.copyWith(
function: Status(code: code, value: totalSeconds),
);
}
return schedule;
}).toList(),
countdownHours: event.mode == ScheduleModes.countdown ? event.hours : 0,
));
final success = await RemoteControlDeviceService().controlDevice(
deviceUuid: deviceId,
status: Status(
code: code,
value: totalSeconds,
),
);
if (success) {
if (code == 'countdown_1') {
final countdownDuration = Duration(seconds: totalSeconds);
emit(
currentState.copyWith(
countdownHours: countdownDuration.inHours,
countdownMinutes: countdownDuration.inMinutes % 60,
countdownRemaining: countdownDuration,
isCountdownActive: true,
countdownSeconds: countdownDuration.inSeconds,
),
);
if (countdownDuration.inSeconds > 0) {
_startCountdownTimer(emit, countdownDuration);
} else {
_countdownTimer?.cancel();
emit(
currentState.copyWith(
countdownHours: 0,
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
countdownSeconds: 0,
),
);
}
} else if (code == 'switch_inching') {
final inchingDuration = Duration(seconds: totalSeconds);
emit(
currentState.copyWith(
inchingHours: inchingDuration.inHours,
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: true,
countdownRemaining: inchingDuration,
countdownSeconds: inchingDuration.inSeconds,
),
);
}
}
}
}
void _startCountdownTimer(
Emitter<ScheduleState> emit,
Duration duration,
) {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
_currentCountdown = _currentCountdown! - const Duration(seconds: 1);
countdownRemaining = _currentCountdown!;
add(const ScheduleDecrementCountdownEvent());
} else {
timer.cancel();
add(StopScheduleEvent(
mode: _currentCountdown == null
? ScheduleModes.countdown
: ScheduleModes.inching,
deviceId: deviceId,
));
}
});
}
void _onDecrementCountdown(
ScheduleDecrementCountdownEvent event,
Emitter<ScheduleState> emit,
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
countdownRemaining: countdownRemaining,
));
}
}
@override
Future<void> close() {
_countdownTimer?.cancel();
return super.close();
}
Future<void> _fetchStatus(
ScheduleFetchStatusEvent event,
Emitter<ScheduleState> emit,
) async {
emit(ScheduleLoading());
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
print(status.status);
final deviceStatus =
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
final scheduleMode =
deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0
? ScheduleModes.countdown
: deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0
? ScheduleModes.inching
: ScheduleModes.schedule;
final isCountdown = scheduleMode == ScheduleModes.countdown;
final isInching = scheduleMode == ScheduleModes.inching;
Duration? countdownRemaining;
var isCountdownActive = false;
var isInchingActive = false;
if (isCountdown) {
countdownRemaining = Duration(
hours: deviceStatus.countdownHours,
minutes: deviceStatus.countdownMinutes,
);
isCountdownActive = countdownRemaining > Duration.zero;
} else if (isInching) {
isInchingActive = Duration(
hours: deviceStatus.inchingHours,
minutes: deviceStatus.inchingMinutes,
) >
Duration.zero;
}
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
scheduleMode: scheduleMode,
countdownHours: deviceStatus.countdownHours,
countdownMinutes: deviceStatus.countdownMinutes,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: countdownRemaining ?? Duration.zero,
));
} else {
emit(ScheduleLoaded(
schedules: const [],
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
deviceId: deviceId,
scheduleMode: scheduleMode,
countdownHours: deviceStatus.countdownHours,
countdownMinutes: deviceStatus.countdownMinutes,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: countdownRemaining ?? Duration.zero,
));
}
// if (isCountdownActive && countdownRemaining != null) {
// _startCountdownTimer(emit, countdownRemaining);
// }
} catch (e) {
emit(ScheduleError('Failed to fetch device status: $e'));
}
}
String extractTime(String isoDateTime) {
return isoDateTime.split('T')[1].split('.')[0];
}
int? getTimeStampWithoutSeconds(DateTime? dateTime) {
if (dateTime == null) return null;
DateTime dateTimeWithoutSeconds = DateTime(dateTime.year, dateTime.month,
dateTime.day, dateTime.hour, dateTime.minute);
return dateTimeWithoutSeconds.millisecondsSinceEpoch ~/ 1000;
}
}

View File

@ -0,0 +1,236 @@
part of 'schedule_bloc.dart';
abstract class ScheduleEvent extends Equatable {
const ScheduleEvent();
}
class ScheduleInitializeAddEvent extends ScheduleEvent {
final bool isEditing;
final ScheduleModes scheduleMode;
final TimeOfDay? selectedTime;
final List<bool>? selectedDays;
final bool? functionOn;
const ScheduleInitializeAddEvent({
required this.isEditing,
required this.scheduleMode,
this.selectedTime,
this.selectedDays,
this.functionOn,
});
@override
List<Object?> get props => [
isEditing,
scheduleMode,
selectedTime,
selectedDays,
functionOn,
];
}
class ScheduleUpdateSelectedTimeEvent extends ScheduleEvent {
final TimeOfDay selectedTime;
const ScheduleUpdateSelectedTimeEvent(this.selectedTime);
@override
List<Object> get props => [selectedTime];
}
class ScheduleUpdateSelectedDayEvent extends ScheduleEvent {
final int index;
final bool value;
const ScheduleUpdateSelectedDayEvent(this.index, this.value);
@override
List<Object> get props => [index, value];
}
class ScheduleUpdateFunctionOnEvent extends ScheduleEvent {
final bool isOn;
const ScheduleUpdateFunctionOnEvent(this.isOn);
@override
List<Object> get props => [isOn];
}
class ScheduleGetEvent extends ScheduleEvent {
final String category;
const ScheduleGetEvent({required this.category});
@override
List<Object> get props => [category];
}
class ScheduleAddEvent extends ScheduleEvent {
final String category;
final String time;
final List<String> selectedDays;
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, code];
}
class ScheduleEditEvent extends ScheduleEvent {
final String scheduleId;
final String category;
final String time;
final List<String> selectedDays;
final bool functionOn;
const ScheduleEditEvent({
required this.scheduleId,
required this.category,
required this.time,
required this.selectedDays,
required this.functionOn,
});
@override
List<Object> get props => [
scheduleId,
category,
time,
selectedDays,
functionOn,
];
}
class ScheduleDeleteEvent extends ScheduleEvent {
final String scheduleId;
const ScheduleDeleteEvent(this.scheduleId);
@override
List<Object> get props => [scheduleId];
}
class ScheduleUpdateEntryEvent extends ScheduleEvent {
final String scheduleId;
final bool functionOn;
final bool enable;
final String category;
const ScheduleUpdateEntryEvent({
required this.scheduleId,
required this.functionOn,
required this.enable,
required this.category,
});
@override
List<Object> get props => [scheduleId, functionOn, enable, category];
}
class UpdateScheduleModeEvent extends ScheduleEvent {
final ScheduleModes scheduleMode;
const UpdateScheduleModeEvent({required this.scheduleMode});
@override
List<Object> get props => [scheduleMode];
}
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, seconds];
}
class UpdateInchingTimeEvent extends ScheduleEvent {
final int hours;
final int minutes;
const UpdateInchingTimeEvent({
required this.hours,
required this.minutes,
});
@override
List<Object> get props => [hours, minutes];
}
class StartScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final int hours;
final int minutes;
const StartScheduleEvent({
required this.mode,
required this.hours,
required this.minutes,
});
@override
List<Object?> get props => [mode, hours, minutes];
}
class StopScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final String deviceId;
const StopScheduleEvent({
required this.mode,
required this.deviceId,
});
@override
List<Object?> get props => [mode, deviceId];
}
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
const ScheduleDecrementCountdownEvent();
@override
List<Object> get props => [];
}
class ScheduleFetchStatusEvent extends ScheduleEvent {
final String deviceId;
const ScheduleFetchStatusEvent(this.deviceId);
@override
List<Object> get props => [deviceId];
}
class DeleteScheduleEvent extends ScheduleEvent {
final String scheduleId;
const DeleteScheduleEvent(this.scheduleId);
@override
List<Object> get props => [scheduleId];
}
class StatusUpdatedScheduleEvent extends ScheduleEvent {
final String id;
const StatusUpdatedScheduleEvent(this.id);
@override
List<Object> get props => [id];
}

View File

@ -0,0 +1,120 @@
part of 'schedule_bloc.dart';
abstract class ScheduleState extends Equatable {
const ScheduleState();
}
class ScheduleInitial extends ScheduleState {
@override
List<Object> get props => [];
}
class ScheduleLoading extends ScheduleState {
@override
List<Object> get props => [];
}
class ScheduleLoaded extends ScheduleState {
final List<ScheduleModel> schedules;
final TimeOfDay? selectedTime;
final List<bool> selectedDays;
final bool functionOn;
final bool isEditing;
final String deviceId;
final int countdownHours;
final int countdownMinutes;
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,
required this.functionOn,
required this.isEditing,
required this.deviceId,
this.countdownHours = 0,
this.countdownMinutes = 0,
this.isCountdownActive = false,
this.inchingHours = 0,
this.inchingMinutes = 0,
this.isInchingActive = false,
this.scheduleMode = ScheduleModes.countdown,
this.countdownRemaining,
});
ScheduleLoaded copyWith({
List<ScheduleModel>? schedules,
TimeOfDay? selectedTime,
List<bool>? selectedDays,
bool? functionOn,
bool? isEditing,
int? countdownHours,
int? countdownMinutes,
bool? isCountdownActive,
int? inchingHours,
int? inchingMinutes,
bool? isInchingActive,
ScheduleModes? scheduleMode,
Duration? countdownRemaining,
String? deviceId,
int? countdownSeconds,
int? inchingSeconds,
}) {
return ScheduleLoaded(
schedules: schedules ?? this.schedules,
selectedTime: selectedTime ?? this.selectedTime,
selectedDays: selectedDays ?? this.selectedDays,
functionOn: functionOn ?? this.functionOn,
isEditing: isEditing ?? this.isEditing,
deviceId: deviceId ?? this.deviceId,
countdownHours: countdownHours ?? this.countdownHours,
countdownMinutes: countdownMinutes ?? this.countdownMinutes,
isCountdownActive: isCountdownActive ?? this.isCountdownActive,
inchingHours: inchingHours ?? this.inchingHours,
inchingMinutes: inchingMinutes ?? this.inchingMinutes,
isInchingActive: isInchingActive ?? this.isInchingActive,
scheduleMode: scheduleMode ?? this.scheduleMode,
countdownRemaining: countdownRemaining ?? this.countdownRemaining,
countdownSeconds: countdownSeconds ?? this.countdownSeconds,
inchingSeconds: inchingSeconds ?? this.inchingSeconds,
);
}
@override
List<Object?> get props => [
schedules,
selectedTime,
selectedDays,
functionOn,
isEditing,
deviceId,
countdownHours,
countdownMinutes,
isCountdownActive,
inchingHours,
inchingMinutes,
isInchingActive,
scheduleMode,
countdownRemaining,
countdownSeconds,
inchingSeconds,
];
}
class ScheduleError extends ScheduleState {
final String error;
const ScheduleError(this.error);
@override
List<Object> get props => [error];
}

View File

@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CountdownModeButtons extends StatelessWidget {
@ -38,14 +39,10 @@ class CountdownModeButtons extends StatelessWidget {
? DefaultButton(
height: 40,
onPressed: () {
context
.read<WaterHeaterBloc>()
.add(StopScheduleEvent(deviceId));
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: deviceId,
code: 'countdown_1',
value: 0,
),
);
},
@ -55,12 +52,11 @@ class CountdownModeButtons extends StatelessWidget {
: DefaultButton(
height: 40,
onPressed: () {
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'countdown_1',
value: Duration(hours: hours, minutes: minutes)
.inSeconds,
context.read<ScheduleBloc>().add(
StartScheduleEvent(
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
),
);
},

View File

@ -0,0 +1,239 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CountdownInchingView extends StatefulWidget {
final String deviceId;
const CountdownInchingView({super.key, required this.deviceId});
@override
State<CountdownInchingView> createState() => _CountdownInchingViewState();
}
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, int displaySeconds) {
if (_lastHours != displayHours) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_hoursController.hasClients) {
_hoursController.jumpToItem(displayHours);
}
});
_lastHours = displayHours;
}
if (_lastMinutes != displayMinutes) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_minutesController.hasClients) {
_minutesController.jumpToItem(displayMinutes);
}
});
_lastMinutes = displayMinutes;
}
// Update seconds controller
if (_lastSeconds != displaySeconds) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_secondsController.hasClients) {
_secondsController.jumpToItem(displaySeconds);
}
});
_lastSeconds = displaySeconds;
}
}
@override
Widget build(BuildContext context) {
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;
final displayHours = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inHours
: (isCountDown ? state.countdownHours : state.inchingHours);
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,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isCountDown ? 'Countdown:' : 'Inching:',
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
const SizedBox(height: 8),
Visibility(
visible: !isCountDown,
child: const Text(
'Once enabled this feature, each time the device is turned on, '
'it will automatically turn off after a preset time.',
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildPickerColumn(
context,
'h',
displayHours,
100,
_hoursController,
(value) {
if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
hours: value,
minutes: displayMinutes,
seconds: displaySeconds,
));
}
},
isActive: isActive,
),
const SizedBox(width: 10),
_buildPickerColumn(
context,
'm',
displayMinutes,
60,
_minutesController,
(value) {
if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
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,
),
],
),
],
);
},
);
}
Widget _buildPickerColumn(
BuildContext context,
String label,
int initialValue,
int itemCount,
FixedExtentScrollController controller,
ValueChanged<int> onSelected, {
required bool isActive,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 40,
width: 80,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(8),
),
child: ListWheelScrollView.useDelegate(
controller: controller,
itemExtent: 40.0,
physics: isActive
? const NeverScrollableScrollPhysics()
: const FixedExtentScrollPhysics(),
onSelectedItemChanged: isActive ? null : onSelected,
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
return Center(
child: Text(
index.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 24,
color: isActive ? ColorsManager.grayColor : Colors.black,
),
),
);
},
childCount: itemCount,
),
),
),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 18,
),
),
],
);
}
}

View File

@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'
hide StopScheduleEvent;
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class InchingModeButtons extends StatelessWidget {
@ -38,15 +41,9 @@ class InchingModeButtons extends StatelessWidget {
? DefaultButton(
height: 40,
onPressed: () {
context
.read<WaterHeaterBloc>()
.add(StopScheduleEvent(deviceId));
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'switch_inching',
value: 0,
),
context.read<ScheduleBloc>().add(
StopScheduleEvent(
deviceId: deviceId, mode: ScheduleModes.inching),
);
},
backgroundColor: Colors.red,

View File

@ -0,0 +1,127 @@
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';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart';
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,
this.code,
});
final String deviceUuid;
final String category;
final String? code;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(deviceUuid)),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: 700,
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20),
child: BlocBuilder<ScheduleBloc, ScheduleState>(
builder: (context, state) {
if (state is ScheduleLoaded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ScheduleHeader(),
const SizedBox(height: 20),
ScheduleModeSelector(
currentMode: state.scheduleMode,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule)
ScheduleManagementUI(
category: category,
deviceUuid: deviceUuid,
onAddSchedule: () async {
final entry = await ScheduleDialogHelper
.showAddScheduleDialog(
context,
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: category,
code: entry.function.code,
time: entry.time,
functionOn: entry.function.value,
selectedDays: entry.days,
),
);
}
},
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
isActive: state.isCountdownActive,
deviceId: deviceUuid,
hours: state.countdownHours,
minutes: state.countdownMinutes,
),
if (state.scheduleMode == ScheduleModes.inching)
InchingModeButtons(
isActive: state.isInchingActive,
deviceId: deviceUuid,
hours: state.inchingHours,
minutes: state.inchingMinutes,
),
if (state.scheduleMode != ScheduleModes.countdown &&
state.scheduleMode != ScheduleModes.inching)
ScheduleModeButtons(
onSave: () => Navigator.pop(context),
),
],
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ScheduleControlButton extends StatelessWidget {
final VoidCallback onTap;
final String mainText;
final String subtitle;
final String iconPath;
const ScheduleControlButton({
super.key,
required this.onTap,
required this.mainText,
required this.subtitle,
required this.iconPath,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: DeviceControlsContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: ColorsManager.whiteColors,
),
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(12),
child: ClipOval(
child: SvgPicture.asset(
iconPath,
fit: BoxFit.fill,
),
),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
mainText,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w200,
fontSize: 12,
color: ColorsManager.blackColor,
),
),
Text(
subtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.blackColor,
),
),
],
),
],
),
),
);
}
}

View File

@ -1,18 +1,19 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleManagementUI extends StatelessWidget {
final WaterHeaterDeviceStatusLoaded state;
final Function onAddSchedule;
final String deviceUuid;
final VoidCallback onAddSchedule;
final String category;
const ScheduleManagementUI({
super.key,
required this.state,
required this.deviceUuid,
required this.onAddSchedule,
this.category = 'switch_1',
});
@override
@ -28,7 +29,7 @@ class ScheduleManagementUI extends StatelessWidget {
padding: 2,
backgroundColor: ColorsManager.graysColor,
borderRadius: 15,
onPressed: () => onAddSchedule(),
onPressed: onAddSchedule,
child: Row(
children: [
const Icon(Icons.add, color: ColorsManager.primaryColor),
@ -43,7 +44,7 @@ class ScheduleManagementUI extends StatelessWidget {
),
),
const SizedBox(height: 20),
ScheduleTableWidget(state: state),
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
],
);
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleModeSelector extends StatelessWidget {
final ScheduleModes currentMode;
const ScheduleModeSelector({
super.key,
required this.currentMode,
});
@override
Widget build(BuildContext context) {
final currentMode = context.select<ScheduleBloc, ScheduleModes>(
(bloc) => bloc.state is ScheduleLoaded &&
(bloc.state as ScheduleLoaded).scheduleMode != null
? (bloc.state as ScheduleLoaded).scheduleMode
: ScheduleModes.schedule,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Type:',
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildRadioTile(
context, 'Countdown', ScheduleModes.countdown, currentMode),
_buildRadioTile(
context, 'Schedule', ScheduleModes.schedule, currentMode),
// _buildRadioTile(
// context, 'Circulate', ScheduleModes.circulate, currentMode),
// _buildRadioTile(
// context, 'Inching', ScheduleModes.inching, currentMode),
],
),
],
);
}
Widget _buildRadioTile(
BuildContext context,
String label,
ScheduleModes mode,
ScheduleModes currentMode,
) {
return Flexible(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
label,
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.blackColor,
),
),
leading: Radio<ScheduleModes>(
value: mode,
groupValue: currentMode,
onChanged: (ScheduleModes? value) {
if (value != null) {
context.read<ScheduleBloc>().add(
UpdateScheduleModeEvent(scheduleMode: value),
);
if (value == ScheduleModes.schedule) {
context.read<ScheduleBloc>().add(
const ScheduleGetEvent(category: 'switch_1'),
);
}
}
},
),
),
);
}
}

View File

@ -0,0 +1,293 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.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/schedule_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/format_date_time.dart';
class ScheduleTableWidget extends StatelessWidget {
final String deviceUuid;
final String category;
const ScheduleTableWidget({
super.key,
required this.deviceUuid,
this.category = 'switch_1',
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)..add(ScheduleGetEvent(category: category)),
child: _ScheduleTableView(),
);
}
}
class _ScheduleTableView extends StatelessWidget {
const _ScheduleTableView();
@override
Widget build(BuildContext context) {
return Column(
children: [
Table(
border: TableBorder.all(
color: ColorsManager.graysColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20)),
),
children: [
TableRow(
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
children: [
_buildTableHeader('Active'),
_buildTableHeader('Days'),
_buildTableHeader('Time'),
_buildTableHeader('Function'),
_buildTableHeader('Action'),
],
),
],
),
BlocBuilder<ScheduleBloc, ScheduleState>(
builder: (context, state) {
if (state is ScheduleLoading) {
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()));
}
if (state is ScheduleLoaded && state.schedules.isEmpty) {
return _buildEmptyState(context);
}
if (state is ScheduleLoaded) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20)),
),
child: _buildTableBody(state.schedules, context));
}
if (state is ScheduleError) {
return Center(child: Text(state.error));
}
return const SizedBox(height: 200);
},
),
],
);
}
Widget _buildEmptyState(BuildContext context) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'No schedules added yet',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
),
],
),
),
);
}
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
return SizedBox(
height: 200,
child: SingleChildScrollView(
child: Table(
border: TableBorder.all(color: ColorsManager.graysColor),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
for (int i = 0; i < schedules.length; i++)
_buildScheduleRow(schedules[i], i, context),
],
),
),
);
}
Widget _buildTableHeader(String label) {
return TableCell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
label,
style: const TextStyle(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
),
);
}
TableRow _buildScheduleRow(
ScheduleModel schedule, int index, BuildContext context) {
return TableRow(
children: [
Center(
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: temp,
// schedule.function.value,
enable: !schedule.enable,
),
);
},
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: schedule.enable
? const Icon(Icons.radio_button_checked,
color: ColorsManager.blueColor)
: const Icon(Icons.radio_button_unchecked,
color: ColorsManager.grayColor),
),
),
),
),
Center(
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
schedule.category == 'CUR_2'
? Center(
child: Text(schedule.function.value == true ? 'open' : 'close'))
: Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
child: Wrap(
runAlignment: WrapAlignment.center,
children: [
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
ScheduleDialogHelper.showAddScheduleDialog(
context,
schedule: ScheduleEntry.fromScheduleModel(schedule),
isEdit: true,
).then((updatedSchedule) {
if (updatedSchedule != null) {
context.read<ScheduleBloc>().add(
ScheduleEditEvent(
scheduleId: schedule.scheduleId,
category: schedule.category,
time: updatedSchedule.time,
functionOn: updatedSchedule.function.value,
selectedDays: updatedSchedule.days),
);
}
});
},
child: Text(
'Edit',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text('Confirm Delete'),
content: const Text(
'Are you sure you want to delete this schedule?'),
actions: [
TextButton(
onPressed: () =>
Navigator.of(dialogContext).pop(false),
child: Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.of(dialogContext).pop(true),
child: const Text(
'Delete',
style: TextStyle(color: Colors.red),
),
),
],
);
},
);
if (confirmed == true) {
context.read<ScheduleBloc>().add(
ScheduleDeleteEvent(schedule.scheduleId),
);
}
},
child: Text(
'Delete',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
)
],
),
),
],
);
}
String _getSelectedDays(List<bool> selectedDays) {
const days = ScheduleDialogHelper.allDays;
return selectedDays
.asMap()
.entries
.where((entry) => entry.value)
.map((entry) => days[entry.key])
.join(', ');
}
}

View File

@ -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(

View File

@ -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,
),
),
),

View File

@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import '../models/three_gang_glass_switch.dart';
class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout {
class ThreeGangGlassSwitchControlView extends StatelessWidget
with HelperResponsiveLayout {
final String deviceId;
const ThreeGangGlassSwitchControlView({required this.deviceId, super.key});
@ -17,7 +19,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)),
ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)
..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<ThreeGangGlassSwitchBloc, ThreeGangGlassSwitchState>(
builder: (context, state) {
if (state is ThreeGangGlassSwitchLoading) {
@ -34,7 +37,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons
);
}
Widget _buildStatusControls(BuildContext context, ThreeGangGlassStatusModel status) {
Widget _buildStatusControls(
BuildContext context, ThreeGangGlassStatusModel status) {
final isExtraLarge = isExtraLargeScreenSize(context);
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
@ -98,6 +102,54 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons
);
},
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<ThreeGangGlassSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
),
));
},
mainText: 'Wall Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<ThreeGangGlassSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceId,
),
));
},
mainText: 'Ceiling Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<ThreeGangGlassSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_3',
deviceUuid: deviceId,
),
));
},
mainText: 'SpotLight',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ToggleWidget(
value: false,
code: '',
@ -107,15 +159,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons
onChange: (value) {},
showToggle: false,
),
ToggleWidget(
value: false,
code: '',
deviceId: deviceId,
label: 'Scheduling',
icon: Assets.scheduling,
onChange: (value) {},
showToggle: false,
),
],
);
}

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/three_gang_switch/bloc/living_room_bloc.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class LivingRoomDeviceControlsView extends StatelessWidget
@ -90,6 +93,54 @@ class LivingRoomDeviceControlsView extends StatelessWidget
);
},
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<LivingRoomBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
),
));
},
mainText: 'Wall Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<LivingRoomBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
),
));
},
mainText: 'Ceiling Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<LivingRoomBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_3',
),
));
},
mainText: 'Spotlight',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
],
);
}

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart';
@ -16,8 +18,9 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId)
..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)),
create: (context) =>
TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId)
..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)),
child: BlocBuilder<TwoGangGlassSwitchBloc, TwoGangGlassSwitchState>(
builder: (context, state) {
if (state is TwoGangGlassSwitchLoading) {
@ -92,14 +95,37 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
onChange: (value) {},
showToggle: false,
),
ToggleWidget(
value: false,
code: '',
deviceId: deviceId,
label: 'Scheduling',
icon: Assets.scheduling,
onChange: (value) {},
showToggle: false,
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
),
));
},
mainText: 'Wall Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
),
));
},
mainText: 'Ceiling Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
],
);

View File

@ -1,6 +1,8 @@
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/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/batch_control/factory_reset.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart';
@ -8,9 +10,11 @@ import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayout {
class TwoGangBatchControlView extends StatelessWidget
with HelperResponsiveLayout {
const TwoGangBatchControlView({super.key, required this.deviceIds});
final List<String> deviceIds;
@ -18,15 +22,17 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(TwoGangSwitchFetchBatchEvent(deviceIds)),
create: (context) =>
TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first)
..add(TwoGangSwitchFetchBatchEvent(deviceIds)),
child: BlocBuilder<TwoGangSwitchBloc, TwoGangSwitchState>(
builder: (context, state) {
if (state is TwoGangSwitchLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is TwoGangSwitchStatusLoaded) {
return _buildStatusControls(context, state.status);
} else if (state is TwoGangSwitchError || state is TwoGangSwitchControlError) {
} else if (state is TwoGangSwitchError ||
state is TwoGangSwitchControlError) {
return const Center(child: Text('Error fetching status'));
} else {
return const Center(child: CircularProgressIndicator());
@ -82,6 +88,39 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou
));
},
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceIds.first,
),
));
},
mainText: 'Wall Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangSwitchBloc>(context),
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceIds.first,
),
));
},
mainText: 'Ceiling Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
// FirmwareUpdateWidget(
// deviceId: deviceIds.first,
// version: 12,

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class TwoGangDeviceControlView extends StatelessWidget
@ -37,43 +40,101 @@ class TwoGangDeviceControlView extends StatelessWidget
Widget _buildStatusControls(BuildContext context, TwoGangStatusModel status) {
return Center(
child: Wrap(
alignment: WrapAlignment.center,
spacing: 12,
runSpacing: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 200,
child: ToggleWidget(
value: status.switch1,
code: 'switch_1',
deviceId: deviceId,
label: 'Wall Light',
onChange: (value) {
context.read<TwoGangSwitchBloc>().add(TwoGangSwitchControl(
deviceId: deviceId,
code: 'switch_1',
value: value,
));
},
),
),
SizedBox(
width: 200,
child: ToggleWidget(
value: status.switch2,
code: 'switch_2',
deviceId: deviceId,
label: 'Ceiling Light',
onChange: (value) {
context.read<TwoGangSwitchBloc>().add(TwoGangSwitchControl(
deviceId: deviceId,
code: 'switch_2',
value: value,
));
},
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 200,
height: 150,
child: ToggleWidget(
value: status.switch1,
code: 'switch_1',
deviceId: deviceId,
label: 'Wall Light',
onChange: (value) {
context.read<TwoGangSwitchBloc>().add(TwoGangSwitchControl(
deviceId: deviceId,
code: 'switch_1',
value: value,
));
},
),
),
const SizedBox(width: 10),
SizedBox(
width: 200,
height: 150,
child: ToggleWidget(
value: status.switch2,
code: 'switch_2',
deviceId: deviceId,
label: 'Ceiling Light',
onChange: (value) {
context.read<TwoGangSwitchBloc>().add(TwoGangSwitchControl(
deviceId: deviceId,
code: 'switch_2',
value: value,
));
},
),
),
],
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 200,
height: 150,
child: ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value:
BlocProvider.of<TwoGangSwitchBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
),
));
},
mainText: 'Wall Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
),
const SizedBox(width: 10),
SizedBox(
width: 200,
height: 150,
child: ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value:
BlocProvider.of<TwoGangSwitchBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
),
));
},
mainText: 'Ceiling Light',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
),
],
)
],
),
);

View File

@ -1,242 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
class ScheduleDialogHelper {
static void showAddScheduleDialog(BuildContext context, {ScheduleModel? schedule, int? index, bool? isEdit}) {
final bloc = context.read<WaterHeaterBloc>();
static const List<String> allDays = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat'
];
if (schedule == null) {
bloc.add((const UpdateSelectedTimeEvent(null)));
bloc.add(InitializeAddScheduleEvent(
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
));
} else {
final time = _convertStringToTimeOfDay(schedule.time);
final selectedDays = _convertDaysStringToBooleans(schedule.days);
static Future<ScheduleEntry?> showAddScheduleDialog(
BuildContext context, {
ScheduleEntry? schedule,
bool isEdit = false,
String? code,
}) {
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;
TimeOfDay selectedTime = initialTime;
List<bool> selectedDays = List.of(initialDays);
bloc.add(InitializeAddScheduleEvent(
selectedTime: time,
selectedDays: selectedDays,
functionOn: schedule.function.value,
isEditing: true,
index: index,
));
}
showDialog(
return showDialog<ScheduleEntry>(
context: context,
builder: (ctx) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<WaterHeaterBloc, WaterHeaterState>(
builder: (context, state) {
if (state is WaterHeaterDeviceStatusLoaded) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
return StatefulBuilder(
builder: (ctx, setState) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(),
Text(
'Scheduling',
style: context.textTheme.titleLarge!.copyWith(
color: ColorsManager.dialogBlueTitle,
const SizedBox(),
Text(
isEdit ? 'Edit Schedule' : 'Add Schedule',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
const SizedBox(),
],
),
const SizedBox(height: 24),
SizedBox(
width: 150,
height: 40,
child: DefaultButton(
padding: 8,
backgroundColor: ColorsManager.boxColor,
borderRadius: 15,
onPressed: () async {
TimeOfDay? time = await showTimePicker(
context: context,
initialTime: state.selectedTime ?? TimeOfDay.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: const ColorScheme.light(
primary: ColorsManager.primaryColor,
),
),
child: child!,
);
},
);
if (time != null) {
bloc.add(UpdateSelectedTimeEvent(time));
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
state.selectedTime == null ? 'Time' : state.selectedTime!.format(context),
style: context.textTheme.bodySmall!.copyWith(
color: ColorsManager.grayColor,
),
),
const Icon(
Icons.access_time,
color: ColorsManager.grayColor,
size: 18,
),
],
),
),
),
const SizedBox(height: 16),
_buildDayCheckboxes(context, state.selectedDays, isEdit: isEdit),
const SizedBox(height: 16),
_buildFunctionSwitch(context, state.functionOn, isEdit),
const SizedBox(),
],
),
actions: [
SizedBox(
width: 200,
child: DefaultButton(
height: 40,
onPressed: () {
Navigator.pop(context);
},
backgroundColor: ColorsManager.boxColor,
child: Text(
'Cancel',
style: context.textTheme.bodyMedium,
const SizedBox(height: 24),
SizedBox(
width: 150,
height: 40,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
),
SizedBox(
width: 200,
child: DefaultButton(
height: 40,
onPressed: () {
if (state.selectedTime != null) {
if (state.isEditing && index != null) {
bloc.add(EditWaterHeaterScheduleEvent(
scheduleId: schedule?.scheduleId ?? '',
category: 'switch_1',
time: state.selectedTime!,
selectedDays: state.selectedDays,
functionOn: state.functionOn,
));
} else {
bloc.add(AddScheduleEvent(
category: 'switch_1',
time: state.selectedTime!,
selectedDays: state.selectedDays,
functionOn: state.functionOn,
));
}
Navigator.pop(context);
}
},
backgroundColor: ColorsManager.primaryColor,
child: const Text('Save'),
onPressed: () async {
TimeOfDay? time = await showTimePicker(
context: ctx,
initialTime: selectedTime,
);
if (time != null) {
setState(() => selectedTime = time);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedTime.format(context),
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.grey),
),
const Icon(Icons.access_time,
color: Colors.grey, size: 18),
],
),
),
],
);
}
return const SizedBox();
},
),
),
const SizedBox(height: 16),
_buildDayCheckboxes(ctx, selectedDays, (i, v) {
setState(() => selectedDays[i] = v);
}),
const SizedBox(height: 16),
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
(v) {
setState(() => functionOn = v);
}),
],
),
actions: [
SizedBox(
width: 100,
child: OutlinedButton(
onPressed: () {
Navigator.pop(ctx, null);
},
child: const Text('Cancel'),
),
),
SizedBox(
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: code ?? 'switch_1',
value: temp,
// functionOn,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule?.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
),
),
],
);
},
);
},
);
}
static TimeOfDay _convertStringToTimeOfDay(String timeString) {
final regex = RegExp(r'^(\d{2}):(\d{2})$');
final match = regex.firstMatch(timeString);
if (match != null) {
final hour = int.parse(match.group(1)!);
final minute = int.parse(match.group(2)!);
return TimeOfDay(hour: hour, minute: minute);
} else {
throw const FormatException('Invalid time format');
}
static TimeOfDay _convertStringToTimeOfDay(String iso) {
final dt = DateTime.tryParse(iso);
if (dt != null) return TimeOfDay(hour: dt.hour, minute: dt.minute);
return const TimeOfDay(hour: 9, minute: 0);
}
static List<bool> _convertDaysStringToBooleans(List<String> selectedDays) {
final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
List<bool> daysBoolean = List.filled(7, false);
for (int i = 0; i < daysOfWeek.length; i++) {
if (selectedDays.contains(daysOfWeek[i])) {
daysBoolean[i] = true;
}
}
return daysBoolean;
return daysOfWeek
.map((d) =>
selectedDays.map((e) => e.toLowerCase()).contains(d.toLowerCase()))
.toList();
}
static Widget _buildDayCheckboxes(BuildContext context, List<bool> selectedDays, {bool? isEdit}) {
final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
static String _formatTimeOfDayToISO(TimeOfDay t) {
final now = DateTime.now();
final dt = DateTime(now.year, now.month, now.day, t.hour, t.minute);
return dt.toIso8601String();
}
static List<String> _convertSelectedDaysToStrings(List<bool> selectedDays) {
const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
List<String> result = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) result.add(allDays[i]);
}
return result;
}
static Widget _buildDayCheckboxes(BuildContext ctx, List<bool> selectedDays,
Function(int, bool) onChanged) {
final dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return Row(
children: List.generate(7, (index) {
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: List.generate(
7,
(index) => Row(
children: [
Checkbox(
value: selectedDays[index],
onChanged: (bool? value) {
context.read<WaterHeaterBloc>().add(UpdateSelectedDayEvent(index, value!));
},
onChanged: (val) => onChanged(index, val!),
),
Text(dayLabels[index]),
],
);
}),
),
),
);
}
static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) {
static Widget _buildFunctionSwitch(
String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) {
return Row(
children: [
Text(
'Function:',
style: context.textTheme.bodySmall!.copyWith(color: ColorsManager.grayColor),
style:
Theme.of(ctx).textTheme.bodySmall!.copyWith(color: Colors.grey),
),
const SizedBox(width: 10),
Radio<bool>(
value: true,
groupValue: isOn,
onChanged: (bool? value) {
context.read<WaterHeaterBloc>().add(const UpdateFunctionOnEvent(true));
},
onChanged: (val) => onChanged(true),
),
const Text('On'),
Text(categor == 'CUR_2' ? 'open' : 'On'),
const SizedBox(width: 10),
Radio<bool>(
value: false,
groupValue: isOn,
onChanged: (bool? value) {
context.read<WaterHeaterBloc>().add(const UpdateFunctionOnEvent(false));
},
onChanged: (val) => onChanged(false),
),
const Text('Off'),
Text(categor == 'CUR_2' ? 'close' : 'Off'),
],
);
}

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
class ScheduleEntry {
final String category;
@ -58,7 +59,8 @@ class ScheduleEntry {
String toJson() => json.encode(toMap());
factory ScheduleEntry.fromJson(String source) => ScheduleEntry.fromMap(json.decode(source));
factory ScheduleEntry.fromJson(String source) =>
ScheduleEntry.fromMap(json.decode(source));
@override
bool operator ==(Object other) {
@ -73,6 +75,23 @@ class ScheduleEntry {
@override
int get hashCode {
return category.hashCode ^ time.hashCode ^ function.hashCode ^ days.hashCode;
return category.hashCode ^
time.hashCode ^
function.hashCode ^
days.hashCode;
}
// Existing properties and methods
// Add the fromScheduleModel method
static ScheduleEntry fromScheduleModel(ScheduleModel scheduleModel) {
return ScheduleEntry(
days: scheduleModel.days,
time: scheduleModel.time,
function: scheduleModel.function,
category: scheduleModel.category,
scheduleId: scheduleModel.scheduleId,
);
}
}

View File

@ -16,7 +16,7 @@ class WaterHeaterStatusModel extends Equatable {
final String cycleTiming;
final List<ScheduleModel> schedules;
const WaterHeaterStatusModel({
const WaterHeaterStatusModel({
required this.uuid,
required this.heaterSwitch,
required this.countdownHours,

View File

@ -2,12 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart';
import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart';
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart';
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';
@ -35,7 +36,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget
state is WaterHeaterBatchFailedState) {
return const Center(child: Text('Error fetching status'));
} else {
return const SizedBox(height: 200, child: Center(child: SizedBox()));
return const SizedBox(
height: 200, child: Center(child: SizedBox()));
}
},
));
@ -73,48 +75,22 @@ class WaterHeaterDeviceControlView extends StatelessWidget
));
},
),
GestureDetector(
ScheduleControlButton(
onTap: () {
showDialog(
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<WaterHeaterBloc>(context),
child: BuildScheduleView(status: status),
child: BuildScheduleView(
deviceUuid: device.uuid ?? '',
category: 'switch_1',
),
));
},
child: DeviceControlsContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 60,
height: 60,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: ColorsManager.whiteColors,
),
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(12),
child: ClipOval(
child: SvgPicture.asset(
Assets.scheduling,
fit: BoxFit.fill,
),
),
),
const Spacer(),
Text(
'Scheduling',
textAlign: TextAlign.center,
style: context.textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.blackColor,
),
),
],
),
),
)
mainText: '',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
],
);
}

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