diff --git a/assets/images/autocad_occupancy_image.png b/assets/images/autocad_occupancy_image.png new file mode 100644 index 00000000..39d9625b Binary files /dev/null and b/assets/images/autocad_occupancy_image.png differ diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart new file mode 100644 index 00000000..bc811c56 --- /dev/null +++ b/lib/common/widgets/app_loading_indicator.dart @@ -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()); + } +} diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart index 3340a41d..869de23f 100644 --- a/lib/pages/analytics/models/analytics_device.dart +++ b/lib/pages/analytics/models/analytics_device.dart @@ -25,8 +25,8 @@ class AnalyticsDevice { factory AnalyticsDevice.fromJson(Map 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) : 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, ); } } diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart index 73e7d5d7..e4b7d730 100644 --- a/lib/pages/analytics/models/occupancy_heat_map_model.dart +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -14,12 +14,21 @@ class OccupancyHeatMapModel extends Equatable { }); factory OccupancyHeatMapModel.fromJson(Map 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()}', - ), - countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + eventDate: DateTime( + int.parse(year ?? '2025'), + int.parse(month ?? '1'), + int.parse(day ?? '1'), + ).toUtc(), + countTotalPresenceDetected: num.parse( + json['count_total_presence_detected']?.toString() ?? '0', + ).toInt(), ); } diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart index 0308d564..dfb48ecb 100644 --- a/lib/pages/analytics/models/range_of_aqi.dart +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable { factory RangeOfAqiValue.fromJson(Map 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(), ); } diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart index 223c0357..5c63e397 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -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().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; - loadAnalyticsDevices( - context, - communityUuid: communityUuid, - spaceUuid: spaceUuid, - ); + if (shouldFetchAnalyticsDevices) { + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); + } loadRangeOfAqi( context, spaceUuid: spaceUuid, diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart index 21cb2a9e..f46f2708 100644 --- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -18,11 +18,16 @@ abstract final class RangeOfAqiChartsHelper { (ColorsManager.hazardousPurple, 'Hazardous'), ]; - static FlTitlesData titlesData(BuildContext context, List data) { + static FlTitlesData titlesData( + BuildContext context, + List 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( diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart index b6d403eb..61179d15 100644 --- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -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()), ], ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart index ebe88614..23ae874e 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget { ); final tvocValue = _getValueForStatus( status, - 'tvoc_value', + 'voc_value', formatter: (value) => (value / 100).toStringAsFixed(2), ); diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart index 2f3d7ff0..4fdd8a2a 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -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 _buildBarGroups() { - return List.generate(chartData.length, (index) { - final data = chartData[index]; + final groups = []; + 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 = []; 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, ), ), ), diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart index 926d28e1..f7be6ee3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -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(); - 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(); + try { + final param = _makeLoadAqiDistributionParam(context, value); + bloc.add(LoadAirQualityDistribution(param)); + } catch (_) { + return; + } finally { + bloc.add(UpdateAqiTypeEvent(value)); + } } - } - }, + }, + ), ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart new file mode 100644 index 00000000..3a00925d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart @@ -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(), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart index fa0216a1..00233ad3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -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), - ), + ], ), ), ], diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart index 5d482d9c..6640c717 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -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); diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart index 5e731d90..0914eab3 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -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 chartData; + final AqiType selectedAqiType; const RangeOfAqiChart({ super.key, required this.chartData, + required this.selectedAqiType, }); List<(List values, Color color, Color? dotColor)> get _lines { @@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget { ]; } + (double maxY, double interval) get _maxYForAqiType { + const aqiMaxValues = { + 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: [ diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart index 6548c696..cb189dce 100644 --- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -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, + ), + ), ], ), ); diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart index eec31998..f79ecb44 100644 --- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -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, diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index b1af85c8..6b44e125 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -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 getTooltipItems(List touchedSpots) { return touchedSpots.map((spot) { return LineTooltipItem( - getToolTipLabel(spot.x, spot.y), + getToolTipLabel(spot.y), const TextStyle( color: ColorsManager.textPrimaryColor, fontWeight: FontWeight.w600, diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart index f88febcc..09fb6155 100644 --- a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -46,7 +46,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget { spacing: 32, children: [ Expanded( - flex: 2, + flex: 7, child: Column( spacing: 20, children: [ @@ -55,7 +55,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget { ], ), ), - Expanded(child: PowerClampEnergyDataWidget()), + Expanded(flex: 4, child: PowerClampEnergyDataWidget()), ], ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart index f7b33309..055e9675 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -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, diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index be5faf57..06b6c529 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -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'), ), ), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index e197c297..4d88471d 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -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), diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart index 0b01fda2..3bd96bce 100644 --- a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart +++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart @@ -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) { diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart index 679c9927..56f8ce08 100644 --- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -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()), ], @@ -31,12 +31,12 @@ class AnalyticsOccupancyView extends StatelessWidget { return SingleChildScrollView( child: Container( padding: _padding, - height: height * 0.9, + height: height * 1, child: const Row( spacing: 32, children: [ Expanded( - flex: 5, + flex: 7, child: Column( spacing: 20, children: [ @@ -45,7 +45,7 @@ class AnalyticsOccupancyView extends StatelessWidget { ], ), ), - Expanded(flex: 2, child: OccupancyEndSideBar()), + Expanded(flex: 4, child: OccupancyEndSideBar()), ], ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart index c7695064..66612a3e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart @@ -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, diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart index a652ae73..514ebb65 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart @@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State { 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), ), ), ), diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart index 70087c46..1205a66e 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -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( diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart index b3f162fa..7c9ed548 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -23,38 +23,45 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'Presnce Sensor'), - SizedBox( - height: MediaQuery.sizeOf(context).height * 0.2, - child: PowerClampEnergyStatusWidget( - status: [ - PowerClampEnergyStatus( - iconPath: Assets.presenceState, - title: 'Presence Status', - value: _valueFromCode( - 'presence_state', - state.deviceStatusList, + const AnalyticsSidebarHeader(title: 'Presence Sensor'), + Expanded( + child: SizedBox( + child: PowerClampEnergyStatusWidget( + status: [ + PowerClampEnergyStatus( + iconPath: Assets.presenceState, + title: 'Presence Status', + value: _valueFromCode( + 'presence_state', + state.deviceStatusList, + ), + unit: '', ), - unit: '', - ), - PowerClampEnergyStatus( - iconPath: Assets.presenceTimeIcon, - title: 'Presence Time', - value: - '${_valueFromCode('none_body_time', state.deviceStatusList)} Min', - unit: '', - ), - PowerClampEnergyStatus( - iconPath: Assets.currentDistanceIcon, - title: 'Detection Distance', - value: - '${_valueFromCode('space_move_val', state.deviceStatusList)} M', - unit: '', - ), - ], + PowerClampEnergyStatus( + iconPath: Assets.presenceTimeIcon, + title: 'Presence Time', + value: + '${_valueFromCode('none_body_time', state.deviceStatusList)} Min', + unit: '', + ), + PowerClampEnergyStatus( + iconPath: Assets.currentDistanceIcon, + title: 'Detection Distance', + value: + '${_valueFromCode('space_move_val', state.deviceStatusList)} M', + unit: '', + ), + ], + ), ), ), const SizedBox(height: 20), + Expanded( + flex: 2, + child: FittedBox( + child: Image.asset(Assets.autocadOccupancyImage), + ), + ), ], ), ); diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart index 05415421..0809a990 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc(); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } List _generatePaintItems(DateTime startDate) { return List.generate(_totalWeeks * 7, (index) { - final date = startDate.add(Duration(days: index)); + final date = startDate.toUtc().add(Duration(days: index)); final value = heatMapData[date] ?? 0; return OccupancyPaintItem(index: index, value: value, date: date); }); diff --git a/lib/pages/analytics/widgets/charts_x_axis_title.dart b/lib/pages/analytics/widgets/charts_x_axis_title.dart new file mode 100644 index 00000000..746a8cbb --- /dev/null +++ b/lib/pages/analytics/widgets/charts_x_axis_title.dart @@ -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, + ), + ); + } +} diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 58950089..bfe0b3eb 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -36,7 +36,8 @@ class AuthBloc extends Bloc { ////////////////////////////// forget password ////////////////////////////////// final TextEditingController forgetEmailController = TextEditingController(); - final TextEditingController forgetPasswordController = TextEditingController(); + final TextEditingController forgetPasswordController = + TextEditingController(); final TextEditingController forgetOtp = TextEditingController(); final forgetFormKey = GlobalKey(); final forgetEmailKey = GlobalKey(); @@ -53,7 +54,8 @@ class AuthBloc extends Bloc { 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 { _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 { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } -Future changePassword( + Future changePassword( ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { @@ -122,7 +125,6 @@ Future changePassword( } } - String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -131,7 +133,9 @@ Future changePassword( } void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) { - emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime)); + emit(TimerState( + isButtonEnabled: event.isButtonEnabled, + remainingTime: event.remainingTime)); } ///////////////////////////////////// login ///////////////////////////////////// @@ -151,7 +155,6 @@ Future changePassword( static UserModel? user; bool showValidationMessage = false; - void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -170,11 +173,11 @@ Future 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 changePassword( } } - checkBoxToggle( CheckBoxEvent event, Emitter emit, @@ -339,12 +341,14 @@ Future changePassword( static Future 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 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(':'); diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index f23daa45..fb8237b7 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -50,20 +50,11 @@ class _DynamicTableState extends State { 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 { context.read().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 { ); } - 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 { ); } - 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 { 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 { 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, ), ), ), diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 9a8e18a2..ed0b07dd 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -69,12 +69,13 @@ class AcBloc extends Bloc { } } - void _listenToChanges(deviceId) { + StreamSubscription? _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 usersMap = @@ -109,15 +110,14 @@ class AcBloc extends Bloc { Emitter emit, ) async { emit(AcsLoadingState()); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); try { final success = await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); if (!success) { emit(const AcsFailedState(error: 'Failed to control device')); } @@ -298,13 +298,17 @@ class AcBloc extends Bloc { 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()); + } } }); } @@ -331,7 +335,9 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - add(UpdateTimerEvent()); + if (!isClosed) { + add(UpdateTimerEvent()); + } } } @@ -375,6 +381,8 @@ class AcBloc extends Bloc { @override Future close() { add(OnClose()); + _countdownTimer?.cancel(); + _deviceStatusSubscription?.cancel(); return super.close(); } } diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart index 05e82f1f..98b0c195 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart @@ -40,17 +40,18 @@ class DeviceManagementBloc List devices = []; _devices.clear(); var spaceBloc = event.context.read(); - 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 spacesList = spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; for (var space in spacesList) { - devices.addAll(await DevicesManagementApi().fetchDevices( - community, space, projectUuid)); + devices.addAll(await DevicesManagementApi() + .fetchDevices(community, space, projectUuid)); } } } @@ -100,7 +101,7 @@ class DeviceManagementBloc )); if (currentProductName.isNotEmpty) { - add(SearchDevices(productName: currentProductName)); + add(SearchDevices(deviceNameOrProductName: currentProductName)); } } } @@ -269,34 +270,41 @@ class DeviceManagementBloc return 'All'; } } - void _onSearchDevices( SearchDevices event, Emitter 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 devicesToSearch = _filteredDevices; + List devicesToSearch = _devices; if (devicesToSearch.isNotEmpty) { + final searchText = event.deviceNameOrProductName?.toLowerCase() ?? ''; + final filteredDevices = devicesToSearch.where((device) { final matchesCommunity = event.community == null || event.community!.isEmpty || @@ -304,31 +312,25 @@ class DeviceManagementBloc ?.toLowerCase() .contains(event.community!.toLowerCase()) ?? false); + final matchesUnit = event.unitName == null || event.unitName!.isEmpty || (device.spaces != null && - device.spaces!.isNotEmpty && - device.spaces![0].spaceName! - .toLowerCase() - .contains(event.unitName!.toLowerCase())); - final matchesProductName = event.productName == null || - event.productName!.isEmpty || - (device.name - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); - final matchesDeviceName = event.productName == null || - event.productName!.isEmpty || - (device.categoryName - ?.toLowerCase() - .contains(event.productName!.toLowerCase()) ?? - false); + device.spaces!.any((space) => + space.spaceName != null && + space.spaceName! + .toLowerCase() + .contains(event.unitName!.toLowerCase()))); - return matchesCommunity && - matchesUnit && - (matchesProductName || matchesDeviceName); + final matchesSearchText = searchText.isEmpty || + (device.name?.toLowerCase().contains(searchText) ?? false) || + (device.productName?.toLowerCase().contains(searchText) ?? false); + + return matchesCommunity && matchesUnit && matchesSearchText; }).toList(); + _filteredDevices = filteredDevices; + emit(DeviceManagementFiltered( filteredDevices: filteredDevices, selectedIndex: _selectedIndex, diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart index 9928c50e..5292de0e 100644 --- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart +++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart @@ -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 get props => [community, unitName, productName]; + List get props => [community, unitName, deviceNameOrProductName]; } class SelectDevice extends DeviceManagementEvent { diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index 808a683f..e491214d 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_tag import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart'; import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/flush/flush_functions.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/one_gang_switch.dart'; @@ -359,6 +360,14 @@ SOS uuid: uuid ?? '', name: name ?? '', ); + case 'CUR': + return [ + ControlCurtainFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'BOTH', + ) + ]; case 'NCPS': return [ FlushPresenceDelayFunction( @@ -441,15 +450,10 @@ SOS VoltageCStatusFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), CurrentCStatusFunction( - deviceId: uuid ?? '', - deviceName: name ?? '', - type: 'IF'), + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), PowerFactorCStatusFunction( - deviceId: uuid ?? '', - deviceName: name ?? '', - type: 'IF'), + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), ]; - default: return []; } diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index f4baad0c..c865a5dc 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -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, diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index 6440d18f..7e998ed6 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State 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 onSearch: () => context.read().add( SearchDevices( unitName: _unitNameController.text, - productName: _productNameController.text, + deviceNameOrProductName: _productNameController.text, searchField: true, ), ), diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart index f83ced1a..f6cebe4d 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart @@ -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 { DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) { on(_onFetchDeviceStatus); - //on(_onDoorLockControl); on(_updateLock); on(_onFactoryReset); on(_onStatusUpdated); } - _listenToChanges(deviceId) { + void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream 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? ?? []; + final statusList = statusData.map((item) { + return Status(code: item['code'], value: item['value']); + }).toList(); - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List 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 emit) { - emit(DoorLockStatusLoading()); - deviceStatus = event.deviceStatus; emit(DoorLockStatusLoaded(deviceStatus)); } - FutureOr _onFetchDeviceStatus( + Future _onFetchDeviceStatus( DoorLockFetchStatus event, Emitter emit) async { emit(DoorLockStatusLoading()); try { @@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc { deviceStatus = DoorLockStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); - emit(DoorLockStatusLoaded(deviceStatus)); } catch (e) { emit(DoorLockControlError(e.toString())); } } - FutureOr _updateLock( + Future _updateLock( UpdateLockEvent event, Emitter emit) async { final oldValue = deviceStatus.normalOpenSwitch; deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue); @@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc { try { final response = await DevicesManagementApi.openDoorLock(deviceId); - if (!response) { _revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit); } @@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc { } } - Future _runDebounce({ - required String deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter 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 emit, - ) { + void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, + Emitter emit) { _updateLocalValue(code, oldValue); emit(DoorLockStatusLoaded(deviceStatus)); emit(const DoorLockControlError('Failed to control the device.')); @@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc { 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 _onFactoryReset( + Future _onFactoryReset( DoorLockFactoryReset event, Emitter emit) async { emit(DoorLockStatusLoading()); try { diff --git a/lib/pages/device_managment/door_lock/widget/door_button.dart b/lib/pages/device_managment/door_lock/widget/door_button.dart index e8e3066e..c1ac7bc0 100644 --- a/lib/pages/device_managment/door_lock/widget/door_button.dart +++ b/lib/pages/device_managment/door_lock/widget/door_button.dart @@ -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 createState() => - _DoorLockButtonState(smartDoorModel: smartDoorModel); -} - -class _DoorLockButtonState extends State - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _animation; - DoorLockStatusModel smartDoorModel; - - _DoorLockButtonState({required this.smartDoorModel}); - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 500), - ); - _animation = Tween(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(context) - .add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch)); - }, + onTap: isEnabled + ? () { + BlocProvider.of(context).add( + UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch), + ); + } + : null, child: Container( width: 255, height: 255, @@ -115,15 +73,16 @@ class _DoorLockButtonState extends State ), ), ), - SizedBox.expand( - child: CircularProgressIndicator( - value: _animation.value, - strokeWidth: 8, - backgroundColor: Colors.transparent, - valueColor: const AlwaysStoppedAnimation( - ColorsManager.primaryColor), + if (progress > 0) + SizedBox.expand( + child: CircularProgressIndicator( + value: progress, + strokeWidth: 8, + backgroundColor: Colors.transparent, + valueColor: const AlwaysStoppedAnimation( + ColorsManager.primaryColor), + ), ), - ), ], ), ), diff --git a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart index 7b133d45..8c9820f9 100644 --- a/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart +++ b/lib/pages/device_managment/garage_door/helper/garage_door_helper.dart @@ -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'; diff --git a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart similarity index 88% rename from lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart index 843bac9b..0cc9485a 100644 --- a/lib/pages/device_managment/garage_door/widgets/opening_clsoing_time_dialog_body.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/opening_clsoing_time_dialog_body.dart @@ -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 onDurationChanged; final GarageDoorBloc bloc; - OpeningAndClosingTimeDialogBody({ + const OpeningAndClosingTimeDialogBody({ required this.onDurationChanged, required this.bloc, }); diff --git a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart similarity index 80% rename from lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart index 07cd9c7a..525e79c9 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule__garage_table.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule__garage_table.dart @@ -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( 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().add(DeleteGarageDoorScheduleEvent( + context + .read() + .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), ), ), ], diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_header.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_header.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart similarity index 93% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart index e5819e89..9b2c2b3f 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_managment_ui.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_managment_ui.dart @@ -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'; diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_buttons.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_buttons.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_mode_selector.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_mode_selector.dart diff --git a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart similarity index 91% rename from lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart rename to lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart index 107c8e0a..7b772135 100644 --- a/lib/pages/device_managment/garage_door/widgets/schedule_garage_view.dart +++ b/lib/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart @@ -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}); diff --git a/lib/pages/device_managment/garage_door/widgets/seconds_picker.dart b/lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/seconds_picker.dart rename to lib/pages/device_managment/garage_door/schedule_view/seconds_picker.dart diff --git a/lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart b/lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart similarity index 100% rename from lib/pages/device_managment/garage_door/widgets/time_out_alarm_dialog_body.dart rename to lib/pages/device_managment/garage_door/schedule_view/time_out_alarm_dialog_body.dart diff --git a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart index ae2fc9e4..30d9bf5d 100644 --- a/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart +++ b/lib/pages/device_managment/garage_door/view/garage_door_control_view.dart @@ -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'; diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 997be513..1ad5d43b 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -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( 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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceId, + ), + )); + }, + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ], ); diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index f1861c55..2f6008d2 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceId, + ), + )); + }, + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart index 67313802..11d1cc8f 100644 --- a/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart +++ b/lib/pages/device_managment/power_clamp/view/smart_power_device_control.dart @@ -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 diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart new file mode 100644 index 00000000..fbf7ae64 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -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 { + final String deviceId; + + ScheduleBloc({ + required this.deviceId, + }) : super(ScheduleInitial()) { + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + on(_getSchedule); + on(_onAddSchedule); + on(_onEditSchedule); + on(_onUpdateSchedule); + on(_onUpdateScheduleMode); + on(_onUpdateCountdownTime); + on(_onUpdateInchingTime); + on(_onStartScheduleEvent); + on(_onStopScheduleEvent); + on(_onDecrementCountdown); + on(_fetchStatus); + on(_onDeleteSchedule); + } + Timer? _countdownTimer; + Duration countdownRemaining = Duration.zero; + + Future _onStopScheduleEvent( + StopScheduleEvent event, + Emitter 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 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 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 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 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 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 emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + final updatedDays = List.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 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 _getSchedule( + ScheduleGetEvent event, + Emitter 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 _onAddSchedule( + ScheduleAddEvent event, + Emitter 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.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 _onEditSchedule( + ScheduleEditEvent event, + Emitter 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 _onUpdateSchedule( + ScheduleUpdateEntryEvent event, + Emitter 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 _onDeleteSchedule( + ScheduleDeleteEvent event, + Emitter 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 _onStartScheduleEvent( + StartScheduleEvent event, + Emitter 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 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 emit, + ) { + if (state is ScheduleLoaded) { + final currentState = state as ScheduleLoaded; + emit(currentState.copyWith( + countdownRemaining: countdownRemaining, + )); + } + } + + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + Future _fetchStatus( + ScheduleFetchStatusEvent event, + Emitter 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; + } +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart new file mode 100644 index 00000000..0b9ec581 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -0,0 +1,234 @@ +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? selectedDays; + final bool? functionOn; + + const ScheduleInitializeAddEvent({ + required this.isEditing, + required this.scheduleMode, + this.selectedTime, + this.selectedDays, + this.functionOn, + }); + + @override + List get props => [ + isEditing, + scheduleMode, + selectedTime, + selectedDays, + functionOn, + ]; +} + +class ScheduleUpdateSelectedTimeEvent extends ScheduleEvent { + final TimeOfDay selectedTime; + + const ScheduleUpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class ScheduleUpdateSelectedDayEvent extends ScheduleEvent { + final int index; + final bool value; + + const ScheduleUpdateSelectedDayEvent(this.index, this.value); + + @override + List get props => [index, value]; +} + +class ScheduleUpdateFunctionOnEvent extends ScheduleEvent { + final bool isOn; + + const ScheduleUpdateFunctionOnEvent(this.isOn); + + @override + List get props => [isOn]; +} + +class ScheduleGetEvent extends ScheduleEvent { + final String category; + + const ScheduleGetEvent({required this.category}); + + @override + List get props => [category]; +} + +class ScheduleAddEvent extends ScheduleEvent { + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleAddEvent({ + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [category, time, selectedDays, functionOn]; +} + +class ScheduleEditEvent extends ScheduleEvent { + final String scheduleId; + final String category; + final String time; + final List selectedDays; + final bool functionOn; + + const ScheduleEditEvent({ + required this.scheduleId, + required this.category, + required this.time, + required this.selectedDays, + required this.functionOn, + }); + + @override + List get props => [ + scheduleId, + category, + time, + selectedDays, + functionOn, + ]; +} + +class ScheduleDeleteEvent extends ScheduleEvent { + final String scheduleId; + + const ScheduleDeleteEvent(this.scheduleId); + + @override + List 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 get props => [scheduleId, functionOn, enable, category]; +} + +class UpdateScheduleModeEvent extends ScheduleEvent { + final ScheduleModes scheduleMode; + + const UpdateScheduleModeEvent({required this.scheduleMode}); + + @override + List 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 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 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 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 get props => [mode, deviceId]; +} + +class ScheduleDecrementCountdownEvent extends ScheduleEvent { + const ScheduleDecrementCountdownEvent(); + + @override + List get props => []; +} + +class ScheduleFetchStatusEvent extends ScheduleEvent { + final String deviceId; + + const ScheduleFetchStatusEvent(this.deviceId); + + @override + List get props => [deviceId]; +} + +class DeleteScheduleEvent extends ScheduleEvent { + final String scheduleId; + + const DeleteScheduleEvent(this.scheduleId); + + @override + List get props => [scheduleId]; +} + +class StatusUpdatedScheduleEvent extends ScheduleEvent { + final String id; + + const StatusUpdatedScheduleEvent(this.id); + + @override + List get props => [id]; +} diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart new file mode 100644 index 00000000..63551c3a --- /dev/null +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -0,0 +1,120 @@ +part of 'schedule_bloc.dart'; + +abstract class ScheduleState extends Equatable { + const ScheduleState(); +} + +class ScheduleInitial extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoading extends ScheduleState { + @override + List get props => []; +} + +class ScheduleLoaded extends ScheduleState { + final List schedules; + final TimeOfDay? selectedTime; + final List 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? schedules, + TimeOfDay? selectedTime, + List? 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 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 get props => [error]; +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart similarity index 72% rename from lib/pages/device_managment/water_heater/widgets/count_down_button.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart index e60c7def..4919018c 100644 --- a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart @@ -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() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( + context.read().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().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'countdown_1', - value: Duration(hours: hours, minutes: minutes) - .inSeconds, + context.read().add( + StartScheduleEvent( + mode: ScheduleModes.countdown, + hours: hours, + minutes: minutes, ), ); }, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart new file mode 100644 index 00000000..418bab6c --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart @@ -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 createState() => _CountdownInchingViewState(); +} + +class _CountdownInchingViewState extends State { + 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( + 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().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().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().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() + .add(UpdateCountdownTimeEvent( + hours: displayHours, + minutes: displayMinutes, + seconds: value, + )); + } + }, + isActive: isActive, + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + FixedExtentScrollController controller, + ValueChanged 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, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart similarity index 82% rename from lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart index 8eec5cca..e75c5d46 100644 --- a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart @@ -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() - .add(StopScheduleEvent(deviceId)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: deviceId, - code: 'switch_inching', - value: 0, - ), + context.read().add( + StopScheduleEvent( + deviceId: deviceId, mode: ScheduleModes.inching), ); }, backgroundColor: Colors.red, diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart new file mode 100644 index 00000000..47534d37 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -0,0 +1,112 @@ +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/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/water_heater_status_model.dart'; + +class BuildScheduleView extends StatelessWidget { + const BuildScheduleView( + {super.key, required this.deviceUuid, required this.category}); + final String deviceUuid; + final String category; + + @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( + 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: null, + isEdit: false, + ); + if (entry != null) { + context.read().add( + ScheduleAddEvent( + category: entry.category, + 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()); + }, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart new file mode 100644 index 00000000..86fc5ba5 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart @@ -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, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_header.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart similarity index 76% rename from lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart index 1710c439..8f871ce4 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart @@ -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), ], ); } diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart similarity index 100% rename from lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart rename to lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..25bf7f2c --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart @@ -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( + (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( + value: mode, + groupValue: currentMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + context.read().add( + UpdateScheduleModeEvent(scheduleMode: value), + ); + if (value == ScheduleModes.schedule) { + context.read().add( + const ScheduleGetEvent(category: 'switch_1'), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart new file mode 100644 index 00000000..98ae0515 --- /dev/null +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -0,0 +1,283 @@ +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( + 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 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: () { + context.read().add( + ScheduleUpdateEntryEvent( + category: schedule.category, + scheduleId: schedule.scheduleId, + functionOn: 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))), + 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().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( + 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().add( + ScheduleDeleteEvent(schedule.scheduleId), + ); + } + }, + child: Text( + 'Delete', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ) + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + const days = ScheduleDialogHelper.allDays; + return selectedDays + .asMap() + .entries + .where((entry) => entry.value) + .map((entry) => days[entry.key]) + .join(', '); + } +} diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index beb3b52c..7a046bea 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -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, ), ), ), diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 21a81df0..72435b74 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -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( 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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceId, + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceId, + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(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, - ), ], ); } diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index 731b354c..66784bd5 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_3', + ), + )); + }, + mainText: 'Spotlight', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), ], ); } diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index 575deeac..34b30dd3 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -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( 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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_1', + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, ), ], ); diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index e8346cb2..849412f2 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -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 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( 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( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_1', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Wall Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + + ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: BuildScheduleView( + category: 'switch_2', + deviceUuid: deviceIds.first, + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), // FirmwareUpdateWidget( // deviceId: deviceIds.first, // version: 12, diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 882aac3e..ac3fe579 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -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().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().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().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().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( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(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( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'switch_2', + ), + )); + }, + mainText: 'Ceiling Light', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ), + ], + ) ], ), ); diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 9278e396..ae7feac9 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -1,240 +1,210 @@ 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(); + static const List 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 showAddScheduleDialog( + BuildContext context, { + ScheduleEntry? schedule, + bool isEdit = false, + }) { + 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 selectedDays = List.of(initialDays); - bloc.add(InitializeAddScheduleEvent( - selectedTime: time, - selectedDays: selectedDays, - functionOn: schedule.function.value, - isEditing: true, - index: index, - )); - } - - showDialog( + return showDialog( context: context, builder: (ctx) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - 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(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: () { + final entry = ScheduleEntry( + category: schedule?.category ?? 'switch_1', + time: _formatTimeOfDayToISO(selectedTime), + function: Status(code: 'switch_1', value: 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 _convertDaysStringToBooleans(List selectedDays) { final daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List 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 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 _convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) result.add(allDays[i]); + } + return result; + } + + static Widget _buildDayCheckboxes(BuildContext ctx, List 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().add(UpdateSelectedDayEvent(index, value!)); - }, + onChanged: (val) => onChanged(index, val!), ), Text(dayLabels[index]), ], - ); - }), + ), + ), ); } - static Widget _buildFunctionSwitch(BuildContext context, bool isOn, bool? isEdit) { + static Widget _buildFunctionSwitch( + 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( value: true, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(true)); - }, + onChanged: (val) => onChanged(true), ), const Text('On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, - onChanged: (bool? value) { - context.read().add(const UpdateFunctionOnEvent(false)); - }, + onChanged: (val) => onChanged(false), ), const Text('Off'), ], diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart index a2a109af..d6a530bb 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_entry.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -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, + ); } } diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index c535bda2..bf9ab2cd 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -16,7 +16,7 @@ class WaterHeaterStatusModel extends Equatable { final String cycleTiming; final List schedules; - const WaterHeaterStatusModel({ + const WaterHeaterStatusModel({ required this.uuid, required this.heaterSwitch, required this.countdownHours, diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index f1e56136..16eff86a 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -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( context: context, builder: (ctx) => BlocProvider.value( value: BlocProvider.of(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, + ), ], ); } diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart deleted file mode 100644 index 9c28d4d6..00000000 --- a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.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/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 StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const CountdownInchingView({ - super.key, - required this.state, - }); - - @override - Widget build(BuildContext context) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - - 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), - _hourMinutesWheel(context, state), - ], - ); - } - - Row _hourMinutesWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Row _hourMinutesSecondWheel( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - late bool isActive; - if (isCountDown && - state.countdownRemaining != null && - state.isCountdownActive == true) { - isActive = true; - } else if (!isCountDown && - state.countdownRemaining != null && - state.isInchingActive == true) { - isActive = true; - } else { - isActive = false; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn( - context, - 'h', - isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'm', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn( - context, - 'S', - isCountDown - ? (state.countdownMinutes ?? 0) - : (state.inchingMinutes ?? 0), - 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: isCountDown - ? (state.countdownHours ?? 0) - : (state.inchingHours ?? 0), - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Widget _buildPickerColumn( - BuildContext context, - String label, - int initialValue, - int itemCount, - ValueChanged 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( - key: ValueKey('$label-$initialValue'), - controller: FixedExtentScrollController( - initialItem: initialValue, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: 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, - ), - ), - ], - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart deleted file mode 100644 index 9d4a2497..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_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/water_heater_status_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; - -class BuildScheduleView extends StatefulWidget { - const BuildScheduleView({super.key, required this.status}); - - final WaterHeaterStatusModel status; - - @override - State createState() => _BuildScheduleViewState(); -} - -class _BuildScheduleViewState extends State { - @override - Widget build(BuildContext context) { - final bloc = BlocProvider.of(context); - - return BlocProvider.value( - value: bloc, - 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( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ScheduleHeader(), - const SizedBox(height: 20), - ScheduleModeSelector(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.schedule) - ScheduleManagementUI( - state: state, - onAddSchedule: () { - ScheduleDialogHelper.showAddScheduleDialog( - context, - schedule: null, - index: null, - isEdit: false); - }, - ), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) - CountdownInchingView(state: state), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.countdown) - CountdownModeButtons( - isActive: state.isCountdownActive ?? false, - deviceId: widget.status.uuid, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - ), - if (state.scheduleMode == ScheduleModes.inching) - InchingModeButtons( - isActive: state.isInchingActive ?? false, - deviceId: widget.status.uuid, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - ), - if (state.scheduleMode != ScheduleModes.countdown && - state.scheduleMode != ScheduleModes.inching) - ScheduleModeButtons( - onSave: () { - Navigator.pop(context); - }, - ), - ], - ); - } - if (state is WaterHeaterLoadingState) { - return const SizedBox( - height: 200, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ScheduleHeader(), - SizedBox( - height: 20, - ), - Center(child: CircularProgressIndicator()), - ], - )); - } - return const SizedBox( - height: 200, - child: ScheduleHeader(), - ); - }, - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart deleted file mode 100644 index bb9ddc8f..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.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/water_heater_status_model.dart'; - -class ScheduleModeSelector extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const ScheduleModeSelector({super.key, required this.state}); - - @override - Widget build(BuildContext context) { - 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, state), - _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, state), - _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), - ], - ), - ], - ); - } - - Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, - WaterHeaterDeviceStatusLoaded state) { - return Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: mode, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - if (value == ScheduleModes.countdown) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.countdownHours ?? 0, - minutes: state.countdownMinutes ?? 0, - )); - } else if (value == ScheduleModes.inching) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.inchingHours ?? 0, - minutes: state.inchingMinutes ?? 0, - )); - } - - if (value == ScheduleModes.schedule) { - context.read().add( - GetSchedulesEvent( - category: 'switch_1', - uuid: state.status.uuid, - ), - ); - } - } - }, - ), - ), - ); - } -} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart deleted file mode 100644 index 18cbbe5a..00000000 --- a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart +++ /dev/null @@ -1,222 +0,0 @@ -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/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/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; -import 'package:syncrow_web/utils/format_date_time.dart'; - -import '../helper/add_schedule_dialog_helper.dart'; - -class ScheduleTableWidget extends StatelessWidget { - final WaterHeaterDeviceStatusLoaded state; - - const ScheduleTableWidget({ - super.key, - required this.state, - }); - - @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( - builder: (context, state) { - if (state is ScheduleLoadingState) { - return const SizedBox( - height: 200, - child: Center(child: CircularProgressIndicator())); - } - if (state is WaterHeaterDeviceStatusLoaded && - state.schedules.isEmpty) { - return _buildEmptyState(context); - } else if (state is WaterHeaterDeviceStatusLoaded) { - 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, context)); - } - 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: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildTableBody( - WaterHeaterDeviceStatusLoaded state, 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 < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context, state), - ], - ), - ), - ); - } - - 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, WaterHeaterDeviceStatusLoaded state) { - return TableRow( - children: [ - Center( - child: GestureDetector( - onTap: () { - context.read().add(UpdateScheduleEntryEvent( - index: index, - enable: !schedule.enable, - scheduleId: schedule.scheduleId, - deviceId: state.status.uuid, - functionOn: schedule.function.value, - )); - }, - 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))), - 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: schedule, index: index, isEdit: true); - }, - child: Text( - 'Edit', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - context.read().add(DeleteScheduleEvent( - index: index, - scheduleId: schedule.scheduleId, - )); - }, - child: Text( - 'Delete', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - ], - ), - ), - ], - ); - } - - String _getSelectedDays(List selectedDays) { - final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr.join(', '); - } -} diff --git a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart index 071de067..00c566c6 100644 --- a/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart +++ b/lib/pages/roles_and_permission/users_page/add_user_dialog/view/edit_user_dialog.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/auth/model/user_model.dart'; import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/roles_and_permission/model/roles_user_model.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart'; import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart'; @@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; class EditUserDialog extends StatefulWidget { - final String? userId; - const EditUserDialog({super.key, this.userId}); + final RolesUserModel? user; + const EditUserDialog({ + super.key, + this.user, + }); @override _EditUserDialogState createState() => _EditUserDialogState(); @@ -28,10 +33,11 @@ class _EditUserDialogState extends State { create: (BuildContext context) => UsersBloc() // ..add(const LoadCommunityAndSpacesEvent()) ..add(const RoleEvent()) - ..add(GetUserByIdEvent(uuid: widget.userId)), + ..add(GetUserByIdEvent(uuid: widget.user!.uuid)), child: BlocConsumer(listener: (context, state) { if (state is SpacesLoadedState) { - BlocProvider.of(context).add(GetUserByIdEvent(uuid: widget.userId)); + BlocProvider.of(context) + .add(GetUserByIdEvent(uuid: widget.user!.uuid)); } }, builder: (context, state) { final _blocRole = BlocProvider.of(context); @@ -39,7 +45,8 @@ class _EditUserDialogState extends State { return Dialog( child: Container( decoration: const BoxDecoration( - color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))), + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(20))), width: 900, child: Column( children: [ @@ -68,7 +75,8 @@ class _EditUserDialogState extends State { children: [ _buildStep1Indicator(1, "Basics", _blocRole), _buildStep2Indicator(2, "Spaces", _blocRole), - _buildStep3Indicator(3, "Role & Permissions", _blocRole), + _buildStep3Indicator( + 3, "Role & Permissions", _blocRole), ], ), ), @@ -86,7 +94,7 @@ class _EditUserDialogState extends State { children: [ const SizedBox(height: 10), Expanded( - child: _getFormContent(widget.userId), + child: _getFormContent(widget.user!), ), const SizedBox(height: 20), ], @@ -116,13 +124,14 @@ class _EditUserDialogState extends State { if (currentStep < 3) { currentStep++; if (currentStep == 2) { - _blocRole.add(CheckStepStatus(isEditUser: true)); + _blocRole + .add(CheckStepStatus(isEditUser: true)); } else if (currentStep == 3) { _blocRole.add(const CheckSpacesStepStatus()); } } else { - _blocRole - .add(EditInviteUsers(context: context, userId: widget.userId!)); + _blocRole.add(EditInviteUsers( + context: context, userId: widget.user!.uuid)); } }); }, @@ -131,7 +140,8 @@ class _EditUserDialogState extends State { style: TextStyle( color: (_blocRole.isCompleteSpaces == false || _blocRole.isCompleteBasics == false || - _blocRole.isCompleteRolePermissions == false) && + _blocRole.isCompleteRolePermissions == + false) && currentStep == 3 ? ColorsManager.grayColor : ColorsManager.secondaryColor), @@ -146,15 +156,15 @@ class _EditUserDialogState extends State { })); } - Widget _getFormContent(userid) { + Widget _getFormContent(RolesUserModel user) { switch (currentStep) { case 1: return BasicsView( - userId: userid, + userId: user.uuid, ); case 2: return SpacesAccessView( - userId: userid, + userId: user.uuid, ); case 3: return const RolesAndPermission(); @@ -166,6 +176,7 @@ class _EditUserDialogState extends State { int step3 = 0; Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -189,7 +200,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteBasics == false ? Assets.wrongProcessIcon @@ -204,8 +215,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: isCurrentStep + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -229,6 +243,7 @@ class _EditUserDialogState extends State { } Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -248,7 +263,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteSpaces == false ? Assets.wrongProcessIcon @@ -263,8 +278,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: isCurrentStep + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], @@ -288,6 +306,7 @@ class _EditUserDialogState extends State { } Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) { + final isCurrentStep = currentStep == step; return GestureDetector( onTap: () { setState(() { @@ -306,7 +325,7 @@ class _EditUserDialogState extends State { child: Row( children: [ SvgPicture.asset( - currentStep == step + isCurrentStep ? Assets.currentProcessIcon : bloc.isCompleteRolePermissions == false ? Assets.wrongProcessIcon @@ -321,8 +340,11 @@ class _EditUserDialogState extends State { label, style: TextStyle( fontSize: 16, - color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor, - fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal, + color: isCurrentStep + ? ColorsManager.blackColor + : ColorsManager.greyColor, + fontWeight: + isCurrentStep ? FontWeight.bold : FontWeight.normal, ), ), ], diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 767fd9a6..da159d94 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -19,6 +19,7 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; + class UsersPage extends StatelessWidget { UsersPage({super.key}); @@ -451,33 +452,31 @@ class UsersPage extends StatelessWidget { ), Row( children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context - .read() - .add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog( - userId: user.uuid); - }, - ).then((v) { - if (v != null) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - } - }); + if (user.isEnabled != false) + actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog(user: user); }, - ) - : actionButton( - title: "Edit", - ), + ).then((v) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + }); + }, + ) + else + actionButton( + title: "Edit", + ), actionButton( title: "Delete", onTap: () { diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart index 013626d8..5e313d86 100644 --- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/curtain_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart'; @@ -26,7 +27,7 @@ class DeviceDialogHelper { final result = await _getDialogForDeviceType( dialogType: dialogType, context: context, - productType: data['productType'], + productType: data['productType'] as String, data: data, functions: functions, removeComparetors: removeComparetors, @@ -65,7 +66,14 @@ class DeviceDialogHelper { removeComparetors: removeComparetors, dialogType: dialogType, ); - + case 'CUR': + return CurtainHelper.showControlDialog( + dialogType: dialogType, + context: context, + functions: functions, + uniqueCustomId: data['uniqueCustomId'], + device: data['device'], + ); case '1G': return OneGangSwitchHelper.showSwitchFunctionsDialog( dialogType: dialogType, diff --git a/lib/pages/routines/models/curtain/curtain_function.dart b/lib/pages/routines/models/curtain/curtain_function.dart new file mode 100644 index 00000000..e0689e5e --- /dev/null +++ b/lib/pages/routines/models/curtain/curtain_function.dart @@ -0,0 +1,49 @@ +import 'package:syncrow_web/pages/device_managment/curtain/model/curtain_model.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart' + show DeviceFunction; +import 'package:syncrow_web/utils/constants/app_enum.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class CurtainFunction extends DeviceFunction { + final String type; + CurtainFunction({ + required super.deviceId, + required super.deviceName, + required this.type, + required super.code, + required super.operationName, + required super.icon, + }); + List getOperationalValues(); +} + +class ControlCurtainFunction extends CurtainFunction { + ControlCurtainFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + super.code = 'control', + super.operationName = 'Control', + super.icon = Assets.curtain, + }); + + @override + List getOperationalValues() => [ + CurtainOperationalValue( + icon: Assets.curtain, + description: 'OPEN', + value: 'open', + ), + CurtainOperationalValue( + icon: Assets.curtain, + description: 'STOP', + value: 'stop', + ), + CurtainOperationalValue( + icon: Assets.curtain, + description: 'CLOSE', + value: 'close', + ) + ]; +} diff --git a/lib/pages/routines/models/curtain/curtain_opertion_value.dart b/lib/pages/routines/models/curtain/curtain_opertion_value.dart new file mode 100644 index 00000000..faa81cfd --- /dev/null +++ b/lib/pages/routines/models/curtain/curtain_opertion_value.dart @@ -0,0 +1,11 @@ +class CurtainOperationalValue { + final String icon; + final String description; + final String value; + + CurtainOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart index da77c7c2..a85e25bc 100644 --- a/lib/pages/routines/widgets/if_container.dart +++ b/lib/pages/routines/widgets/if_container.dart @@ -148,6 +148,7 @@ class IfContainer extends StatelessWidget { 'NCPS', 'WH', 'PC', + 'CUR', ].contains(mutableData['productType'])) { context .read() diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart index f0b77467..f260b262 100644 --- a/lib/pages/routines/widgets/routine_devices.dart +++ b/lib/pages/routines/widgets/routine_devices.dart @@ -28,6 +28,7 @@ class _RoutineDevicesState extends State { 'NCPS', 'WH', 'PC', + 'CUR', }; @override diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart new file mode 100644 index 00000000..bdf8660d --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -0,0 +1,270 @@ +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/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart'; +import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CurtainHelper { + static Future?> showControlDialog({ + required String dialogType, + required BuildContext context, + required List functions, + required String uniqueCustomId, + required AllDevicesModel? device, + }) async { + List curtainFunctions = + functions.whereType().where((function) { + if (dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + return showDialog?>( + context: context, + builder: (context) => BlocProvider( + create: (_) => FunctionBloc()..add(const InitializeFunctions([])), + child: AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + final selectedOperationName = state.selectedOperationName; + final selectedFunctionData = state.addedFunctions + .firstWhere((f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + )); + + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DialogHeader('AC Functions'), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Function list + SizedBox( + width: selectedFunction != null ? 320 : 360, + child: _buildFunctionsList( + context: context, + curtainFunctions: curtainFunctions, + onFunctionSelected: + (functionCode, operationName) { + RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: functionCode, + functionOperationName: operationName, + functionValueDescription: + selectedFunctionData.valueDescription, + deviceUuid: device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'temp_set', + 'temp_current', + ], + defaultValue: 0); + }), + ), + // Value selector + if (selectedFunction != null) + Expanded( + child: _buildValueSelector( + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: selectedFunctionData, + controlFunctions: curtainFunctions, + device: device, + operationName: selectedOperationName ?? '', + ), + ), + ], + ), + ), + DialogFooter( + onCancel: () { + Navigator.pop(context); + }, + onConfirm: state.addedFunctions.isNotEmpty + ? () { + /// add the functions to the routine bloc + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + uniqueCustomId, + ), + ); + + // Return the device data to be added to the container + Navigator.pop(context, { + 'deviceId': functions.first.deviceId, + }); + } + : null, + isConfirmEnabled: selectedFunction != null, + ), + ], + ), + ); + }, + ), + ), + ), + ).then((value) { + return value; + }); + } + + static Widget _buildFunctionsList({ + required BuildContext context, + required List curtainFunctions, + required Function(String, String) onFunctionSelected, + }) { + return ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: curtainFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider( + color: ColorsManager.dividerColor, + ), + ), + itemBuilder: (context, index) { + final function = curtainFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => onFunctionSelected( + function.code, + function.operationName, + ), + ); + }, + ); + } + + static Widget _buildValueSelector({ + required BuildContext context, + required String selectedFunction, + required DeviceFunctionData? selectedFunctionData, + required List controlFunctions, + AllDevicesModel? device, + required String operationName, + }) { + final selectedFn = + controlFunctions.firstWhere((f) => f.code == selectedFunction); + + // Rest of your existing code for other value selectors + final values = selectedFn.getOperationalValues(); + return _buildOperationalValuesList( + context: context, + values: values, + selectedValue: selectedFunctionData?.value, + device: device, + operationName: operationName, + selectCode: selectedFunction, + selectedFunctionData: selectedFunctionData, + ); + } + + static Widget _buildOperationalValuesList({ + required BuildContext context, + required List values, + required dynamic selectedValue, + AllDevicesModel? device, + required String operationName, + required String selectCode, + DeviceFunctionData? selectedFunctionData, + + // required Function(dynamic) onValueChanged, + }) { + return ListView.builder( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: values.length, + itemBuilder: (context, index) { + final value = values[index]; + final isSelected = selectedValue == value.value; + return ListTile( + leading: SvgPicture.asset( + value.icon, + width: 24, + height: 24, + placeholderBuilder: (BuildContext context) => Container( + width: 24, + height: 24, + color: Colors.transparent, + ), + ), + title: Text( + value.description, + style: context.textTheme.bodyMedium, + ), + trailing: Icon( + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, + size: 24, + color: isSelected + ? ColorsManager.primaryColorWithOpacity + : ColorsManager.textGray, + ), + onTap: () { + if (!isSelected) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value.value, + condition: selectedFunctionData?.condition, + valueDescription: + selectedFunctionData?.valueDescription, + ), + ), + ); + } + }, + ); + }, + ); + } +} diff --git a/lib/pages/routines/widgets/then_container.dart b/lib/pages/routines/widgets/then_container.dart index d9eee4c4..d1f66733 100644 --- a/lib/pages/routines/widgets/then_container.dart +++ b/lib/pages/routines/widgets/then_container.dart @@ -30,123 +30,121 @@ class ThenContainer extends StatelessWidget { style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 16), - state.isLoading && state.isUpdate == true - ? const Center( - child: CircularProgressIndicator(), - ) - : Wrap( - spacing: 8, - runSpacing: 8, - children: List.generate( - state.thenItems.length, - (index) => GestureDetector( - onTap: () async { - if (state.thenItems[index] - ['deviceId'] == - 'delay') { - final result = await DelayHelper - .showDelayPickerDialog(context, - state.thenItems[index]); - - if (result != null) { - context - .read() - .add(AddToThenContainer({ - ...state.thenItems[index], - 'imagePath': Assets.delay, - 'title': 'Delay', - })); - } - return; - } - - if (state.thenItems[index]['type'] == - 'automation') { - final result = await showDialog( - context: context, - builder: (BuildContext context) => - AutomationDialog( - automationName: - state.thenItems[index] - ['name'] ?? - 'Automation', - automationId: - state.thenItems[index] - ['deviceId'] ?? - '', - uniqueCustomId: - state.thenItems[index] - ['uniqueCustomId'], - ), - ); - - if (result != null) { - context - .read() - .add(AddToThenContainer({ - ...state.thenItems[index], - 'imagePath': - Assets.automation, - 'title': - state.thenItems[index] - ['name'] ?? - state.thenItems[index] - ['title'], - })); - } - return; - } - - final result = await DeviceDialogHelper - .showDeviceDialog( - context: context, - data: state.thenItems[index], - removeComparetors: true, - dialogType: "THEN"); + if (state.isLoading && state.isUpdate == true) + const Center( + child: CircularProgressIndicator(), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + state.thenItems.length, + (index) => GestureDetector( + onTap: () async { + if (state.thenItems[index]['deviceId'] == + 'delay') { + final result = await DelayHelper + .showDelayPickerDialog(context, + state.thenItems[index]); if (result != null) { - context.read().add( - AddToThenContainer( - state.thenItems[index])); - } else if (![ - 'AC', - '1G', - '2G', - '3G', - 'WPS', - 'CPS', - "GW", - "NCPS", - 'WH', - ].contains(state.thenItems[index] - ['productType'])) { - context.read().add( - AddToThenContainer( - state.thenItems[index])); + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.delay, + 'title': 'Delay', + })); } + return; + } + + if (state.thenItems[index]['type'] == + 'automation') { + final result = await showDialog( + context: context, + builder: (BuildContext context) => + AutomationDialog( + automationName: + state.thenItems[index]['name'] + as String? ?? + 'Automation', + automationId: state.thenItems[index] + ['deviceId'] as String? ?? + '', + uniqueCustomId: state + .thenItems[index] + ['uniqueCustomId'] as String, + ), + ); + + if (result != null) { + context + .read() + .add(AddToThenContainer({ + ...state.thenItems[index], + 'imagePath': Assets.automation, + 'title': state.thenItems[index] + ['name'] ?? + state.thenItems[index] + ['title'], + })); + } + return; + } + + final result = await DeviceDialogHelper + .showDeviceDialog( + context: context, + data: state.thenItems[index], + removeComparetors: true, + dialogType: 'THEN'); + if (result != null) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } else if (![ + 'AC', + '1G', + '2G', + '3G', + 'WPS', + 'CPS', + 'GW', + 'NCPS', + 'WH', + 'CUR', + ].contains(state.thenItems[index] + ['productType'])) { + context.read().add( + AddToThenContainer( + state.thenItems[index])); + } + }, + child: DraggableCard( + imagePath: state.thenItems[index] + ['imagePath'] as String? ?? + '', + title: state.thenItems[index]['title'] + as String? ?? + '', + deviceData: state.thenItems[index], + padding: const EdgeInsets.symmetric( + horizontal: 4, vertical: 8), + isFromThen: true, + isFromIf: false, + onRemove: () { + context.read().add( + RemoveDragCard( + index: index, + isFromThen: true, + key: state.thenItems[index] + ['uniqueCustomId'] + as String)); }, - child: DraggableCard( - imagePath: state.thenItems[index] - ['imagePath'] ?? - '', - title: state.thenItems[index] - ['title'] ?? - '', - deviceData: state.thenItems[index], - padding: const EdgeInsets.symmetric( - horizontal: 4, vertical: 8), - isFromThen: true, - isFromIf: false, - onRemove: () { - context.read().add( - RemoveDragCard( - index: index, - isFromThen: true, - key: state.thenItems[index] - ['uniqueCustomId'])); - }, - ), - ))), + ), + ))), ], ), ), @@ -230,7 +228,7 @@ class ThenContainer extends StatelessWidget { context: context, data: mutableData, removeComparetors: true, - dialogType: "THEN"); + dialogType: 'THEN'); if (result != null) { context.read().add(AddToThenContainer(mutableData)); } else if (![ @@ -241,9 +239,10 @@ class ThenContainer extends StatelessWidget { 'WPS', 'GW', 'CPS', - "NCPS", - "WH", + 'NCPS', + 'WH', 'PC', + 'CUR', ].contains(mutableData['productType'])) { context.read().add(AddToThenContainer(mutableData)); } diff --git a/lib/pages/space_management_v2/main_module/models/space_connection_model.dart b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart new file mode 100644 index 00000000..538a922c --- /dev/null +++ b/lib/pages/space_management_v2/main_module/models/space_connection_model.dart @@ -0,0 +1,6 @@ +class SpaceConnectionModel { + final String from; + final String to; + + const SpaceConnectionModel({required this.from, required this.to}); +} diff --git a/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart new file mode 100644 index 00000000..e9fa0a15 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpacesConnectionsArrowPainter extends CustomPainter { + final List connections; + final Map positions; + final double cardWidth = 150.0; + final double cardHeight = 90.0; + final Set highlightedUuids; + + SpacesConnectionsArrowPainter({ + required this.connections, + required this.positions, + required this.highlightedUuids, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final connection in connections) { + final isSelected = highlightedUuids.contains(connection.from) || + highlightedUuids.contains(connection.to); + final paint = Paint() + ..color = isSelected + ? ColorsManager.blackColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..strokeWidth = 2.0 + ..style = PaintingStyle.stroke; + + final from = positions[connection.from]; + final to = positions[connection.to]; + + if (from != null && to != null) { + final startPoint = + Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); + final endPoint = Offset(to.dx + cardWidth / 2, to.dy); + + final path = Path()..moveTo(startPoint.dx, startPoint.dy); + + final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); + final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); + + path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, + controlPoint2.dy, endPoint.dx, endPoint.dy); + + canvas.drawPath(path, paint); + + final circlePaint = Paint() + ..color = isSelected + ? ColorsManager.blackColor + : ColorsManager.blackColor.withValues(alpha: 0.5) + ..style = PaintingStyle.fill + ..blendMode = BlendMode.srcIn; + canvas.drawCircle(endPoint, 4, circlePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} diff --git a/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart new file mode 100644 index 00000000..5322c3ea --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart'; + +abstract final class SpaceManagementCommunityDialogHelper { + static void showCreateDialog(BuildContext context) { + showDialog( + context: context, + builder: (_) => CreateCommunityDialog( + title: const SelectableText('Community Name'), + onCreateCommunity: (community) { + context.read().add( + InsertCommunity(community), + ); + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart new file mode 100644 index 00000000..ac35975d --- /dev/null +++ b/lib/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart @@ -0,0 +1,43 @@ +import 'package:equatable/equatable.dart'; + +class PaginatedDataModel extends Equatable { + const PaginatedDataModel({ + required this.data, + required this.page, + required this.size, + required this.hasNext, + required this.totalItems, + required this.totalPages, + }); + + final List data; + final int page; + final int size; + final bool hasNext; + final int totalItems; + final int totalPages; + + factory PaginatedDataModel.fromJson( + Map json, + List Function(List) fromJsonList, + ) { + return PaginatedDataModel( + data: fromJsonList(json['data'] as List), + page: json['page'] as int? ?? 1, + size: json['size'] as int? ?? 25, + hasNext: json['hasNext'] as bool? ?? false, + totalItems: json['totalItem'] as int? ?? 0, + totalPages: json['totalPage'] as int? ?? 0, + ); + } + + @override + List get props => [ + data, + page, + size, + hasNext, + totalItems, + totalPages, + ]; +} diff --git a/lib/pages/space_management_v2/main_module/views/space_management_page.dart b/lib/pages/space_management_v2/main_module/views/space_management_page.dart new file mode 100644 index 00000000..957be65a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/views/space_management_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; +import 'package:syncrow_web/web_layout/web_scaffold.dart'; + +class SpaceManagementPage extends StatelessWidget { + const SpaceManagementPage({super.key}); + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CommunitiesBloc( + communitiesService: DebouncedCommunitiesService( + RemoteCommunitiesService(HTTPService()), + ), + )..add(const LoadCommunities(LoadCommunitiesParam())), + ), + BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()), + ], + child: WebScaffold( + appBarTitle: Text( + 'Space Management', + style: ResponsiveTextTheme.of(context).deviceManagementTitle, + ), + enableMenuSidebar: false, + centerBody: Text( + 'Community Structure', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontWeight: FontWeight.bold, + ), + ), + rightBody: const NavigateHomeGridView(), + scaffoldBody: const SpaceManagementBody(), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart new file mode 100644 index 00000000..4aea103a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; + +class CommunityStructureCanvas extends StatefulWidget { + const CommunityStructureCanvas({ + required this.community, + required this.selectedSpace, + super.key, + }); + + final CommunityModel community; + final SpaceModel? selectedSpace; + + @override + State createState() => _CommunityStructureCanvasState(); +} + +class _CommunityStructureCanvasState extends State + with SingleTickerProviderStateMixin { + final Map _positions = {}; + final double _cardWidth = 150.0; + final double _cardHeight = 90.0; + final double _horizontalSpacing = 150.0; + final double _verticalSpacing = 120.0; + + late TransformationController _transformationController; + late AnimationController _animationController; + + @override + void initState() { + _transformationController = TransformationController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 150), + ); + super.initState(); + } + + @override + void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _animateToSpace(widget.selectedSpace); + } + }); + } + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + Set _getAllDescendantUuids(SpaceModel space) { + final uuids = {}; + for (final child in space.children) { + uuids.add(child.uuid); + uuids.addAll(_getAllDescendantUuids(child)); + } + return uuids; + } + + void _runAnimation(Matrix4 target) { + final animation = Matrix4Tween( + begin: _transformationController.value, + end: target, + ).animate(_animationController); + + void listener() { + _transformationController.value = animation.value; + } + + animation.addListener(listener); + _animationController.forward(from: 0).whenCompleteOrCancel(() { + animation.removeListener(listener); + }); + } + + void _animateToSpace(SpaceModel? space) { + if (space == null) { + _runAnimation(Matrix4.identity()); + return; + } + + final position = _positions[space.uuid]; + if (position == null) return; + + const scale = 1.5; + final viewSize = context.size; + if (viewSize == null) return; + + final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); + final y = + -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); + + final matrix = Matrix4.identity() + ..translate(x, y) + ..scale(scale); + + _runAnimation(matrix); + } + + void _onSpaceTapped(SpaceModel? space) { + context.read().add( + SelectSpaceEvent(community: widget.community, space: space), + ); + } + + void _resetSelectionAndZoom() { + context.read().add( + SelectSpaceEvent( + community: widget.community, + space: null, + ), + ); + } + + void _calculateLayout( + List spaces, + int depth, + Map levelXOffset, + ) { + for (final space in spaces) { + double childSubtreeWidth = 0; + if (space.children.isNotEmpty) { + _calculateLayout(space.children, depth + 1, levelXOffset); + final firstChildPos = _positions[space.children.first.uuid]; + final lastChildPos = _positions[space.children.last.uuid]; + if (firstChildPos != null && lastChildPos != null) { + childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; + } + } + + final currentX = levelXOffset.putIfAbsent(depth, () => 0.0); + double? x; + + if (space.children.isNotEmpty) { + final firstChildPos = _positions[space.children.first.uuid]!; + x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; + } else { + x = currentX; + } + + if (x < currentX) { + final shiftX = currentX - x; + _shiftSubtree(space, shiftX); + final keysToShift = levelXOffset.keys.where((d) => d > depth).toList(); + for (final key in keysToShift) { + levelXOffset[key] = levelXOffset[key]! + shiftX; + } + x += shiftX; + } + + final y = depth * (_verticalSpacing + _cardHeight); + _positions[space.uuid] = Offset(x, y); + levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; + } + } + + void _shiftSubtree(SpaceModel space, double shiftX) { + if (_positions.containsKey(space.uuid)) { + _positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0); + } + for (final child in space.children) { + _shiftSubtree(child, shiftX); + } + } + + List _buildTreeWidgets() { + _positions.clear(); + final community = widget.community; + + _calculateLayout(community.spaces, 0, {}); + + final selectedSpace = widget.selectedSpace; + final highlightedUuids = {}; + if (selectedSpace != null) { + highlightedUuids.add(selectedSpace.uuid); + highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace)); + } + + final widgets = []; + final connections = []; + _generateWidgets(community.spaces, widgets, connections, highlightedUuids); + + return [ + CustomPaint( + painter: SpacesConnectionsArrowPainter( + connections: connections, + positions: _positions, + highlightedUuids: highlightedUuids, + ), + child: Stack(alignment: AlignmentDirectional.center, children: widgets), + ), + ]; + } + + void _generateWidgets( + List spaces, + List widgets, + List connections, + Set highlightedUuids, + ) { + for (final space in spaces) { + final position = _positions[space.uuid]; + if (position == null) continue; + + final isHighlighted = highlightedUuids.contains(space.uuid); + final hasNoSelectedSpace = widget.selectedSpace == null; + + widgets.add( + Positioned( + left: position.dx, + top: position.dy, + width: _cardWidth, + height: _cardHeight, + child: SpaceCardWidget( + buildSpaceContainer: () { + return Opacity( + opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, + child: Tooltip( + message: space.spaceName, + preferBelow: false, + child: SpaceCell( + onTap: () => _onSpaceTapped(space), + icon: space.icon, + name: space.spaceName, + ), + ), + ); + }, + onTap: () => SpaceDetailsDialogHelper.showCreate(context), + ), + ), + ); + + for (final child in space.children) { + connections.add( + SpaceConnectionModel(from: space.uuid, to: child.uuid), + ); + } + _generateWidgets(space.children, widgets, connections, highlightedUuids); + } + } + + @override + Widget build(BuildContext context) { + final treeWidgets = _buildTreeWidgets(); + return InteractiveViewer( + transformationController: _transformationController, + boundaryMargin: EdgeInsets.symmetric( + horizontal: MediaQuery.sizeOf(context).width * 0.3, + vertical: MediaQuery.sizeOf(context).height * 0.3, + ), + minScale: 0.5, + maxScale: 3.0, + constrained: false, + child: GestureDetector( + onTap: _resetSelectionAndZoom, + child: SizedBox( + width: MediaQuery.sizeOf(context).width * 5, + height: MediaQuery.sizeOf(context).height * 5, + child: Stack(children: treeWidgets), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart new file mode 100644 index 00000000..4352d069 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/community_template_cell.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CommunityTemplateCell extends StatelessWidget { + const CommunityTemplateCell({ + super.key, + required this.onTap, + required this.title, + }); + + final void Function() onTap; + final Widget title; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: AspectRatio( + aspectRatio: 2.0, + child: Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 4, + strokeAlign: BorderSide.strokeAlignOutside, + color: ColorsManager.borderColor, + ), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), + ), + DefaultTextStyle( + style: context.textTheme.bodyLarge!.copyWith( + color: ColorsManager.blackColor, + ), + child: title, + ), + ], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart new file mode 100644 index 00000000..4cbfd7fd --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/create_space_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateSpaceButton extends StatelessWidget { + const CreateSpaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => SpaceDetailsDialogHelper.showCreate(context), + child: Container( + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: Colors.blue, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart new file mode 100644 index 00000000..68169861 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/plus_button_widget.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class PlusButtonWidget extends StatelessWidget { + final Offset offset; + final void Function() onButtonTap; + + const PlusButtonWidget({ + super.key, + required this.offset, + required this.onButtonTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onButtonTap, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart new file mode 100644 index 00000000..e91e577f --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_card_widget.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart'; + +class SpaceCardWidget extends StatefulWidget { + final void Function() onTap; + final Widget Function() buildSpaceContainer; + + const SpaceCardWidget({ + required this.onTap, + required this.buildSpaceContainer, + super.key, + }); + + @override + State createState() => _SpaceCardWidgetState(); +} + +class _SpaceCardWidgetState extends State { + bool isHovered = false; + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: SizedBox( + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + widget.buildSpaceContainer(), + if (isHovered) + Positioned( + bottom: 0, + child: PlusButtonWidget( + offset: Offset.zero, + onButtonTap: widget.onTap, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_cell.dart b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart new file mode 100644 index 00000000..bcde6560 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_cell.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceCell extends StatelessWidget { + final String icon; + final String name; + final VoidCallback? onTap; + + const SpaceCell({ + super.key, + required this.icon, + required this.name, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 150, + height: 70, + decoration: _containerDecoration(), + child: Row( + children: [ + _buildIconContainer(), + const SizedBox(width: 10), + Expanded( + child: Text( + name, + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + } + + Widget _buildIconContainer() { + return Container( + width: 40, + height: double.infinity, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(15), + bottomLeft: Radius.circular(15), + ), + ), + child: Center( + child: SvgPicture.asset( + icon, + colorFilter: const ColorFilter.mode( + ColorsManager.whiteColors, + BlendMode.srcIn, + ), + width: 24, + height: 24, + ), + ), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: ColorsManager.lightGrayColor.withValues(alpha: 0.5), + spreadRadius: 2, + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart new file mode 100644 index 00000000..5d81bffb --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_body.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart'; + +class SpaceManagementBody extends StatelessWidget { + const SpaceManagementBody({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SpaceManagementCommunitiesTree(), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + previous.selectedCommunity != current.selectedCommunity, + builder: (context, state) => Visibility( + visible: state.selectedCommunity == null, + replacement: const SpaceManagementCommunityStructure(), + child: const SpaceManagementTemplatesView(), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart new file mode 100644 index 00000000..99d0668a --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; + +class SpaceManagementCommunityStructure extends StatelessWidget { + const SpaceManagementCommunityStructure({super.key}); + + @override + Widget build(BuildContext context) { + final selectionBloc = context.watch().state; + final selectedCommunity = selectionBloc.selectedCommunity; + final selectedSpace = selectionBloc.selectedSpace; + const spacer = Spacer(flex: 10); + return Visibility( + visible: selectedCommunity!.spaces.isNotEmpty, + replacement: const Row( + children: [spacer, Expanded(child: CreateSpaceButton()), spacer], + ), + child: CommunityStructureCanvas( + community: selectedCommunity, + selectedSpace: selectedSpace, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart new file mode 100644 index 00000000..138dbbc4 --- /dev/null +++ b/lib/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class SpaceManagementTemplatesView extends StatelessWidget { + const SpaceManagementTemplatesView({super.key}); + @override + Widget build(BuildContext context) { + return ColoredBox( + color: ColorsManager.whiteColors, + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 400, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.0, + ), + itemCount: _gridItems(context).length, + itemBuilder: (context, index) { + final model = _gridItems(context)[index]; + return CommunityTemplateCell( + onTap: model.onTap, + title: model.title, + ); + }, + ), + ); + } + + List<_CommunityTemplateModel> _gridItems(BuildContext context) { + return [ + _CommunityTemplateModel( + title: const Text('Blank'), + onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context), + ), + ]; + } +} + +class _CommunityTemplateModel { + final Widget title; + final void Function() onTap; + + _CommunityTemplateModel({ + required this.title, + required this.onTap, + }); +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart new file mode 100644 index 00000000..a97e8524 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; + +final class DebouncedCommunitiesService implements CommunitiesService { + DebouncedCommunitiesService( + this._decoratee, { + this.debounceDuration = const Duration(milliseconds: 500), + }); + + final CommunitiesService _decoratee; + final Duration debounceDuration; + + Timer? _debounceTimer; + late Completer? _completer; + + @override + Future getCommunity( + LoadCommunitiesParam param, + ) async { + _debounceTimer?.cancel(); + + _completer = Completer(); + final currentCompleter = _completer!; + + _debounceTimer = Timer(debounceDuration, () async { + try { + final result = await _decoratee.getCommunity(param); + if (!currentCompleter.isCompleted) { + currentCompleter.complete(result); + } + } catch (error) { + if (!currentCompleter.isCompleted) { + currentCompleter.completeError(error); + } + } + }); + + return currentCompleter.future; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart index 36682bb4..cc842de8 100644 --- a/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart @@ -1,9 +1,11 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart'; import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; +import 'package:syncrow_web/utils/constants/api_const.dart'; class RemoteCommunitiesService implements CommunitiesService { const RemoteCommunitiesService(this._httpService); @@ -13,14 +15,26 @@ class RemoteCommunitiesService implements CommunitiesService { static const _defaultErrorMessage = 'Failed to load communities'; @override - Future> getCommunity(LoadCommunitiesParam param) async { + Future getCommunity( + LoadCommunitiesParam param, + ) async { try { - return _httpService.get( - path: '/api/communities/', - expectedResponseModel: (json) => (json as List) - .map((e) => CommunityModel.fromJson(e as Map)) - .toList(), + final response = await _httpService.get( + path: await _makeUrl(), + queryParameters: { + 'page': param.page, + 'size': param.size, + 'includeSpaces': param.includeSpaces, + if (param.search.isNotEmpty && param.search != 'null') + 'search': param.search, + }, + expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson( + json as Map, + CommunityModel.fromJsonList, + ), ); + + return response; } on DioException catch (e) { final message = e.response?.data as Map?; final error = message?['error'] as Map?; @@ -31,4 +45,13 @@ class RemoteCommunitiesService implements CommunitiesService { throw APIException(formattedErrorMessage); } } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) throw APIException('Project UUID is required'); + return ApiEndpoints.getCommunityListv2.replaceAll( + '{projectId}', + projectUuid, + ); + } } diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart index c6efad9e..37f131b3 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/community_model.dart @@ -4,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain class CommunityModel extends Equatable { final String uuid; final String name; + final DateTime createdAt; + final DateTime updatedAt; + final String description; + final String externalId; final List spaces; const CommunityModel({ required this.uuid, required this.name, + required this.createdAt, + required this.updatedAt, + required this.description, + required this.externalId, required this.spaces, }); @@ -16,11 +24,20 @@ class CommunityModel extends Equatable { return CommunityModel( uuid: json['uuid'] as String, name: json['name'] as String, - spaces: (json['spaces'] as List) + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + description: json['description'] as String, + externalId: json['externalId']?.toString() ?? '', + spaces: (json['spaces'] as List? ?? []) .map((e) => SpaceModel.fromJson(e as Map)) .toList(), ); } + static List fromJsonList(List json) { + return json + .map((e) => CommunityModel.fromJson(e as Map)) + .toList(); + } @override List get props => [uuid, name, spaces]; diff --git a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart index 0f8aadb2..36943adb 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/models/space_model.dart @@ -2,26 +2,37 @@ import 'package:equatable/equatable.dart'; class SpaceModel extends Equatable { final String uuid; + final DateTime? createdAt; + final DateTime? updatedAt; final String spaceName; final String icon; final List children; + final SpaceModel? parent; const SpaceModel({ required this.uuid, + required this.createdAt, + required this.updatedAt, required this.spaceName, required this.icon, required this.children, + required this.parent, }); factory SpaceModel.fromJson(Map json) { return SpaceModel( - uuid: json['uuid'] as String, - spaceName: json['spaceName'] as String, - icon: json['icon'] as String, + uuid: json['uuid'] as String? ?? '', + createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''), + updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''), + spaceName: json['spaceName'] as String? ?? '', + icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg', children: (json['children'] as List?) ?.map((e) => SpaceModel.fromJson(e as Map)) .toList() ?? [], + parent: json['parent'] != null + ? SpaceModel.fromJson(json['parent'] as Map) + : null, ); } diff --git a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart index 9bdc215c..774c4c31 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart @@ -1,3 +1,32 @@ -class LoadCommunitiesParam { - const LoadCommunitiesParam(); +import 'package:equatable/equatable.dart'; + +class LoadCommunitiesParam extends Equatable { + const LoadCommunitiesParam({ + this.page = 1, + this.size = 25, + this.search = '', + this.includeSpaces = true, + }); + + final int page; + final int size; + final String search; + final bool includeSpaces; + + LoadCommunitiesParam copyWith({ + int? page, + int? size, + String? search, + bool? includeSpaces, + }) { + return LoadCommunitiesParam( + page: page ?? this.page, + size: size ?? this.size, + search: search ?? this.search, + includeSpaces: includeSpaces ?? this.includeSpaces, + ); + } + + @override + List get props => [page, size, search, includeSpaces]; } diff --git a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart index bccad2ad..baa84590 100644 --- a/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart +++ b/lib/pages/space_management_v2/modules/communities/domain/services/communities_service.dart @@ -1,6 +1,9 @@ +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +typedef CommunitiesPaginationModel = PaginatedDataModel; + abstract class CommunitiesService { - Future> getCommunity(LoadCommunitiesParam param); + Future getCommunity(LoadCommunitiesParam param); } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart index 0d85b22f..9094a632 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart @@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc { }) : _communitiesService = communitiesService, super(const CommunitiesState()) { on(_onLoadCommunities); + on(_onLoadMoreCommunities); + on(_onInsertCommunity); } final CommunitiesService _communitiesService; @@ -23,28 +25,93 @@ class CommunitiesBloc extends Bloc { Emitter emit, ) async { try { - emit(const CommunitiesState(status: CommunitiesStatus.loading)); - final communities = await _communitiesService.getCommunity(event.param); + emit( + state.copyWith(status: CommunitiesStatus.loading), + ); + + final paginationResponse = await _communitiesService.getCommunity( + event.param, + ); + emit( CommunitiesState( status: CommunitiesStatus.success, - communities: communities, + communities: paginationResponse.data, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + searchQuery: event.param.search, + isLoadingMore: false, ), ); } on APIException catch (e) { - emit( - CommunitiesState( - status: CommunitiesStatus.failure, - errorMessage: e.message, - ), - ); + _onApiException(e, emit); } catch (e) { - emit( - CommunitiesState( - status: CommunitiesStatus.failure, - errorMessage: e.toString(), - ), - ); + _onError(e, emit); } } + + Future _onLoadMoreCommunities( + LoadMoreCommunities event, + Emitter emit, + ) async { + if (!state.hasNext || state.isLoadingMore) return; + + try { + emit(state.copyWith(isLoadingMore: true)); + + final param = LoadCommunitiesParam( + page: state.currentPage + 1, + search: state.searchQuery, + ); + + final paginationResponse = await _communitiesService.getCommunity(param); + + final updatedCommunities = List.from(state.communities) + ..addAll(paginationResponse.data); + + emit( + state.copyWith( + status: CommunitiesStatus.success, + communities: updatedCommunities, + hasNext: paginationResponse.hasNext, + currentPage: paginationResponse.page, + isLoadingMore: false, + ), + ); + } on APIException catch (e) { + _onApiException(e, emit); + } catch (e) { + _onError(e, emit); + } + } + + void _onApiException( + APIException e, + Emitter emit, + ) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + isLoadingMore: false, + errorMessage: e.message, + ), + ); + } + + void _onError(Object e, Emitter emit) { + emit( + state.copyWith( + status: CommunitiesStatus.failure, + isLoadingMore: false, + errorMessage: e.toString(), + ), + ); + } + + void _onInsertCommunity( + InsertCommunity event, + Emitter emit, + ) { + emit(state.copyWith(communities: [event.community, ...state.communities])); + } } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart index ef375c5a..cd14fa3d 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_event.dart @@ -15,3 +15,19 @@ class LoadCommunities extends CommunitiesEvent { @override List get props => [param]; } + +class LoadMoreCommunities extends CommunitiesEvent { + const LoadMoreCommunities(); + + @override + List get props => []; +} + +final class InsertCommunity extends CommunitiesEvent { + const InsertCommunity(this.community); + + final CommunityModel community; + + @override + List get props => [community]; +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart index 94740f0b..c0e57ffd 100644 --- a/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart +++ b/lib/pages/space_management_v2/modules/communities/presentation/bloc/communities_state.dart @@ -7,12 +7,48 @@ final class CommunitiesState extends Equatable { this.status = CommunitiesStatus.initial, this.communities = const [], this.errorMessage, + this.isLoadingMore = false, + this.hasNext = false, + this.currentPage = 1, + this.searchQuery = '', }); final CommunitiesStatus status; final List communities; final String? errorMessage; + final bool isLoadingMore; + final bool hasNext; + final int currentPage; + final String searchQuery; + + CommunitiesState copyWith({ + CommunitiesStatus? status, + List? communities, + String? errorMessage, + bool? isLoadingMore, + bool? hasNext, + int? currentPage, + String? searchQuery, + }) { + return CommunitiesState( + status: status ?? this.status, + communities: communities ?? this.communities, + errorMessage: errorMessage ?? this.errorMessage, + isLoadingMore: isLoadingMore ?? this.isLoadingMore, + hasNext: hasNext ?? this.hasNext, + currentPage: currentPage ?? this.currentPage, + searchQuery: searchQuery ?? this.searchQuery, + ); + } @override - List get props => [status, communities, errorMessage]; + List get props => [ + status, + communities, + errorMessage, + isLoadingMore, + hasNext, + currentPage, + searchQuery, + ]; } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart new file mode 100644 index 00000000..bdda04ee --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart @@ -0,0 +1,47 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; + +part 'communities_tree_selection_event.dart'; +part 'communities_tree_selection_state.dart'; + +class CommunitiesTreeSelectionBloc + extends Bloc { + CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { + on(_onSelectCommunity); + on(_onSelectSpace); + on(_onClearSelection); + } + + void _onSelectCommunity( + SelectCommunityEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: null, + ), + ); + } + + void _onSelectSpace( + SelectSpaceEvent event, + Emitter emit, + ) { + emit( + CommunitiesTreeSelectionState( + selectedCommunity: event.community, + selectedSpace: event.space, + ), + ); + } + + void _onClearSelection( + ClearCommunitiesTreeSelectionEvent event, + Emitter emit, + ) { + emit(const CommunitiesTreeSelectionState()); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart new file mode 100644 index 00000000..21088632 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_event.dart @@ -0,0 +1,31 @@ +part of 'communities_tree_selection_bloc.dart'; + +sealed class CommunitiesTreeSelectionEvent extends Equatable { + const CommunitiesTreeSelectionEvent(); + + @override + List get props => []; +} + +final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { + final CommunityModel community; + + const SelectCommunityEvent({required this.community}); + @override + List get props => [community]; +} + +final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { + final SpaceModel? space; + final CommunityModel community; + + const SelectSpaceEvent({required this.space, required this.community}); + + @override + List get props => [space]; +} + +final class ClearCommunitiesTreeSelectionEvent + extends CommunitiesTreeSelectionEvent { + const ClearCommunitiesTreeSelectionEvent(); +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart new file mode 100644 index 00000000..b14d330b --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_state.dart @@ -0,0 +1,29 @@ +part of 'communities_tree_selection_bloc.dart'; + +final class CommunitiesTreeSelectionState extends Equatable { + const CommunitiesTreeSelectionState({ + this.selectedCommunity, + this.selectedSpace, + }); + + final CommunityModel? selectedCommunity; + final SpaceModel? selectedSpace; + + CommunitiesTreeSelectionState copyWith({ + CommunityModel? selectedCommunity, + SpaceModel? selectedSpace, + List? expandedCommunities, + List? expandedSpaces, + }) { + return CommunitiesTreeSelectionState( + selectedCommunity: selectedCommunity ?? this.selectedCommunity, + selectedSpace: selectedSpace ?? this.selectedSpace, + ); + } + + @override + List get props => [ + selectedCommunity, + selectedSpace, + ]; + } diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart new file mode 100644 index 00000000..cfd32f52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; + +class CommunitiesTreeFailureWidget extends StatelessWidget { + const CommunitiesTreeFailureWidget({super.key, this.errorMessage}); + + final String? errorMessage; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'Something went wrong', + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.read().add( + LoadCommunities( + LoadCommunitiesParam( + search: context.read().state.searchQuery, + ), + ), + ), + child: const Text('Retry'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart new file mode 100644 index 00000000..0baaae52 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class CommunityTile extends StatelessWidget { + final String title; + final List? children; + final bool isExpanded; + final bool isSelected; + final void Function(String, bool isExpanded) onExpansionChanged; + final void Function() onItemSelected; + + const CommunityTile({ + super.key, + required this.title, + required this.isExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: CustomExpansionTile( + title: title, + initiallyExpanded: isExpanded, + isSelected: isSelected, + onExpansionChanged: (bool expanded) { + onExpansionChanged(title, expanded); + }, + onItemSelected: onItemSelected, + children: children ?? [], + )); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart new file mode 100644 index 00000000..bfc9e30e --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget { + const EmptyCommunitiesTreeSearchResultWidget({ + required this.searchQuery, + super.key, + }); + + final String searchQuery; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + searchQuery.isEmpty + ? 'No communities found' + : 'No communities found for "$searchQuery"', + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart new file mode 100644 index 00000000..1adf9911 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/app_loading_indicator.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementCommunitiesTree extends StatefulWidget { + const SpaceManagementCommunitiesTree({super.key}); + + @override + State createState() => + _SpaceManagementCommunitiesTreeState(); +} + +class _SpaceManagementCommunitiesTreeState + extends State { + @override + void initState() { + context.read().add( + const LoadCommunities(LoadCommunitiesParam()), + ); + super.initState(); + } + + void _onSearchChanged(String searchQuery) { + context + .read() + .add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim()))); + } + + void _onLoadMore() { + context.read().add(const LoadMoreCommunities()); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Container( + width: 320, + decoration: subSectionContainerDecoration, + child: Column( + children: [ + const SpaceManagementSidebarHeader(), + CustomSearchBar( + onSearchChanged: _onSearchChanged, + ), + const SizedBox(height: 16), + switch (state.status) { + CommunitiesStatus.initial => const AppLoadingIndicator(), + CommunitiesStatus.loading => state.communities.isEmpty + ? const AppLoadingIndicator() + : _buildCommunitiesTree(context, state), + CommunitiesStatus.success => _buildCommunitiesTree(context, state), + CommunitiesStatus.failure => CommunitiesTreeFailureWidget( + errorMessage: state.errorMessage, + ), + }, + Visibility( + visible: state.isLoadingMore, + child: const AppLoadingIndicator(), + ), + ], + ), + ), + ); + } + + Widget _buildCommunitiesTree( + BuildContext context, + CommunitiesState state, + ) { + final communitiesIsEmpty = state.communities.isEmpty; + final statusIsSuccess = state.status == CommunitiesStatus.success; + + return Expanded( + child: Visibility( + visible: statusIsSuccess && communitiesIsEmpty, + replacement: Stack( + children: [ + SpaceManagementSidebarCommunitiesList( + communities: state.communities, + onLoadMore: state.hasNext ? _onLoadMore : null, + isLoadingMore: state.isLoadingMore, + hasNext: state.hasNext, + itemBuilder: (context, index) { + return SpaceManagementCommunitiesTreeCommunityTile( + community: state.communities[index], + ); + }, + ), + if (state.status == CommunitiesStatus.loading && + state.communities.isNotEmpty) + ColoredBox( + color: Colors.white.withValues(alpha: 0.7), + child: const AppLoadingIndicator(), + ), + ], + ), + child: EmptyCommunitiesTreeSearchResultWidget( + searchQuery: state.searchQuery, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart new file mode 100644 index 00000000..736a499f --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart'; + +class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeCommunityTile({ + required this.community, + super.key, + }); + + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaces = community.spaces + .map( + (space) => SpaceManagementCommunitiesTreeSpaceTile( + space: space, + community: community, + ), + ) + .toList(); + return CommunityTile( + title: community.name, + key: ValueKey(community.uuid), + isSelected: context + .watch() + .state + .selectedCommunity + ?.uuid == + community.uuid, + isExpanded: false, + onItemSelected: () { + context.read().add( + SelectCommunityEvent(community: community), + ); + }, + onExpansionChanged: (title, expanded) {}, + children: spaces, + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart new file mode 100644 index 00000000..795e2c3a --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart'; + +class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { + const SpaceManagementCommunitiesTreeSpaceTile({ + required this.space, + required this.community, + super.key, + }); + + final SpaceModel space; + final CommunityModel community; + + @override + Widget build(BuildContext context) { + final spaceIsExpanded = _isSpaceOrChildSelected(context, space); + final isSelected = + context.watch().state.selectedSpace?.uuid == + space.uuid; + return Padding( + padding: const EdgeInsetsDirectional.only(start: 16.0), + child: SpaceTile( + title: space.spaceName, + key: ValueKey(space.uuid), + isSelected: isSelected, + initiallyExpanded: spaceIsExpanded, + onExpansionChanged: (expanded) {}, + onItemSelected: () => context.read().add( + SelectSpaceEvent(community: community, space: space), + ), + children: space.children + .map( + (childSpace) => SpaceManagementCommunitiesTreeSpaceTile( + space: childSpace, + community: community, + ), + ) + .toList(), + ), + ); + } + + bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) { + final selectedSpace = + context.read().state.selectedSpace; + final isSpaceSelected = selectedSpace?.uuid == space.uuid; + final anySubSpaceIsSelected = space.children.any( + (child) => _isSpaceOrChildSelected(context, child), + ); + return isSpaceSelected || anySubSpaceIsSelected; + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart new file mode 100644 index 00000000..ba281335 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart @@ -0,0 +1,34 @@ +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'; + +class SpaceManagementSidebarAddCommunityButton extends StatelessWidget { + const SpaceManagementSidebarAddCommunityButton({ + required this.onTap, + super.key, + }); + + final void Function() onTap; + + @override + Widget build(BuildContext context) { + return SizedBox.square( + dimension: 30, + child: IconButton( + style: IconButton.styleFrom( + iconSize: 20, + backgroundColor: ColorsManager.circleImageBackground, + shape: const CircleBorder( + side: BorderSide( + color: ColorsManager.lightGrayBorderColor, + width: 3, + ), + ), + ), + onPressed: onTap, + icon: SvgPicture.asset(Assets.addIcon), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart new file mode 100644 index 00000000..40766be5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SpaceManagementSidebarCommunitiesList extends StatefulWidget { + const SpaceManagementSidebarCommunitiesList({ + required this.communities, + required this.itemBuilder, + this.onLoadMore, + this.isLoadingMore = false, + this.hasNext = false, + super.key, + }); + + final List communities; + final Widget Function(BuildContext context, int index) itemBuilder; + final VoidCallback? onLoadMore; + final bool isLoadingMore; + final bool hasNext; + + @override + State createState() => + _SpaceManagementSidebarCommunitiesListState(); +} + +class _SpaceManagementSidebarCommunitiesListState + extends State { + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100) { + if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) { + widget.onLoadMore!(); + } + } + } + + bool _onNotification(ScrollEndNotification notification) { + final hasReachedEnd = notification.metrics.extentAfter == 0; + if (hasReachedEnd && + widget.hasNext && + !widget.isLoadingMore && + widget.onLoadMore != null) { + widget.onLoadMore!(); + return true; + } + + return false; + } + + @override + void dispose() { + _scrollController + ..removeListener(_onScroll) + ..dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0); + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SizedBox( + width: context.screenWidth * 0.5, + child: Scrollbar( + scrollbarOrientation: ScrollbarOrientation.left, + thumbVisibility: true, + controller: _scrollController, + child: NotificationListener( + onNotification: _onNotification, + child: ListView.builder( + shrinkWrap: true, + padding: const EdgeInsetsDirectional.only(start: 16), + itemCount: itemCount, + controller: _scrollController, + itemBuilder: (context, index) { + if (index == widget.communities.length) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return widget.itemBuilder(context, index); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart new file mode 100644 index 00000000..b5f2a1b7 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class SpaceManagementSidebarHeader extends StatelessWidget { + const SpaceManagementSidebarHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration, + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Communities', + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + ), + ), + SpaceManagementSidebarAddCommunityButton( + onTap: () => _onAddCommunity(context), + ), + ], + ), + ); + } + + void _onAddCommunity(BuildContext context) { + final bloc = context.read(); + final selectedCommunity = bloc.state.selectedCommunity; + final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false; + + if (isSelected) { + _clearSelection(context); + } else { + SpaceManagementCommunityDialogHelper.showCreateDialog(context); + } + } + + void _clearSelection(BuildContext context) { + context.read().add( + const ClearCommunitiesTreeSelectionEvent(), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart new file mode 100644 index 00000000..d05199f0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; + +class SpaceTile extends StatefulWidget { + final String title; + final bool isSelected; + final bool initiallyExpanded; + final ValueChanged onExpansionChanged; + final List? children; + final void Function() onItemSelected; + + const SpaceTile({ + super.key, + required this.title, + required this.initiallyExpanded, + required this.onExpansionChanged, + required this.onItemSelected, + required this.isSelected, + this.children, + }); + + @override + State createState() => _SpaceTileState(); +} + +class _SpaceTileState extends State { + late bool _isExpanded; + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0), + child: CustomExpansionTile( + isSelected: widget.isSelected, + title: widget.title, + initiallyExpanded: _isExpanded, + onItemSelected: widget.onItemSelected, + onExpansionChanged: (bool expanded) { + setState(() { + _isExpanded = expanded; + }); + widget.onExpansionChanged(expanded); + }, + children: widget.children ?? [], + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart index be83124b..aae92e9f 100644 --- a/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart +++ b/lib/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart'; @@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService { Future createCommunity(CreateCommunityParam param) async { try { final response = await _httpService.post( - path: 'endpoint', - expectedResponseModel: (data) => CommunityModel.fromJson( - data as Map, - ), + path: await _makeUrl(), + body: { + 'name': param.name, + 'description': param.description, + }, + expectedResponseModel: (data) { + final json = data as Map; + if (json['success'] == true) { + return CommunityModel.fromJson( + json['data'] as Map, + ); + } + return null; + }, ); + + if (response == null) { + throw APIException( + _getErrorMessageFromBody(response as Map?), + ); + } return response; } on DioException catch (e) { final message = e.response?.data as Map?; - final error = message?['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; - final formattedErrorMessage = [ - _defaultErrorMessage, - errorMessage, - ].join(': '); - throw APIException(formattedErrorMessage); + throw APIException(_getErrorMessageFromBody(message)); } catch (e) { final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); throw APIException(formattedErrorMessage); } } + + String _getErrorMessageFromBody(Map? body) { + if (body == null) { + return _defaultErrorMessage; + } + final error = body['error'] as Map?; + final errorMessage = error?['message'] as String? ?? ''; + return errorMessage; + } + + Future _makeUrl() async { + final projectUuid = await ProjectManager.getProjectUUID(); + if (projectUuid == null) { + throw APIException('Project UUID is not set'); + } + return '/projects/$projectUuid/communities'; + } } diff --git a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart index 3d7c203b..68a9fa11 100644 --- a/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart +++ b/lib/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart @@ -1,9 +1,13 @@ import 'package:equatable/equatable.dart'; class CreateCommunityParam extends Equatable { - const CreateCommunityParam({required this.name}); - + const CreateCommunityParam({ + required this.name, + this.description = '', + }); + final String name; + final String description; @override List get props => [name]; diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart new file mode 100644 index 00000000..a9af44d6 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class CreateCommunityDialog extends StatelessWidget { + final void Function(CommunityModel community) onCreateCommunity; + final String? initialName; + final Widget title; + + const CreateCommunityDialog({ + super.key, + required this.onCreateCommunity, + required this.title, + this.initialName, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())), + child: BlocListener( + listener: (context, state) { + switch (state) { + case CreateCommunityLoading(): + showDialog( + context: context, + builder: (context) => const Center( + child: CircularProgressIndicator(), + ), + ); + break; + case CreateCommunitySuccess(:final community): + Navigator.of(context).pop(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Community created successfully')), + ); + onCreateCommunity.call(community); + break; + case CreateCommunityFailure(): + Navigator.of(context).pop(); + break; + default: + break; + } + }, + child: CreateCommunityDialogWidget( + title: title, + initialName: initialName, + ), + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart new file mode 100644 index 00000000..ab9f7b9a --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CreateCommunityDialogWidget extends StatefulWidget { + final String? initialName; + final Widget title; + + const CreateCommunityDialogWidget({ + super.key, + required this.title, + this.initialName, + }); + + @override + State createState() => + _CreateCommunityDialogWidgetState(); +} + +class _CreateCommunityDialogWidgetState extends State { + late final TextEditingController _nameController; + + @override + void initState() { + _nameController = TextEditingController(text: widget.initialName ?? ''); + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + final _formKey = GlobalKey(); + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + backgroundColor: ColorsManager.transparentColor, + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withValues(alpha: 0.25), + blurRadius: 20, + spreadRadius: 5, + offset: const Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: Theme.of(context).textTheme.headlineMedium!, + child: widget.title, + ), + const SizedBox(height: 18), + CreateCommunityNameTextField( + nameController: _nameController, + ), + if (state case CreateCommunityFailure(:final message)) + Padding( + padding: const EdgeInsets.only(top: 18), + child: SelectableText( + '* $message', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Widget _buildActionButtons(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () => Navigator.of(context).pop(), + ), + ), + const SizedBox(width: 16), + _buildCreateCommunityButton(context), + ], + ); + } + + Widget _buildCreateCommunityButton(BuildContext context) { + return Expanded( + child: DefaultButton( + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _onSubmit(context); + } + }, + borderRadius: 10, + foregroundColor: ColorsManager.whiteColors, + child: const Text('OK'), + ), + ); + } + + void _onSubmit(BuildContext context) { + if (_formKey.currentState?.validate() ?? false) { + context.read().add( + CreateCommunity( + CreateCommunityParam( + name: _nameController.text.trim(), + ), + ), + ); + } + } +} diff --git a/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart new file mode 100644 index 00000000..d42474d5 --- /dev/null +++ b/lib/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CreateCommunityNameTextField extends StatelessWidget { + const CreateCommunityNameTextField({ + required this.nameController, + super.key, + }); + + final TextEditingController nameController; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: nameController, + validator: _validator, + style: context.textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the community name', + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: _buildBorder(ColorsManager.boxColor), + focusedBorder: _buildBorder(), + focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error), + errorBorder: _buildBorder(Theme.of(context).colorScheme.error), + ), + ); + } + + String? _validator(String? value) { + if (value == null || value.isEmpty) { + return '*Name should not be empty.'; + } + + return null; + } + + InputBorder _buildBorder([Color? color]) { + return OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5), + width: 1, + ), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart new file mode 100644 index 00000000..e871f4d0 --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; + +abstract final class SpaceDetailsDialogHelper { + static void showCreate(BuildContext context) { + showDialog( + context: context, + builder: (context) => const SpaceDetailsDialog(), + ); + } +} diff --git a/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart new file mode 100644 index 00000000..7213c99e --- /dev/null +++ b/lib/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class SpaceDetailsDialog extends StatelessWidget { + const SpaceDetailsDialog({super.key}); + + @override + Widget build(BuildContext context) { + return const Dialog( + child: Text('Create Space'), + ); + } +} diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index e8c2e015..7d1a4d96 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc { selectedSpaces: updatedSelectedSpaces, soldCheck: updatedSoldChecks, selectedCommunityAndSpaces: communityAndSpaces)); - emit(state.copyWith(selectedSpaces: updatedSelectedSpaces)); } catch (e) { emit(const SpaceTreeErrorState('Something went wrong')); } @@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc { List _getThePathToChild(String communityId, String selectedSpaceId) { List ids = []; - for (var community in state.communityList) { + final communityDataSource = + state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; + for (final community in communityDataSource) { if (community.uuid == communityId) { - for (var space in community.spaces) { - List list = []; + for (final space in community.spaces) { + final list = []; list.add(space.uuid!); ids = _getAllParentsIds(space, selectedSpaceId, List.from(list)); if (ids.isNotEmpty) { diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart index d72f22ac..d81a3b04 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_tile_widget.dart @@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart'; class SpaceTile extends StatefulWidget { final String title; final bool isSelected; - final bool initiallyExpanded; final ValueChanged onExpansionChanged; final List? children; - final Function() onItemSelected; + final void Function() onItemSelected; const SpaceTile({ super.key, diff --git a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart index 438b1abf..6dc20cfd 100644 --- a/lib/pages/visitor_password/bloc/visitor_password_bloc.dart +++ b/lib/pages/visitor_password/bloc/visitor_password_bloc.dart @@ -68,7 +68,7 @@ class VisitorPasswordBloc DateTime? startTime = DateTime.now(); DateTime? endTime; - String startTimeAccess = 'Start Time'; + String startTimeAccess = DateTime.now().toString().split('.').first; String endTimeAccess = 'End Time'; PasswordStatus? passwordStatus; selectAccessType( @@ -136,6 +136,27 @@ class VisitorPasswordBloc ); return; } + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) { + await showDialog( + context: event.context, + builder: (context) => AlertDialog( + title: const Text('Effective Time cannot be earlier than current time.'), + actionsAlignment: MainAxisAlignment.center, + content: + FilledButton( + onPressed: () { + Navigator.of(event.context).pop(); + add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false)); + }, + child: const Text('OK'), + ), + + ), + ); + } + return; + } effectiveTimeTimeStamp = selectedTimestamp; startTimeAccess = selectedDateTime.toString().split('.').first; } else { diff --git a/lib/pages/visitor_password/model/failed_operation.dart b/lib/pages/visitor_password/model/failed_operation.dart index 223f9ac5..120f6d89 100644 --- a/lib/pages/visitor_password/model/failed_operation.dart +++ b/lib/pages/visitor_password/model/failed_operation.dart @@ -2,11 +2,13 @@ class FailedOperation { final bool success; final dynamic deviceUuid; final dynamic error; + final String deviceName; FailedOperation({ required this.success, required this.deviceUuid, required this.error, + required this.deviceName, }); factory FailedOperation.fromJson(Map json) { @@ -14,6 +16,7 @@ class FailedOperation { success: json['success'], deviceUuid: json['deviceUuid'], error: json['error'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -22,21 +25,22 @@ class FailedOperation { 'success': success, 'deviceUuid': deviceUuid, 'error': error, + 'deviceName': deviceName, }; } } - - class SuccessOperation { final bool success; // final Result result; final String deviceUuid; + final String deviceName; SuccessOperation({ required this.success, // required this.result, required this.deviceUuid, + required this.deviceName, }); factory SuccessOperation.fromJson(Map json) { @@ -44,6 +48,7 @@ class SuccessOperation { success: json['success'], // result: Result.fromJson(json['result']), deviceUuid: json['deviceUuid'], + deviceName: json['deviceName'] as String? ?? '', ); } @@ -52,6 +57,7 @@ class SuccessOperation { 'success': success, // 'result': result.toJson(), 'deviceUuid': deviceUuid, + 'deviceName': deviceName, }; } } @@ -92,8 +98,6 @@ class SuccessOperation { // } // } - - class PasswordStatus { final List successOperations; final List failedOperations; @@ -121,4 +125,3 @@ class PasswordStatus { }; } } - diff --git a/lib/pages/visitor_password/view/access_type_radio_group.dart b/lib/pages/visitor_password/view/access_type_radio_group.dart new file mode 100644 index 00000000..be4adb9d --- /dev/null +++ b/lib/pages/visitor_password/view/access_type_radio_group.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; + +class AccessTypeRadioGroup extends StatelessWidget { + final String? selectedType; + final String? accessTypeSelected; + final Function(String) onTypeSelected; + final VisitorPasswordBloc visitorBloc; + + const AccessTypeRadioGroup({ + super.key, + required this.selectedType, + required this.accessTypeSelected, + required this.onTypeSelected, + required this.visitorBloc, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), + ), + Text('Access Type', style: text), + ], + ), + const SizedBox(height: 8), + if (size.width < 800) + Column( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + ), + ], + ) + else + Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + _buildRadioTile( + context, + 'Online Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + _buildRadioTile( + context, + 'Offline Password', + selectedType ?? accessTypeSelected, + onTypeSelected, + width: size.width * 0.12, + ), + ], + ), + ), + const Spacer(flex: 2), + ], + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + }) { + return SizedBox( + width: width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, + style: Theme.of(context).textTheme.bodySmall!.copyWith( + color: Colors.black, + fontSize: 13, + )), + value: value, + groupValue: groupValue, + onChanged: (value) { + if (value != null) { + onChanged(value); + if (value == 'Dynamic Password') { + visitorBloc.usageFrequencySelected = ''; + } + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/responsive_fields_row.dart b/lib/pages/visitor_password/view/responsive_fields_row.dart new file mode 100644 index 00000000..92a79276 --- /dev/null +++ b/lib/pages/visitor_password/view/responsive_fields_row.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; + +class NameAndEmailFields extends StatelessWidget { + final TextEditingController nameController; + final TextEditingController emailController; + final String? Function(String?)? nameValidator; + final String? Function(String?)? emailValidator; + + const NameAndEmailFields({ + super.key, + required this.nameController, + required this.emailController, + required this.nameValidator, + required this.emailValidator, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + return Container( + width: size.width, + child: size.width < 800 + ? Column( + children: [ + CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + const SizedBox(height: 15), + CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ], + ) + : Row( + children: [ + Expanded( + flex: 2, + child: CustomWebTextField( + validator: nameValidator, + controller: nameController, + isRequired: true, + textFieldName: 'Name', + description: '', + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: CustomWebTextField( + validator: emailValidator, + controller: emailController, + isRequired: true, + textFieldName: 'Email Address', + description: + 'The password will be sent to the visitor’s email address.', + ), + ), + const Spacer(), + ], + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/usage_frequency_radio_group.dart b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart new file mode 100644 index 00000000..aebebefe --- /dev/null +++ b/lib/pages/visitor_password/view/usage_frequency_radio_group.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class UsageFrequencyRadioGroup extends StatelessWidget { + final String? selectedFrequency; + final String? usageFrequencySelected; + final Function(String) onFrequencySelected; + + const UsageFrequencyRadioGroup({ + super.key, + required this.selectedFrequency, + required this.usageFrequencySelected, + required this.onFrequencySelected, + }); + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); + + return size.width < 600 + ? Column( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + const SizedBox(height: 8), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + text: text, + fullWidth: true, + ), + ], + ) + : Row( + children: [ + _buildRadioTile( + context, + 'One-Time', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + _buildRadioTile( + context, + 'Periodic', + selectedFrequency ?? usageFrequencySelected, + onFrequencySelected, + width: size.width * 0.12, + text: text, + ), + ], + ); + } + + Widget _buildRadioTile( + BuildContext context, + String value, + String? groupValue, + Function(String) onChanged, { + double? width, + required TextStyle text, + bool fullWidth = false, + }) { + return SizedBox( + width: fullWidth ? double.infinity : width, + child: RadioListTile( + contentPadding: EdgeInsets.zero, + title: Text(value, style: text), + value: value, + groupValue: groupValue, + onChanged: (String? value) { + if (value != null) { + onChanged(value); + } + }, + ), + ); + } +} diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 1e43af46..d1fb172a 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -9,8 +9,11 @@ import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; +import 'package:syncrow_web/pages/visitor_password/view/access_type_radio_group.dart'; import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; +import 'package:syncrow_web/pages/visitor_password/view/responsive_fields_row.dart'; +import 'package:syncrow_web/pages/visitor_password/view/usage_frequency_radio_group.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -21,7 +24,10 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { Size size = MediaQuery.of(context).size; - var text = Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black, fontSize: 13); + var text = Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.black, fontSize: 13); return BlocProvider( create: (context) => VisitorPasswordBloc(), child: BlocListener( @@ -35,7 +41,8 @@ class VisitorPasswordDialog extends StatelessWidget { title: 'Sent Successfully', widgeta: Column( children: [ - if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.failedOperations.isNotEmpty) Column( children: [ const Text('Failed Devices'), @@ -45,7 +52,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.failedOperations.length, + itemCount: visitorBloc + .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), @@ -53,14 +61,17 @@ class VisitorPasswordDialog extends StatelessWidget { height: 45, child: Center( child: Text(visitorBloc - .passwordStatus!.failedOperations[index].deviceUuid)), + .passwordStatus! + .failedOperations[index] + .deviceName)), ); }, ), ), ], ), - if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc + .passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -70,15 +81,18 @@ class VisitorPasswordDialog extends StatelessWidget { child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, - itemCount: visitorBloc.passwordStatus!.successOperations.length, + itemCount: visitorBloc + .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( margin: EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc.passwordStatus! - .successOperations[index].deviceUuid)), + child: Text(visitorBloc + .passwordStatus! + .successOperations[index] + .deviceName)), ); }, ), @@ -88,8 +102,7 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(true); - + Navigator.of(context).pop(v); }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -102,15 +115,16 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; + bool isRepeat = + state is IsRepeatState ? state.repeat : visitorBloc.repeat; return AlertDialog( backgroundColor: Colors.white, title: Text( 'Create visitor password', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), + style: Theme.of(context).textTheme.headlineLarge!.copyWith( + fontWeight: FontWeight.w400, + fontSize: 24, + color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -121,34 +135,11 @@ class VisitorPasswordDialog extends StatelessWidget { padding: const EdgeInsets.all(5.0), child: ListBody( children: [ - Container( - child: Row( - children: [ - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validate, - controller: visitorBloc.userNameController, - isRequired: true, - textFieldName: 'Name', - description: '', - ), - ), - const Spacer(), - Expanded( - flex: 2, - child: CustomWebTextField( - validator: visitorBloc.validateEmail, - controller: visitorBloc.emailController, - isRequired: true, - textFieldName: 'Email Address', - description: - 'The password will be sent to the visitor’s email address.', - ), - ), - const Spacer(), - ], - ), + NameAndEmailFields( + nameController: visitorBloc.userNameController, + emailController: visitorBloc.emailController, + nameValidator: visitorBloc.validate, + emailValidator: visitorBloc.validateEmail, ), const SizedBox( height: 15, @@ -156,107 +147,43 @@ class VisitorPasswordDialog extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text('Access Type', style: text), - ], + AccessTypeRadioGroup( + selectedType: state is PasswordTypeSelected + ? state.selectedType + : null, + accessTypeSelected: + visitorBloc.accessTypeSelected, + onTypeSelected: (value) { + context + .read() + .add(SelectPasswordType(value)); + }, + visitorBloc: visitorBloc, ), - Row( - children: [ - Expanded( - flex: 2, - child: Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Online Password', - style: text, - ), - value: 'Online Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Offline Password', style: text), - value: 'Offline Password', - groupValue: (state is PasswordTypeSelected) - ? state.selectedType - : visitorBloc.accessTypeSelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectPasswordType(value)); - } - }, - ), - ), - // SizedBox( - // width: size.width * 0.12, - // child: RadioListTile( - // contentPadding: EdgeInsets.zero, - // title: Text( - // 'Dynamic Password', - // style: text, - // ), - // value: 'Dynamic Password', - // groupValue: (state is PasswordTypeSelected) - // ? state.selectedType - // : visitorBloc.accessTypeSelected, - // onChanged: (String? value) { - // if (value != null) { - // context - // .read() - // .add(SelectPasswordType(value)); - // visitorBloc.usageFrequencySelected = ''; - // } - // }, - // ), - // ), - ], - )), - const Spacer( - flex: 2, - ), - ], - ), - if (visitorBloc.accessTypeSelected == 'Online Password') + + if (visitorBloc.accessTypeSelected == + 'Online Password') Text( 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), - if (visitorBloc.accessTypeSelected == 'Offline Password') + if (visitorBloc.accessTypeSelected == + 'Offline Password') Text( 'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), // if (visitorBloc.accessTypeSelected == 'Dynamic Password') // Text( @@ -271,143 +198,170 @@ class VisitorPasswordDialog extends StatelessWidget { ) ], ), - visitorBloc.accessTypeSelected == 'Dynamic Password' - ? const SizedBox() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + if (visitorBloc.accessTypeSelected == + 'Dynamic Password') + const SizedBox() + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Text( - '* ', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.red), - ), - Text( - 'Usage Frequency', - style: text, - ), - ], + Text( + '* ', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.red), ), - Row( - children: [ - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'One-Time', - style: text, - ), - value: 'One-Time', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context - .read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - SizedBox( - width: size.width * 0.12, - child: RadioListTile( - contentPadding: EdgeInsets.zero, - title: Text('Periodic', style: text), - value: 'Periodic', - groupValue: (state is UsageFrequencySelected) - ? state.selectedFrequency - : visitorBloc.usageFrequencySelected, - onChanged: (String? value) { - if (value != null) { - context.read() - .add(SelectUsageFrequency(value)); - } - }, - ), - ), - ], + Text( + 'Usage Frequency', + style: text, ), - - //One-Time - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - // Periodic - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), - - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - Text( - 'Within the validity period, there is no limit to the number of times each device can be unlocked.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, fontSize: 9), - ), ], ), + UsageFrequencyRadioGroup( + selectedFrequency: + state is UsageFrequencySelected + ? state.selectedFrequency + : null, + usageFrequencySelected: + visitorBloc.usageFrequencySelected, + onFrequencySelected: (value) { + context + .read() + .add(SelectUsageFrequency(value)); + }, + ), + + //One-Time + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, each device can be unlocked only once.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + // Periodic + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + Text( + 'Within the validity period, there is no limit to the number of times each device can be unlocked.', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + color: ColorsManager.grayColor, + fontSize: 9), + ), + ], + ), const SizedBox( height: 20, ), - if ((visitorBloc.usageFrequencySelected != 'One-Time' || - visitorBloc.accessTypeSelected != 'Offline Password') && + if ((visitorBloc.usageFrequencySelected != + 'One-Time' || + visitorBloc.accessTypeSelected != + 'Offline Password') && (visitorBloc.usageFrequencySelected != '')) DateTimeWebWidget( isRequired: true, title: 'Access Period', size: size, endTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: false)); } else { - visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { - visitorBloc.add( - SelectTimeEvent(context: context, isEffective: true)); + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { + visitorBloc.add(SelectTimeEvent( + context: context, + isEffective: true)); } else { - visitorBloc.add(SelectTimeVisitorPassword( - context: context, isStart: true, isRepeat: false)); + visitorBloc.add( + SelectTimeVisitorPassword( + context: context, + isStart: true, + isRepeat: false)); } }, - firstString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + firstString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess.toString(), - secondString: (visitorBloc.usageFrequencySelected == - 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') + : visitorBloc.startTimeAccess + .toString(), + secondString: (visitorBloc + .usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') ? visitorBloc.expirationTime : visitorBloc.endTimeAccess.toString(), icon: Assets.calendarIcon), - const SizedBox(height: 10,), - Text(visitorBloc.accessPeriodValidate, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), + const SizedBox( + height: 10, + ), + Text( + visitorBloc.accessPeriodValidate, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.red), + ), const SizedBox( height: 20, ), @@ -431,16 +385,21 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Within the validity period, each device can be unlocked only once.', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.grayColor, - fontSize: 9), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.grayColor, + fontSize: 9), ), const SizedBox( height: 20, ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') SizedBox( width: 100, child: Column( @@ -451,7 +410,8 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc.add(ToggleRepeatEvent()); + visitorBloc + .add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -459,12 +419,16 @@ class VisitorPasswordDialog extends StatelessWidget { ], ), ), - if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') - isRepeat ? const RepeatWidget() : const SizedBox(), + if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') + isRepeat + ? const RepeatWidget() + : const SizedBox(), Container( decoration: containerDecoration, - width: size.width / 9, + width: size.width / 6, child: DefaultButton( onPressed: () { showDialog( @@ -472,22 +436,28 @@ class VisitorPasswordDialog extends StatelessWidget { barrierDismissible: false, builder: (BuildContext context) { return AddDeviceDialog( - selectedDeviceIds: visitorBloc.selectedDevices, + selectedDeviceIds: + visitorBloc.selectedDevices, ); }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = listDevice; + visitorBloc.selectedDevices = + listDevice; } }); }, borderRadius: 8, child: Text( '+ Add Device', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.whiteColors, - fontSize: 12), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith( + fontWeight: FontWeight.w400, + color: + ColorsManager.whiteColors, + fontSize: 12), ), ), ), @@ -506,7 +476,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(true); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -525,30 +495,37 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.selectedDevices.isNotEmpty) { - if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { setPasswordFunction(context, size, visitorBloc); - } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && - visitorBloc.effectiveTime != 'Start Time' ) { + visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); - }else{ + } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - } else if( - visitorBloc.endTimeAccess.toString()!='End Time' - &&visitorBloc.startTimeAccess.toString()!='Start Time') { + } else if (visitorBloc.endTimeAccess.toString() != + 'End Time' && + visitorBloc.startTimeAccess.toString() != + 'Start Time') { if (visitorBloc.effectiveTimeTimeStamp != null && visitorBloc.expirationTimeTimeStamp != null) { if (isRepeat == true) { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time' && visitorBloc.selectedDays.isNotEmpty) { - setPasswordFunction(context, size, visitorBloc); + setPasswordFunction( + context, size, visitorBloc); } else { visitorBloc.stateDialog( context: context, @@ -562,14 +539,16 @@ class VisitorPasswordDialog extends StatelessWidget { } else { visitorBloc.stateDialog( context: context, - message: 'Please select Access Period to continue', + message: + 'Please select Access Period to continue', title: 'Access Period'); } - }else{ - visitorBloc.stateDialog( - context: context, - message: 'Please select Access Period to continue', - title: 'Access Period'); + } else { + visitorBloc.stateDialog( + context: context, + message: + 'Please select Access Period to continue', + title: 'Access Period'); } } else { visitorBloc.stateDialog( @@ -615,7 +594,8 @@ class VisitorPasswordDialog extends StatelessWidget { content: SizedBox( height: size.height * 0.25, child: Center( - child: CircularProgressIndicator(), // Display a loading spinner + child: + CircularProgressIndicator(), // Display a loading spinner ), ), ); @@ -639,7 +619,10 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Set Password', - style: Theme.of(context).textTheme.headlineLarge!.copyWith( + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith( fontSize: 30, fontWeight: FontWeight.w400, color: Colors.black, @@ -668,7 +651,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -689,37 +672,45 @@ class VisitorPasswordDialog extends StatelessWidget { onPressed: () { Navigator.pop(context); if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Online Password') { + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Online Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); - } - else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'One-Time' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, )); - } - else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == 'Offline Password') { + } else if (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, - effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), - invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), + effectiveTime: + visitorBloc.effectiveTimeTimeStamp.toString(), + invalidTime: + visitorBloc.expirationTimeTimeStamp.toString(), )); } }, diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6f60e34f..6fb27daf 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:core'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; @@ -386,4 +387,34 @@ class DevicesManagementApi { ); return response; } + + Future postSchedule({ + required String category, + required String deviceId, + required String time, + required String code, + required bool value, + required List days, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.saveSchedule.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + body: jsonEncode( + { + 'category': category, + 'time': time, + 'function': { + 'code': code, + 'value': value, + }, + 'days': days + }, + ), + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index d58d0f28..eb7b6a3e 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,6 +46,7 @@ abstract class ApiEndpoints { // Community Module static const String createCommunity = '/projects/{projectId}/communities'; static const String getCommunityList = '/projects/{projectId}/communities'; + static const String getCommunityListv2 = '/projects/{projectId}/communities/v2'; static const String getCommunityById = '/projects/{projectId}/communities/{communityId}'; static const String updateCommunity = @@ -136,4 +137,5 @@ abstract class ApiEndpoints { static const String assignDeviceToRoom = '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; + static const String saveSchedule = '/schedule/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 331a4285..8979c446 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -1,128 +1,128 @@ class Assets { Assets._(); - static const String background = "assets/images/Background.png"; - static const String webBackground = "assets/images/web_Background.svg"; - static const String webBackgroundPng = "assets/images/web_Background.png"; - static const String blackLogo = "assets/images/black-logo.png"; - static const String logo = "assets/images/Logo.svg"; - static const String logoHorizontal = "assets/images/logo_horizontal.png"; - static const String vector = "assets/images/Vector.png"; - static const String loginLogo = "assets/images/login_logo.svg"; - static const String whiteLogo = "assets/images/white-logo.png"; - static const String window = "assets/images/Window.png"; - static const String liftLine = "assets/images/lift_line.png"; - static const String rightLine = "assets/images/right_line.png"; - static const String google = "assets/images/google.svg"; - static const String facebook = "assets/images/facebook.svg"; + static const String background = 'assets/images/Background.png'; + static const String webBackground = 'assets/images/web_Background.svg'; + static const String webBackgroundPng = 'assets/images/web_Background.png'; + static const String blackLogo = 'assets/images/black-logo.png'; + static const String logo = 'assets/images/Logo.svg'; + static const String logoHorizontal = 'assets/images/logo_horizontal.png'; + static const String vector = 'assets/images/Vector.png'; + static const String loginLogo = 'assets/images/login_logo.svg'; + static const String whiteLogo = 'assets/images/white-logo.png'; + static const String window = 'assets/images/Window.png'; + static const String liftLine = 'assets/images/lift_line.png'; + static const String rightLine = 'assets/images/right_line.png'; + static const String google = 'assets/images/google.svg'; + static const String facebook = 'assets/images/facebook.svg'; static const String invisiblePassword = - "assets/images/Password_invisible.svg"; - static const String visiblePassword = "assets/images/password_visible.svg"; - static const String accessIcon = "assets/images/access_icon.svg"; + 'assets/images/Password_invisible.svg'; + static const String visiblePassword = 'assets/images/password_visible.svg'; + static const String accessIcon = 'assets/images/access_icon.svg'; static const String spaseManagementIcon = - "assets/images/spase_management_icon.svg"; - static const String devicesIcon = "assets/images/devices_icon.svg"; - static const String analyticsIcon = "assets/icons/landing_analytics.svg"; + 'assets/images/spase_management_icon.svg'; + static const String devicesIcon = 'assets/images/devices_icon.svg'; + static const String analyticsIcon = 'assets/icons/landing_analytics.svg'; - static const String moveinIcon = "assets/images/movein_icon.svg"; - static const String constructionIcon = "assets/images/construction_icon.svg"; - static const String energyIcon = "assets/images/energy_icon.svg"; - static const String integrationsIcon = "assets/images/Integrations_icon.svg"; - static const String assetIcon = "assets/images/asset_icon.svg"; - static const String calendarIcon = "assets/images/calendar_icon.svg"; - static const String deviceNoteIcon = "assets/images/device_note.svg"; - static const String timeIcon = "assets/images/time_icon.svg"; - static const String emptyTable = "assets/images/empty_table.svg"; + static const String moveinIcon = 'assets/images/movein_icon.svg'; + static const String constructionIcon = 'assets/images/construction_icon.svg'; + static const String energyIcon = 'assets/images/energy_icon.svg'; + static const String integrationsIcon = 'assets/images/Integrations_icon.svg'; + static const String assetIcon = 'assets/images/asset_icon.svg'; + static const String calendarIcon = 'assets/images/calendar_icon.svg'; + static const String deviceNoteIcon = 'assets/images/device_note.svg'; + static const String timeIcon = 'assets/images/time_icon.svg'; + static const String emptyTable = 'assets/images/empty_table.svg'; // General assets static const String motionlessDetection = - "assets/icons/motionless_detection.svg"; - static const String acHeating = "assets/icons/ac_heating.svg"; - static const String acPowerOff = "assets/icons/ac_power_off.svg"; - static const String acFanMiddle = "assets/icons/ac_fan_middle.svg"; - static const String switchAlarmSound = "assets/icons/switch_alarm_sound.svg"; - static const String resetOff = "assets/icons/reset_off.svg"; + 'assets/icons/motionless_detection.svg'; + static const String acHeating = 'assets/icons/ac_heating.svg'; + static const String acPowerOff = 'assets/icons/ac_power_off.svg'; + static const String acFanMiddle = 'assets/icons/ac_fan_middle.svg'; + static const String switchAlarmSound = 'assets/icons/switch_alarm_sound.svg'; + static const String resetOff = 'assets/icons/reset_off.svg'; static const String sensitivityOperationIcon = - "assets/icons/sesitivity_operation_icon.svg"; - static const String motionDetection = "assets/icons/motion_detection.svg"; - static const String freezing = "assets/icons/freezing.svg"; - static const String indicator = "assets/icons/indicator.svg"; - static const String sceneRefresh = "assets/icons/scene_refresh.svg"; - static const String temperature = "assets/icons/tempreture.svg"; - static const String acFanHigh = "assets/icons/ac_fan_high.svg"; - static const String fanSpeed = "assets/icons/fan_speed.svg"; - static const String acFanLow = "assets/icons/ac_fan_low.svg"; - static const String sensitivity = "assets/icons/sensitivity.svg"; - static const String lightCountdown = "assets/icons/light_countdown.svg"; - static const String farDetection = "assets/icons/far_detection.svg"; - static const String sceneChildUnlock = "assets/icons/scene_child_unlock.svg"; - static const String acFanAuto = "assets/icons/ac_fan_auto.svg"; - static const String childLock = "assets/icons/child_lock.svg"; - static const String factoryReset = "assets/icons/factory_reset.svg"; - static const String acCooling = "assets/icons/ac_cooling.svg"; - static const String sceneChildLock = "assets/icons/scene_child_lock.svg"; - static const String celsiusDegrees = "assets/icons/celsius_degrees.svg"; - static const String masterState = "assets/icons/master_state.svg"; - static const String acPower = "assets/icons/ac_power.svg"; + 'assets/icons/sesitivity_operation_icon.svg'; + static const String motionDetection = 'assets/icons/motion_detection.svg'; + static const String freezing = 'assets/icons/freezing.svg'; + static const String indicator = 'assets/icons/indicator.svg'; + static const String sceneRefresh = 'assets/icons/scene_refresh.svg'; + static const String temperature = 'assets/icons/tempreture.svg'; + static const String acFanHigh = 'assets/icons/ac_fan_high.svg'; + static const String fanSpeed = 'assets/icons/fan_speed.svg'; + static const String acFanLow = 'assets/icons/ac_fan_low.svg'; + static const String sensitivity = 'assets/icons/sensitivity.svg'; + static const String lightCountdown = 'assets/icons/light_countdown.svg'; + static const String farDetection = 'assets/icons/far_detection.svg'; + static const String sceneChildUnlock = 'assets/icons/scene_child_unlock.svg'; + static const String acFanAuto = 'assets/icons/ac_fan_auto.svg'; + static const String childLock = 'assets/icons/child_lock.svg'; + static const String factoryReset = 'assets/icons/factory_reset.svg'; + static const String acCooling = 'assets/icons/ac_cooling.svg'; + static const String sceneChildLock = 'assets/icons/scene_child_lock.svg'; + static const String celsiusDegrees = 'assets/icons/celsius_degrees.svg'; + static const String masterState = 'assets/icons/master_state.svg'; + static const String acPower = 'assets/icons/ac_power.svg'; static const String farDetectionFunction = - "assets/icons/far_detection_function.svg"; - static const String nobodyTime = "assets/icons/nobody_time.svg"; + 'assets/icons/far_detection_function.svg'; + static const String nobodyTime = 'assets/icons/nobody_time.svg'; // Automation functions static const String tempPasswordUnlock = - "assets/icons/automation_functions/temp_password_unlock.svg"; + 'assets/icons/automation_functions/temp_password_unlock.svg'; static const String doorlockNormalOpen = - "assets/icons/automation_functions/doorlock_normal_open.svg"; + 'assets/icons/automation_functions/doorlock_normal_open.svg'; static const String doorbell = - "assets/icons/automation_functions/doorbell.svg"; + 'assets/icons/automation_functions/doorbell.svg'; static const String remoteUnlockViaApp = - "assets/icons/automation_functions/remote_unlock_via_app.svg"; + 'assets/icons/automation_functions/remote_unlock_via_app.svg'; static const String doubleLock = - "assets/icons/automation_functions/double_lock.svg"; + 'assets/icons/automation_functions/double_lock.svg'; static const String selfTestResult = - "assets/icons/automation_functions/self_test_result.svg"; + 'assets/icons/automation_functions/self_test_result.svg'; static const String lockAlarm = - "assets/icons/automation_functions/lock_alarm.svg"; + 'assets/icons/automation_functions/lock_alarm.svg'; static const String presenceState = - "assets/icons/automation_functions/presence_state.svg"; + 'assets/icons/automation_functions/presence_state.svg'; static const String currentTemp = - "assets/icons/automation_functions/current_temp.svg"; + 'assets/icons/automation_functions/current_temp.svg'; static const String presence = - "assets/icons/automation_functions/presence.svg"; + 'assets/icons/automation_functions/presence.svg'; static const String residualElectricity = - "assets/icons/automation_functions/residual_electricity.svg"; + 'assets/icons/automation_functions/residual_electricity.svg'; static const String hijackAlarm = - "assets/icons/automation_functions/hijack_alarm.svg"; + 'assets/icons/automation_functions/hijack_alarm.svg'; static const String passwordUnlock = - "assets/icons/automation_functions/password_unlock.svg"; + 'assets/icons/automation_functions/password_unlock.svg'; static const String remoteUnlockRequest = - "assets/icons/automation_functions/remote_unlock_req.svg"; + 'assets/icons/automation_functions/remote_unlock_req.svg'; static const String cardUnlock = - "assets/icons/automation_functions/card_unlock.svg"; - static const String motion = "assets/icons/automation_functions/motion.svg"; + 'assets/icons/automation_functions/card_unlock.svg'; + static const String motion = 'assets/icons/automation_functions/motion.svg'; static const String fingerprintUnlock = - "assets/icons/automation_functions/fingerprint_unlock.svg"; + 'assets/icons/automation_functions/fingerprint_unlock.svg'; // Presence Sensor Assets - static const String sensorMotionIcon = "assets/icons/sensor_motion_ic.svg"; + static const String sensorMotionIcon = 'assets/icons/sensor_motion_ic.svg'; static const String sensorPresenceIcon = - "assets/icons/sensor_presence_ic.svg"; - static const String sensorVacantIcon = "assets/icons/sensor_vacant_ic.svg"; + 'assets/icons/sensor_presence_ic.svg'; + static const String sensorVacantIcon = 'assets/icons/sensor_vacant_ic.svg'; static const String illuminanceRecordIcon = - "assets/icons/illuminance_record_ic.svg"; + 'assets/icons/illuminance_record_ic.svg'; static const String presenceRecordIcon = - "assets/icons/presence_record_ic.svg"; + 'assets/icons/presence_record_ic.svg'; static const String helpDescriptionIcon = - "assets/icons/help_description_ic.svg"; + 'assets/icons/help_description_ic.svg'; - static const String lightPulp = "assets/icons/light_pulb.svg"; - static const String acDevice = "assets/icons/ac_device.svg"; - static const String acAirConditioner = "assets/icons/ac_air.svg"; - static const String acSun = "assets/icons/ac_sun.svg"; + static const String lightPulp = 'assets/icons/light_pulb.svg'; + static const String acDevice = 'assets/icons/ac_device.svg'; + static const String acAirConditioner = 'assets/icons/ac_air.svg'; + static const String acSun = 'assets/icons/ac_sun.svg'; //assets/icons/3GangSwitch.svg - static const String gangSwitch = "assets/icons/3GangSwitch.svg"; + static const String gangSwitch = 'assets/icons/3GangSwitch.svg'; //assets/icons/AC.svg - static const String ac = "assets/icons/AC.svg"; + static const String ac = 'assets/icons/AC.svg'; //assets/icons/Curtain.svg static const String curtain = 'assets/icons/Curtain.svg'; static const String openCurtain = 'assets/icons/open_curtain.svg'; @@ -130,13 +130,13 @@ class Assets { static const String closeCurtain = 'assets/icons/close_curtain.svg'; static const String reverseArrows = 'assets/icons/reverse_arrows.svg'; //assets/icons/doorLock.svg - static const String doorLock = "assets/icons/doorLock.svg"; + static const String doorLock = 'assets/icons/doorLock.svg'; //assets/icons/Gateway.svg - static const String gateway = "assets/icons/Gateway.svg"; + static const String gateway = 'assets/icons/Gateway.svg'; //assets/icons/Light.svg - static const String lightBulb = "assets/icons/Light.svg"; + static const String lightBulb = 'assets/icons/Light.svg'; //assets/icons/sensors.svg - static const String sensors = "assets/icons/sensors.svg"; + static const String sensors = 'assets/icons/sensors.svg'; //assets/icons/door_un_look_ic.svg static const String doorUnlock = 'assets/icons/door_un_look_ic.svg'; @@ -179,7 +179,7 @@ class Assets { static const String Gang1SwitchIcon = 'assets/icons/1_Gang_switch_icon.svg'; static const String DoorLockIcon = 'assets/icons/door_lock.svg'; static const String SmartGatewayIcon = 'assets/icons/smart_gateway_icon.svg'; - static const String curtainIcon = "assets/images/curtain.svg"; + static const String curtainIcon = 'assets/images/curtain.svg'; static const String unlock = 'assets/icons/unlock_ic.svg'; static const String firmware = 'assets/icons/firmware.svg'; //assets/images/scheduling.svg @@ -231,12 +231,12 @@ class Assets { //assets/icons/2gang.svg static const String twoGang = 'assets/icons/2gang.svg'; - static const String frequencyIcon = "assets/icons/frequency_icon.svg"; - static const String voltMeterIcon = "assets/icons/volt_meter_icon.svg"; - static const String powerActiveIcon = "assets/icons/power_active_icon.svg"; - static const String searchIcon = "assets/icons/search_icon.svg"; - static const String voltageIcon = "assets/icons/voltage_icon.svg"; - static const String speedoMeter = "assets/icons/speedo_meter.svg"; + static const String frequencyIcon = 'assets/icons/frequency_icon.svg'; + static const String voltMeterIcon = 'assets/icons/volt_meter_icon.svg'; + static const String powerActiveIcon = 'assets/icons/power_active_icon.svg'; + static const String searchIcon = 'assets/icons/search_icon.svg'; + static const String voltageIcon = 'assets/icons/voltage_icon.svg'; + static const String speedoMeter = 'assets/icons/speedo_meter.svg'; //assets/icons/account_setting.svg static const String accountSetting = 'assets/icons/account_setting.svg'; @@ -288,99 +288,99 @@ class Assets { // Assets for functions_icons static const String assetsSensitivityFunction = - "assets/icons/functions_icons/sensitivity.svg"; + 'assets/icons/functions_icons/sensitivity.svg'; static const String assetsSensitivityOperationIcon = - "assets/icons/functions_icons/sesitivity_operation_icon.svg"; + 'assets/icons/functions_icons/sesitivity_operation_icon.svg'; static const String assetsAcPower = - "assets/icons/functions_icons/ac_power.svg"; + 'assets/icons/functions_icons/ac_power.svg'; static const String assetsAcPowerOFF = - "assets/icons/functions_icons/ac_power_off.svg"; + 'assets/icons/functions_icons/ac_power_off.svg'; static const String assetsChildLock = - "assets/icons/functions_icons/child_lock.svg"; + 'assets/icons/functions_icons/child_lock.svg'; static const String assetsFreezing = - "assets/icons/functions_icons/freezing.svg"; + 'assets/icons/functions_icons/freezing.svg'; static const String assetsFanSpeed = - "assets/icons/functions_icons/fan_speed.svg"; + 'assets/icons/functions_icons/fan_speed.svg'; static const String assetsAcCooling = - "assets/icons/functions_icons/ac_cooling.svg"; + 'assets/icons/functions_icons/ac_cooling.svg'; static const String assetsAcHeating = - "assets/icons/functions_icons/ac_heating.svg"; + 'assets/icons/functions_icons/ac_heating.svg'; static const String assetsCelsiusDegrees = - "assets/icons/functions_icons/celsius_degrees.svg"; + 'assets/icons/functions_icons/celsius_degrees.svg'; static const String assetsTempreture = - "assets/icons/functions_icons/tempreture.svg"; + 'assets/icons/functions_icons/tempreture.svg'; static const String assetsAcFanLow = - "assets/icons/functions_icons/ac_fan_low.svg"; + 'assets/icons/functions_icons/ac_fan_low.svg'; static const String assetsAcFanMiddle = - "assets/icons/functions_icons/ac_fan_middle.svg"; + 'assets/icons/functions_icons/ac_fan_middle.svg'; static const String assetsAcFanHigh = - "assets/icons/functions_icons/ac_fan_high.svg"; + 'assets/icons/functions_icons/ac_fan_high.svg'; static const String assetsAcFanAuto = - "assets/icons/functions_icons/ac_fan_auto.svg"; + 'assets/icons/functions_icons/ac_fan_auto.svg'; static const String assetsSceneChildLock = - "assets/icons/functions_icons/scene_child_lock.svg"; + 'assets/icons/functions_icons/scene_child_lock.svg'; static const String assetsSceneChildUnlock = - "assets/icons/functions_icons/scene_child_unlock.svg"; + 'assets/icons/functions_icons/scene_child_unlock.svg'; static const String assetsSceneRefresh = - "assets/icons/functions_icons/scene_refresh.svg"; + 'assets/icons/functions_icons/scene_refresh.svg'; static const String assetsLightCountdown = - "assets/icons/functions_icons/light_countdown.svg"; + 'assets/icons/functions_icons/light_countdown.svg'; static const String assetsFarDetection = - "assets/icons/functions_icons/far_detection.svg"; + 'assets/icons/functions_icons/far_detection.svg'; static const String assetsFarDetectionFunction = - "assets/icons/functions_icons/far_detection_function.svg"; + 'assets/icons/functions_icons/far_detection_function.svg'; static const String assetsIndicator = - "assets/icons/functions_icons/indicator.svg"; + 'assets/icons/functions_icons/indicator.svg'; static const String assetsMotionDetection = - "assets/icons/functions_icons/motion_detection.svg"; + 'assets/icons/functions_icons/motion_detection.svg'; static const String assetsMotionlessDetection = - "assets/icons/functions_icons/motionless_detection.svg"; + 'assets/icons/functions_icons/motionless_detection.svg'; static const String assetsNobodyTime = - "assets/icons/functions_icons/nobody_time.svg"; + 'assets/icons/functions_icons/nobody_time.svg'; static const String assetsFactoryReset = - "assets/icons/functions_icons/factory_reset.svg"; + 'assets/icons/functions_icons/factory_reset.svg'; static const String assetsMasterState = - "assets/icons/functions_icons/master_state.svg"; + 'assets/icons/functions_icons/master_state.svg'; static const String assetsSwitchAlarmSound = - "assets/icons/functions_icons/switch_alarm_sound.svg"; + 'assets/icons/functions_icons/switch_alarm_sound.svg'; static const String assetsResetOff = - "assets/icons/functions_icons/reset_off.svg"; + 'assets/icons/functions_icons/reset_off.svg'; // Assets for automation_functions static const String assetsCardUnlock = - "assets/icons/functions_icons/automation_functions/card_unlock.svg"; + 'assets/icons/functions_icons/automation_functions/card_unlock.svg'; static const String assetsDoorbell = - "assets/icons/functions_icons/automation_functions/doorbell.svg"; + 'assets/icons/functions_icons/automation_functions/doorbell.svg'; static const String assetsDoorlockNormalOpen = - "assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg"; + 'assets/icons/functions_icons/automation_functions/doorlock_normal_open.svg'; static const String assetsDoubleLock = - "assets/icons/functions_icons/automation_functions/double_lock.svg"; + 'assets/icons/functions_icons/automation_functions/double_lock.svg'; static const String assetsFingerprintUnlock = - "assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg"; + 'assets/icons/functions_icons/automation_functions/fingerprint_unlock.svg'; static const String assetsHijackAlarm = - "assets/icons/functions_icons/automation_functions/hijack_alarm.svg"; + 'assets/icons/functions_icons/automation_functions/hijack_alarm.svg'; static const String assetsLockAlarm = - "assets/icons/functions_icons/automation_functions/lock_alarm.svg"; + 'assets/icons/functions_icons/automation_functions/lock_alarm.svg'; static const String assetsPasswordUnlock = - "assets/icons/functions_icons/automation_functions/password_unlock.svg"; + 'assets/icons/functions_icons/automation_functions/password_unlock.svg'; static const String assetsRemoteUnlockReq = - "assets/icons/functions_icons/automation_functions/remote_unlock_req.svg"; + 'assets/icons/functions_icons/automation_functions/remote_unlock_req.svg'; static const String assetsRemoteUnlockViaApp = - "assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg"; + 'assets/icons/functions_icons/automation_functions/remote_unlock_via_app.svg'; static const String assetsResidualElectricity = - "assets/icons/functions_icons/automation_functions/residual_electricity.svg"; + 'assets/icons/functions_icons/automation_functions/residual_electricity.svg'; static const String assetsTempPasswordUnlock = - "assets/icons/functions_icons/automation_functions/temp_password_unlock.svg"; + 'assets/icons/functions_icons/automation_functions/temp_password_unlock.svg'; static const String assetsSelfTestResult = - "assets/icons/functions_icons/automation_functions/self_test_result.svg"; + 'assets/icons/functions_icons/automation_functions/self_test_result.svg'; static const String assetsPresence = - "assets/icons/functions_icons/automation_functions/presence.svg"; + 'assets/icons/functions_icons/automation_functions/presence.svg'; static const String assetsMotion = - "assets/icons/functions_icons/automation_functions/motion.svg"; + 'assets/icons/functions_icons/automation_functions/motion.svg'; static const String assetsCurrentTemp = - "assets/icons/functions_icons/automation_functions/current_temp.svg"; + 'assets/icons/functions_icons/automation_functions/current_temp.svg'; static const String assetsPresenceState = - "assets/icons/functions_icons/automation_functions/presence_state.svg"; + 'assets/icons/functions_icons/automation_functions/presence_state.svg'; //assets/icons/routine/automation.svg static const String automation = 'assets/icons/routine/automation.svg'; static const String searchIconUser = 'assets/icons/search_icon_user.svg'; @@ -505,4 +505,5 @@ class Assets { static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; + static const String autocadOccupancyImage = 'assets/images/autocad_occupancy_image.png'; }