diff --git a/assets/icons/close_curtain.svg b/assets/icons/close_curtain.svg new file mode 100644 index 00000000..53f9e03b --- /dev/null +++ b/assets/icons/close_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/open_curtain.svg b/assets/icons/open_curtain.svg new file mode 100644 index 00000000..715773a5 --- /dev/null +++ b/assets/icons/open_curtain.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/pause_curtain.svg b/assets/icons/pause_curtain.svg new file mode 100644 index 00000000..8f90ea4f --- /dev/null +++ b/assets/icons/pause_curtain.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/reverse_arrows.svg b/assets/icons/reverse_arrows.svg new file mode 100644 index 00000000..fe119c39 --- /dev/null +++ b/assets/icons/reverse_arrows.svg @@ -0,0 +1,10 @@ + + + + + + + + + + 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..4c7a37d4 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()}', + eventDate: DateTime.utc( + int.parse(year ?? '2025'), + int.parse(month ?? '1'), + int.parse(day ?? '1'), ), - countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + countTotalPresenceDetected: num.parse( + json['count_total_presence_detected']?.toString() ?? '0', + ).toInt(), ); } 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 f23abd7b..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,7 +21,6 @@ abstract final class FetchAirQualityDataHelper { required String spaceUuid, bool shouldFetchAnalyticsDevices = true, }) { - final date = context.read().state.monthlyDate; final aqiType = context.read().state.selectedAqiType; if (shouldFetchAnalyticsDevices) { loadAnalyticsDevices( 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 e4aa5b6f..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,7 +18,11 @@ 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( @@ -39,11 +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/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 e35a05e7..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'; @@ -149,6 +150,7 @@ class AqiDistributionChart extends StatelessWidget { ); final bottomTitles = AxisTitles( + axisNameWidget: const ChartsXAxisTitle(), sideTitles: SideTitles( showTitles: chartData.isNotEmpty, getTitlesWidget: (value, _) => FittedBox( 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/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/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.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index 85b95c29..bba1fa14 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + maxY: chartData.isEmpty + ? null + : chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250, clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, - leftTitlesInterval: 250, + leftTitlesInterval: 500, ), gridData: EnergyManagementChartsHelper.gridData().copyWith( checkToShowHorizontalLine: (value) => true, - horizontalInterval: 250, + horizontalInterval: 500, ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(), @@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget { ), duration: Duration.zero, curve: Curves.easeIn, - ), ); } 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 3a025254..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()), ], 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 3dd01bee..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,10 +23,9 @@ class OccupancyEndSideBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const AnalyticsSidebarHeader(title: 'Presnce Sensor'), + const AnalyticsSidebarHeader(title: 'Presence Sensor'), Expanded( child: SizedBox( - // height: MediaQuery.sizeOf(context).height * 0.2, child: PowerClampEnergyStatusWidget( status: [ PowerClampEnergyStatus( 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..482f0029 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_ import 'package:syncrow_web/utils/color_manager.dart'; class OccupancyHeatMap extends StatelessWidget { - const OccupancyHeatMap({required this.heatMapData, super.key}); + const OccupancyHeatMap({ + required this.heatMapData, + required this.selectedDate, + super.key, + }); final Map heatMapData; + final DateTime selectedDate; static const _cellSize = 16.0; static const _totalWeeks = 53; @@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget { : 0; DateTime _getStartingDate() { - final jan1 = DateTime(DateTime.now().year, 1, 1); + final jan1 = DateTime.utc(selectedDate.year, 1, 1); final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); return startOfWeek; } diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart index c3b537e0..a5f56aa4 100644 --- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget { const SizedBox(height: 20), Expanded( child: OccupancyHeatMap( + selectedDate: + context.watch().state.yearlyDate, heatMapData: state.heatMapData.asMap().map( (_, value) => MapEntry( value.eventDate, 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 625c59c2..fb8237b7 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -179,31 +179,36 @@ 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 _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( diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index af5a7b0a..85e119f1 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -68,24 +68,30 @@ 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 = - event.snapshot.value as Map; + final usersMap = event.snapshot.value! as Map; - List statusList = []; + final statusList = []; usersMap['status'].forEach((element) { statusList.add(Status(code: element['code'], value: element['value'])); }); + + deviceStatus = + AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); + print('Device status updated: ${deviceStatus.acSwitch}'); + + if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } @@ -105,22 +111,14 @@ class AcBloc extends Bloc { AcControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); - _updateDeviceFunctionFromCode(event.code, event.value); - emit(ACStatusLoaded(status: deviceStatus)); - try { - final success = await controlDeviceService.controlDevice( + _updateDeviceFunctionFromCode(event.code, event.value); + emit(ACStatusLoaded(status: deviceStatus)); + await controlDeviceService.controlDevice( deviceUuid: event.deviceId, status: Status(code: event.code, value: event.value), ); - - if (!success) { - emit(const AcsFailedState(error: 'Failed to control device')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } FutureOr _onFetchAcBatchStatus( @@ -141,23 +139,16 @@ class AcBloc extends Bloc { AcBatchControlEvent event, Emitter emit, ) async { - emit(AcsLoadingState()); _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); try { - final success = await batchControlDevicesService.batchControlDevices( + await batchControlDevicesService.batchControlDevices( uuids: event.devicesIds, code: event.code, value: event.value, ); - - if (!success) { - emit(const AcsFailedState(error: 'Failed to control devices')); - } - } catch (e) { - emit(AcsFailedState(error: e.toString())); - } + } catch (e) {} } Future _onFactoryReset( @@ -190,8 +181,8 @@ class AcBloc extends Bloc { void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int newHours = scheduledHours; - int newMinutes = scheduledMinutes + 30; + var newHours = scheduledHours; + var newMinutes = scheduledMinutes + 30; newHours += newMinutes ~/ 60; newMinutes = newMinutes % 60; if (newHours > 23) { @@ -213,7 +204,7 @@ class AcBloc extends Bloc { ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; - int totalMinutes = (scheduledHours * 60) + scheduledMinutes; + var totalMinutes = (scheduledHours * 60) + scheduledMinutes; totalMinutes = (totalMinutes - 30).clamp(0, 1440); scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -286,20 +277,24 @@ class AcBloc extends Bloc { void _startCountdownTimer(Emitter emit) { _countdownTimer?.cancel(); - int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); + var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (totalSeconds > 0) { totalSeconds--; scheduledHours = totalSeconds ~/ 3600; scheduledMinutes = (totalSeconds % 3600) ~/ 60; - add(UpdateTimerEvent()); + if (!isClosed) { + add(UpdateTimerEvent()); + } } else { _countdownTimer?.cancel(); timerActive = false; scheduledHours = 0; scheduledMinutes = 0; - add(TimerCompletedEvent()); + if (!isClosed) { + add(TimerCompletedEvent()); + } } }); } @@ -326,7 +321,9 @@ class AcBloc extends Bloc { _startCountdownTimer( emit, ); - add(UpdateTimerEvent()); + if (!isClosed) { + add(UpdateTimerEvent()); + } } } @@ -370,6 +367,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 4c3e5b39..b7f04a58 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 @@ -44,18 +44,14 @@ class DeviceManagementBloc _devices.clear(); var spaceBloc = event.context.read(); 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(projectUuid, spacesId: spacesList)); } } @@ -273,6 +269,7 @@ class DeviceManagementBloc return 'All'; } } + void _onSearchDevices( SearchDevices event, Emitter emit) { if ((event.community == null || event.community!.isEmpty) && diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index 5586a310..08bca73c 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart'; import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart'; @@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart'; import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart'; @@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart'; import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart'; -import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart'; - mixin RouteControlsBasedCode { Widget routeControlsWidgets({required AllDevicesModel device}) { switch (device.productType) { @@ -84,6 +85,10 @@ mixin RouteControlsBasedCode { return CurtainStatusControlsView( deviceId: device.uuid!, ); + case 'CUR_2': + return CurtainModuleItems( + deviceId: device.uuid!, + ); case 'AC': return AcDeviceControlsView(device: device); case 'WH': @@ -107,7 +112,7 @@ mixin RouteControlsBasedCode { case 'SOS': return SosDeviceControlsView(device: device); - case 'NCPS': + case 'NCPS': return FlushMountedPresenceSensorControlView(device: device); default: return const SizedBox(); @@ -132,76 +137,140 @@ mixin RouteControlsBasedCode { switch (devices.first.productType) { case '1G': return WallLightBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1G') + .map((e) => e.uuid!) + .toList(), ); case '2G': return TwoGangBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2G') + .map((e) => e.uuid!) + .toList(), ); case '3G': return LivingRoomBatchControlsView( - deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3G') + .map((e) => e.uuid!) + .toList(), ); case '1GT': return OneGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '1GT') + .map((e) => e.uuid!) + .toList(), ); case '2GT': return TwoGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '2GT') + .map((e) => e.uuid!) + .toList(), ); case '3GT': return ThreeGangGlassSwitchBatchControlView( - deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == '3GT') + .map((e) => e.uuid!) + .toList(), ); case 'GW': return GatewayBatchControlView( - gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(), + gatewayIds: devices + .where((e) => e.productType == 'GW') + .map((e) => e.uuid!) + .toList(), ); case 'DL': return DoorLockBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'DL') + .map((e) => e.uuid!) + .toList()); case 'WPS': return WallSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'WPS') + .map((e) => e.uuid!) + .toList()); case 'CPS': return CeilingSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CPS') + .map((e) => e.uuid!) + .toList(), ); case 'CUR': return CurtainBatchStatusView( - devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'CUR') + .map((e) => e.uuid!) + .toList(), + ); + case 'CUR_2': + return CurtainModuleBatchView( + devicesIds: devices + .where((e) => e.productType == 'CUR_2') + .map((e) => e.uuid!) + .toList(), ); case 'AC': return AcDeviceBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList()); + devicesIds: devices + .where((e) => e.productType == 'AC') + .map((e) => e.uuid!) + .toList()); case 'WH': return WaterHEaterBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WH') + .map((e) => e.uuid!) + .toList(), ); case 'DS': return MainDoorSensorBatchView( - devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'DS') + .map((e) => e.uuid!) + .toList(), ); case 'GD': return GarageDoorBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'GD') + .map((e) => e.uuid!) + .toList(), ); case 'WL': return WaterLeakBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'WL') + .map((e) => e.uuid!) + .toList(), ); case 'PC': return PowerClampBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'PC') + .map((e) => e.uuid!) + .toList(), ); case 'SOS': return SOSBatchControlView( - deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(), + deviceIds: devices + .where((e) => e.productType == 'SOS') + .map((e) => e.uuid!) + .toList(), ); case 'NCPS': return FlushMountedPresenceSensorBatchControlView( - devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(), + devicesIds: devices + .where((e) => e.productType == 'NCPS') + .map((e) => e.uuid!) + .toList(), ); default: return const SizedBox(); 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 8d108671..b4eb60e6 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 @@ -61,7 +61,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( @@ -102,8 +103,28 @@ 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) + .clearSnackBars(); + 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/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart new file mode 100644 index 00000000..b40d7ea6 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart @@ -0,0 +1,379 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; + +part 'curtain_module_event.dart'; +part 'curtain_module_state.dart'; + +class CurtainModuleBloc extends Bloc { + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + StreamSubscription? _firebaseSubscription; + + CurtainModuleBloc({ + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainModuleInitial()) { + on(_onFetchCurtainModuleStatusEvent); + on(_onSendCurtainPercentToApiEvent); + on(_onOpenCurtainEvent); + on(_onCloseCurtainEvent); + on(_onStopCurtainEvent); + on(_onChangeTimerControlEvent); + on(_onChageCurCalibrationEvent); + on(_onChangeElecMachineryModeEvent); + on(_onChangeControlBackEvent); + on(_onChangeControlBackModeEvent); + on(_onChangeCurtainModuleStatusEvent); + //batch + on(_onFetchCurtainModuleBatchStatus); + on(_onSendCurtainBatchPercentToApiEvent); + on(_onOpenCurtainBatchEvent); + on(_onCloseCurtainBatchEvent); + on(_onStopCurtainBatchEvent); + on(_onFactoryReset); + } + + Future _onFetchCurtainModuleStatusEvent( + FetchCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + Map statusMap = {}; + final ref = + FirebaseDatabase.instance.ref('device-status/${event.deviceId}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.deviceId, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); + } + + Future _onChangeCurtainModuleStatusEvent( + ChangeCurtainModuleStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status)); + } + + Future _onSendCurtainPercentToApiEvent( + SendCurtainPercentToApiEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: event.status, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainEvent( + OpenCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'open'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainEvent( + CloseCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'close'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainEvent( + StopCurtainEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'control', value: 'stop'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onChangeTimerControlEvent( + ChangeTimerControlEvent event, + Emitter emit, + ) async { + try { + if (event.timControl < 10 || event.timControl > 120) { + emit(const CurtainModuleError( + message: 'Timer control value must be between 10 and 120')); + return; + } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'tr_timecon', + value: event.timControl, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change timer control: $e')); + } + } + + Future _onChageCurCalibrationEvent( + CurCalibrationEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: 'cur_calibration', value: 'start'), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to start calibration: $e')); + } + } + + Future _onChangeElecMachineryModeEvent( + ChangeElecMachineryModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'elec_machinery_mode', + value: event.elecMachineryMode, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change mode: $e')); + } + } + + Future _onChangeControlBackEvent( + ChangeControlBackEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back', + value: event.controlBack, + ), + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to change control back: $e')); + } + } + + Future _onChangeControlBackModeEvent( + ChangeControlBackModeEvent event, + Emitter emit, + ) async { + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: 'control_back_mode', + value: event.controlBackMode, + ), + ); + } catch (e) { + emit(CurtainModuleError( + message: 'Failed to change control back mode: $e')); + } + } + + FutureOr _onFetchCurtainModuleBatchStatus( + CurtainModuleFetchBatchStatusEvent event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesIds); + + final result = Map.fromEntries( + status.status.map((element) => MapEntry(element.code, element.value)), + ); + + emit(CurtainModuleStatusLoaded( + curtainModuleStatus: CurtainModuleStatusModel.fromJson(result), + )); + + Map statusMap = {}; + final ref = FirebaseDatabase.instance + .ref('device-status/${event.devicesIds.first}'); + final stream = ref.onValue; + + stream.listen((DatabaseEvent DatabaseEvent) async { + if (DatabaseEvent.snapshot.value == null) return; + + Map usersMap = + DatabaseEvent.snapshot.value as Map; + + List statusList = []; + + usersMap['status'].forEach((element) { + statusList + .add(Status(code: element['code'], value: element['value'])); + }); + + statusMap = { + for (final element in statusList) element.code: element.value, + }; + if (!isClosed) { + add( + ChangeCurtainModuleStatusEvent( + deviceId: event.devicesIds.first, + status: CurtainModuleStatusModel.fromJson(statusMap), + ), + ); + } + }); + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + + Future _onSendCurtainBatchPercentToApiEvent( + SendCurtainBatchPercentToApiEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: event.status.code, + value: event.status.value, + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to send control command: $e')); + } + } + + Future _onOpenCurtainBatchEvent( + OpenCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'open', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to open curtain: $e')); + } + } + + Future _onCloseCurtainBatchEvent( + CloseCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'close', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to close curtain: $e')); + } + } + + Future _onStopCurtainBatchEvent( + StopCurtainBatchEvent event, + Emitter emit, + ) async { + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesId, + code: 'control', + value: 'stop', + ); + } catch (e) { + emit(CurtainModuleError(message: 'Failed to stop curtain: $e')); + } + } + + Future _onFactoryReset( + CurtainModuleFactoryReset event, + Emitter emit, + ) async { + emit(CurtainModuleLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const CurtainModuleError(message: 'Failed')); + } else { + add( + FetchCurtainModuleStatusEvent(deviceId: event.deviceId), + ); + } + } catch (e) { + emit(CurtainModuleError(message: e.toString())); + } + } + + @override + Future close() async { + await _firebaseSubscription?.cancel(); + return super.close(); + } +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart new file mode 100644 index 00000000..4eec030d --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart @@ -0,0 +1,193 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleEvent extends Equatable { + const CurtainModuleEvent(); + + @override + List get props => []; +} + +class FetchCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + const FetchCurtainModuleStatusEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class SendCurtainPercentToApiEvent extends CurtainModuleEvent { + final String deviceId; + final Status status; + + const SendCurtainPercentToApiEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} + +class OpenCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const OpenCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class CloseCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const CloseCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class StopCurtainEvent extends CurtainModuleEvent { + final String deviceId; + + const StopCurtainEvent({required this.deviceId}); + + @override + List get props => [deviceId]; +} + +class ChangeTimerControlEvent extends CurtainModuleEvent { + final String deviceId; + final int timControl; + + const ChangeTimerControlEvent({ + required this.deviceId, + required this.timControl, + }); + + @override + List get props => [deviceId, timControl]; +} + +class CurCalibrationEvent extends CurtainModuleEvent { + final String deviceId; + + const CurCalibrationEvent({ + required this.deviceId, + }); + + @override + List get props => [deviceId]; +} + +class ChangeElecMachineryModeEvent extends CurtainModuleEvent { + final String deviceId; + final String elecMachineryMode; + + const ChangeElecMachineryModeEvent({ + required this.deviceId, + required this.elecMachineryMode, + }); + + @override + List get props => [deviceId, elecMachineryMode]; +} + +class ChangeControlBackEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBack; + + const ChangeControlBackEvent({ + required this.deviceId, + required this.controlBack, + }); + + @override + List get props => [deviceId, controlBack]; +} + +class ChangeControlBackModeEvent extends CurtainModuleEvent { + final String deviceId; + final String controlBackMode; + + const ChangeControlBackModeEvent({ + required this.deviceId, + required this.controlBackMode, + }); + + @override + List get props => [deviceId, controlBackMode]; +} + +class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent { + final String deviceId; + final CurtainModuleStatusModel status; + + const ChangeCurtainModuleStatusEvent({ + required this.deviceId, + required this.status, + }); + + @override + List get props => [deviceId, status]; +} + +///batch +class CurtainModuleFetchBatchStatusEvent extends CurtainModuleEvent { + final List devicesIds; + + const CurtainModuleFetchBatchStatusEvent(this.devicesIds); + + @override + List get props => [devicesIds]; +} + +class SendCurtainBatchPercentToApiEvent extends CurtainModuleEvent { + final List devicesId; + final Status status; + + const SendCurtainBatchPercentToApiEvent({ + required this.devicesId, + required this.status, + }); + + @override + List get props => [devicesId, status]; +} + +class OpenCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const OpenCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CloseCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const CloseCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class StopCurtainBatchEvent extends CurtainModuleEvent { + final List devicesId; + + const StopCurtainBatchEvent({required this.devicesId}); + + @override + List get props => [devicesId]; +} + +class CurtainModuleFactoryReset extends CurtainModuleEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const CurtainModuleFactoryReset( + {required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart new file mode 100644 index 00000000..02ef9279 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_state.dart @@ -0,0 +1,37 @@ +part of 'curtain_module_bloc.dart'; + +sealed class CurtainModuleState extends Equatable { + const CurtainModuleState(); + + @override + List get props => []; +} + +class CurtainModuleInitial extends CurtainModuleState {} + +class CurtainModuleLoading extends CurtainModuleState {} + +class CurtainModuleError extends CurtainModuleState { + final String message; + const CurtainModuleError({required this.message}); + + @override + List get props => [message]; +} + +class CurtainModuleStatusLoaded extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusLoaded({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} +class CurtainModuleStatusUpdated extends CurtainModuleState { + final CurtainModuleStatusModel curtainModuleStatus; + + const CurtainModuleStatusUpdated({required this.curtainModuleStatus}); + + @override + List get props => [curtainModuleStatus]; +} diff --git a/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart new file mode 100644 index 00000000..0b6d23fb --- /dev/null +++ b/lib/pages/device_managment/curtain_module/models/curtain_module_model.dart @@ -0,0 +1,53 @@ + +enum CurtainModuleControl { + open, + close, + stop, +} + +// enum CurtainControlBackMode { +// foward, +// backward, +// } + +class CurtainModuleStatusModel { + CurtainModuleControl control; + int percentControl; + String curCalibration; + // CurtainControlBackMode controlBackmode; + int trTimeControl; + String elecMachineryMode; + String controlBack; + CurtainModuleStatusModel({ + required this.control, + required this.percentControl, + required this.curCalibration, + // required this.controlBackmode, + required this.trTimeControl, + required this.controlBack, + required this.elecMachineryMode, + }); + factory CurtainModuleStatusModel.zero() => CurtainModuleStatusModel( + control: CurtainModuleControl.stop, + percentControl: 0, + // controlBackmode: CurtainControlBackMode.foward, + curCalibration: '', + trTimeControl: 0, + controlBack: '', + elecMachineryMode: '', + ); + + factory CurtainModuleStatusModel.fromJson(Map json) { + return CurtainModuleStatusModel( + control: CurtainModuleControl.values.firstWhere( + (e) => e.toString() == json['control'] as String, + orElse: () => CurtainModuleControl.stop, + ), + percentControl: json['percent_control'] as int? ?? 0, + curCalibration: json['cur_calibration'] as String? ?? '', + trTimeControl: json['tr_timecon'] as int? ?? 0, + elecMachineryMode: json['elec_machinery_mode'] as String? ?? '', + controlBack: json['control_back'] as String? ?? '', + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart new file mode 100644 index 00000000..bd28cd8a --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_batch.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class CurtainModuleBatchView extends StatelessWidget { + final List devicesIds; + const CurtainModuleBatchView({ + super.key, + required this.devicesIds, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) + ..add(CurtainModuleFetchBatchStatusEvent(devicesIds)), + child: _buildStatusControls(context), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + devicesId: devicesIds, + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 120, + // width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Expanded( + // child: + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CurtainModuleFactoryReset( + deviceId: devicesIds.first, + factoryReset: + FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), + // ), + // Expanded( + // child: IconNameStatusContainer( + // isFullIcon: false, + // name: 'Firmware Update', + // icon: Assets.firmware, + // onTap: () {}, + // status: false, + // textColor: ColorsManager.blackColor, + // ), + // ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart new file mode 100644 index 00000000..82c812ce --- /dev/null +++ b/lib/pages/device_managment/curtain_module/view/curtain_module_items.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart'; +import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart'; +import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout { + final String deviceId; + const CurtainModuleItems({ + super.key, + required this.deviceId, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => CurtainModuleBloc( + controlDeviceService: RemoteControlDeviceService(), + batchControlDevicesService: RemoteBatchControlDevicesService()) + ..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)), + child: BlocBuilder( + builder: (context, state) { + return _buildStatusControls(context); + }, + ), + ); + } + + Widget _buildStatusControls(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ControlCurtainMovementWidget( + devicesId: [deviceId], + ), + const SizedBox( + height: 10, + ), + SizedBox( + height: 140, + width: 350, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ScheduleControlButton( + onTap: () { + showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: + BlocProvider.of(context), + child: BuildScheduleView( + deviceUuid: deviceId, + category: 'CUR_2', + code: 'control', + + ), + )); + }, + mainText: '', + subtitle: 'Scheduling', + iconPath: Assets.scheduling, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return IconNameStatusContainer( + isFullIcon: false, + name: 'Preferences', + icon: Assets.preferences, + onTap: () => showDialog( + context: context, + builder: (_) => BlocProvider.value( + value: context.read(), + child: CurtainModulePrefrencesDialog( + curtainModuleBloc: + context.watch(), + deviceId: deviceId, + curtainModuleStatusModel: + state.curtainModuleStatus, + ), + ), + ), + status: false, + textColor: ColorsManager.blackColor, + ); + } else { + return const SizedBox(); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart new file mode 100644 index 00000000..54107420 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurteCalibratingDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurteCalibratingDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: const NormalTextBodyForDialog( + title: '', + step1: + '1. Click Close Button to make the Curtain run to Full Close and Position.', + step2: '2. click Next to complete the Calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + parentContext.read().add( + CurCalibrationEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: parentContext, + deviceId: deviceId, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart new file mode 100644 index 00000000..a9d1b010 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; + +class AccurateCalibrationDialog extends StatelessWidget { + final String deviceId; + final BuildContext parentContext; + const AccurateCalibrationDialog({ + super.key, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Accurate Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: '1. Run The Curtain to the Fully Open Position,and pause.', + step2: '2. click Next to Start accurate calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => AccurteCalibratingDialog( + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart new file mode 100644 index 00000000..5be376ae --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AccurateDialogWidget extends StatelessWidget { + final String title; + final Widget body; + final void Function() leftOnTap; + final void Function() rightOnTap; + const AccurateDialogWidget({ + super.key, + required this.title, + required this.body, + required this.leftOnTap, + required this.rightOnTap, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 300, + width: 400, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + Padding( + padding: const EdgeInsets.all(10), + child: body, + ), + const SizedBox(height: 20), + const Spacer(), + const Divider(), + Row( + children: [ + Expanded( + child: InkWell( + onTap: leftOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Cancel', + style: TextStyle(color: ColorsManager.grayBorder), + ), + ), + ), + ), + Expanded( + child: InkWell( + onTap: rightOnTap, + child: Container( + height: 60, + alignment: Alignment.center, + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.grayBorder, + ), + ), + ), + child: const Text( + 'Next', + style: TextStyle( + color: ColorsManager.blueColor, + ), + ), + ), + ), + ) + ], + ) + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart new file mode 100644 index 00000000..9b2b5ea9 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CalibrateCompletedDialog extends StatelessWidget { + final BuildContext parentContext; + final String deviceId; + const CalibrateCompletedDialog({ + super.key, + required this.parentContext, + required this.deviceId, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: SizedBox( + height: 250, + width: 400, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(10), + child: Text( + 'Calibration Completed', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: ColorsManager.blueColor, + ), + ), + ), + const SizedBox(height: 5), + const Divider( + indent: 10, + endIndent: 10, + ), + const Icon( + Icons.check_circle, + size: 100, + color: ColorsManager.blueColor, + ), + const Spacer(), + const Divider( + indent: 10, + endIndent: 10, + ), + InkWell( + onTap: () { + parentContext.read().add( + FetchCurtainModuleStatusEvent( + deviceId: deviceId, + ), + ); + Navigator.of(parentContext).pop(); + Navigator.of(parentContext).pop(); + }, + child: Container( + height: 40, + width: double.infinity, + alignment: Alignment.center, + child: const Text( + 'Close', + style: TextStyle( + color: ColorsManager.grayBorder, + ), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart new file mode 100644 index 00000000..8c2ff81c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class CurtainActionWidget extends StatelessWidget { + final String icon; + final void Function() onTap; + const CurtainActionWidget({ + super.key, + required this.icon, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.whiteColors, + child: ClipOval( + child: Container( + height: 60, + width: 60, + padding: const EdgeInsets.all(8), + color: ColorsManager.graysColor, + child: SvgPicture.asset( + icon, + width: 35, + height: 35, + fit: BoxFit.contain, + ), + ), + ), + )), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart new file mode 100644 index 00000000..e98ff11d --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart'; +import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +class ControlCurtainMovementWidget extends StatelessWidget { + final List devicesId; + const ControlCurtainMovementWidget({ + super.key, + required this.devicesId, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 550, + child: DeviceControlsContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CurtainActionWidget( + icon: Assets.openCurtain, + onTap: () { + if (devicesId.length == 1) { + context.read().add( + OpenCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + OpenCurtainBatchEvent(devicesId: devicesId), + ); + } + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.pauseCurtain, + onTap: () { + if (devicesId.length == 1) { + context.read().add( + StopCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + StopCurtainBatchEvent(devicesId: devicesId), + ); + } + }, + ), + const SizedBox( + width: 30, + ), + CurtainActionWidget( + icon: Assets.closeCurtain, + onTap: () { + if (devicesId.length == 1) { + context.read().add( + CloseCurtainEvent(deviceId: devicesId.first), + ); + } else { + context.read().add( + CloseCurtainBatchEvent(devicesId: devicesId), + ); + } + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is CurtainModuleError) { + return Center( + child: Text( + state.message, + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator( + color: ColorsManager.minBlueDot, + ), + ); + } else if (state is CurtainModuleInitial) { + return const Center( + child: Text( + 'No data available', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } else if (state is CurtainModuleStatusLoaded) { + return CurtainSliderWidget( + status: state.curtainModuleStatus, + devicesId: devicesId, + ); + } else { + return const Center( + child: Text( + 'Unknown state', + style: TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 16, + ), + ), + ); + } + }, + ) + ], + ), + ), + ); + } +} + +class CurtainSliderWidget extends StatefulWidget { + final CurtainModuleStatusModel status; + final List devicesId; + + const CurtainSliderWidget({ + super.key, + required this.status, + required this.devicesId, + }); + + @override + State createState() => _CurtainSliderWidgetState(); +} + +class _CurtainSliderWidgetState extends State { + double? _localValue; // For temporary drag state + + @override + Widget build(BuildContext context) { + // If user is dragging, use local value. Otherwise, use Firebase-synced state + final double currentSliderValue = + _localValue ?? widget.status.percentControl / 100; + + return Column( + children: [ + Text( + '${(currentSliderValue * 100).round()}%', + style: const TextStyle( + color: ColorsManager.minBlueDot, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + Slider( + value: currentSliderValue, + min: 0, + max: 1, + divisions: 10, // 10% step + activeColor: ColorsManager.minBlueDot, + thumbColor: ColorsManager.primaryColor, + inactiveColor: ColorsManager.whiteColors, + + // Start dragging — use local control + onChangeStart: (_) { + setState(() { + _localValue = currentSliderValue; + }); + }, + + // While dragging — update temporary value + onChanged: (value) { + final steppedValue = (value * 10).roundToDouble() / 10; + setState(() { + _localValue = steppedValue; + }); + }, + + // On release — send API and return to Firebase-controlled state + onChangeEnd: (value) { + final int targetPercent = (value * 100).round(); + + if (widget.devicesId.length == 1) { + context.read().add( + SendCurtainPercentToApiEvent( + deviceId: widget.devicesId.first, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + } else { + context.read().add( + SendCurtainBatchPercentToApiEvent( + devicesId: widget.devicesId, + status: Status( + code: 'percent_control', + value: targetPercent, + ), + ), + ); + } + + // Revert back to Firebase-synced stream + setState(() { + _localValue = null; + }); + }, + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart new file mode 100644 index 00000000..8818cb7b --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NormalTextBodyForDialog extends StatelessWidget { + final String title; + final String step1; + final String step2; + + const NormalTextBodyForDialog({ + super.key, + required this.title, + required this.step1, + required this.step2, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step1, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ), + Text( + step2, + style: const TextStyle( + color: ColorsManager.grayColor, + ), + ) + ], + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart new file mode 100644 index 00000000..ea95f838 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/number_input_textfield.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class NumberInputField extends StatelessWidget { + final TextEditingController controller; + + const NumberInputField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: const TextStyle( + fontSize: 20, + color: ColorsManager.blackColor, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart new file mode 100644 index 00000000..81912e80 --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class PrefReversCardWidget extends StatelessWidget { + final void Function() onTap; + final String title; + final String body; + const PrefReversCardWidget({ + super.key, + required this.title, + required this.body, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return DefaultContainer( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 8, + child: Text( + title, + style: const TextStyle( + color: ColorsManager.grayBorder, + fontSize: 15, + ), + ), + ), + const SizedBox( + width: 20, + ), + Expanded( + flex: 2, + child: InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: const BorderRadius.horizontal( + left: Radius.circular(10), + right: Radius.circular(10)), + border: Border.all(color: ColorsManager.grayBorder)), + child: SvgPicture.asset( + Assets.reverseArrows, + height: 15, + ), + ), + ), + ) + ], + ), + SizedBox( + width: 100, + child: Text( + body, + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w500, + fontSize: 18, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart new file mode 100644 index 00000000..1e4f932c --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class CurtainModulePrefrencesDialog extends StatelessWidget { + final CurtainModuleStatusModel curtainModuleStatusModel; + final String deviceId; + final CurtainModuleBloc curtainModuleBloc; + const CurtainModulePrefrencesDialog({ + super.key, + required this.curtainModuleStatusModel, + required this.deviceId, + required this.curtainModuleBloc, + }); + + @override + Widget build(_) { + return AlertDialog( + backgroundColor: ColorsManager.CircleImageBackground, + contentPadding: const EdgeInsets.all(30), + title: const Center( + child: Text( + 'Preferences', + style: TextStyle( + color: ColorsManager.blueColor, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + )), + content: BlocBuilder( + bloc: curtainModuleBloc, + builder: (context, state) { + if (state is CurtainModuleLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (state is CurtainModuleStatusLoaded) { + return SizedBox( + height: 300, + width: 400, + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.5, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + children: [ + PrefReversCardWidget( + title: state.curtainModuleStatus.controlBack, + body: 'Motor Steering', + onTap: () { + context.read().add( + ChangeControlBackEvent( + deviceId: deviceId, + controlBack: + state.curtainModuleStatus.controlBack == + 'forward' + ? 'back' + : 'forward', + ), + ); + }, + ), + PrefReversCardWidget( + title: formatDeviceType( + state.curtainModuleStatus.elecMachineryMode), + body: 'Motor Mode', + onTap: () => context.read().add( + ChangeElecMachineryModeEvent( + deviceId: deviceId, + elecMachineryMode: + state.curtainModuleStatus.elecMachineryMode == + 'dry_contact' + ? 'strong_power' + : 'dry_contact', + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => AccurateCalibrationDialog( + deviceId: deviceId, + parentContext: context, + ), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Accurte Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + DefaultContainer( + padding: const EdgeInsets.all(12), + child: InkWell( + onTap: () => showDialog( + context: context, + builder: (_) => QuickCalibrationDialog( + timControl: state.curtainModuleStatus.trTimeControl, + deviceId: deviceId, + parentContext: context), + ), + child: const Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('Quick Calibration', + style: TextStyle( + fontSize: 18, + color: ColorsManager.blackColor, + )), + ], + ), + ), + ), + ], + ), + ); + } else { + return const SizedBox(); + } + }, + ), + ); + } + + String formatDeviceType(String raw) { + return raw + .split('_') + .map((word) => word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1)}' + : '') + .join(' '); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart new file mode 100644 index 00000000..0b86c96e --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/number_input_textfield.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class QuickCalibratingDialog extends StatefulWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibratingDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + State createState() => _QuickCalibratingDialogState(); +} + +class _QuickCalibratingDialogState extends State { + late TextEditingController _controller; + String? _errorText; + + void _onRightTap() { + final value = int.tryParse(_controller.text); + + if (value == null || value < 10 || value > 120) { + setState(() { + _errorText = 'Number should be between 10 and 120'; + }); + return; + } + + setState(() { + _errorText = null; + }); + widget.parentContext.read().add( + ChangeTimerControlEvent( + deviceId: widget.deviceId, + timControl: value, + ), + ); + Navigator.of(widget.parentContext).pop(); + showDialog( + context: widget.parentContext, + builder: (_) => CalibrateCompletedDialog( + parentContext: widget.parentContext, + deviceId: widget.deviceId, + ), + ); + } + + @override + void initState() { + _controller = TextEditingController(text: widget.timControl.toString()); + super.initState(); + } + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Calibrating', + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '1. please Enter the Travel Time:', + style: TextStyle(color: ColorsManager.grayBorder), + ), + const SizedBox(height: 10), + Container( + width: 150, + height: 40, + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: NumberInputField(controller: _controller), + ), + const Expanded( + child: Text( + 'seconds', + style: TextStyle( + fontSize: 15, + color: ColorsManager.blueColor, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + if (_errorText != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _errorText!, + style: const TextStyle( + color: ColorsManager.red, + fontSize: 14, + ), + ), + ), + ], + ), + leftOnTap: () => Navigator.of(widget.parentContext).pop(), + rightOnTap: _onRightTap, + ), + ); + } +} diff --git a/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart new file mode 100644 index 00000000..803d904f --- /dev/null +++ b/lib/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart'; +import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart'; + +class QuickCalibrationDialog extends StatelessWidget { + final int timControl; + final String deviceId; + final BuildContext parentContext; + const QuickCalibrationDialog({ + super.key, + required this.timControl, + required this.deviceId, + required this.parentContext, + }); + + @override + Widget build(_) { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: AccurateDialogWidget( + title: 'Quick Calibration', + body: const NormalTextBodyForDialog( + title: 'Prepare Calibration:', + step1: + '1. Confirm that the curtain is in the fully closed and suspended state.', + step2: '2. click Next to Start calibration.', + ), + leftOnTap: () => Navigator.of(parentContext).pop(), + rightOnTap: () { + Navigator.of(parentContext).pop(); + showDialog( + context: parentContext, + builder: (_) => QuickCalibratingDialog( + timControl: timControl, + deviceId: deviceId, + parentContext: parentContext, + ), + ); + }, + ), + ); + } +} 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 index 62213205..0ec55e39 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_bloc.dart @@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc { emit(currentState.copyWith( scheduleMode: event.scheduleMode, countdownRemaining: Duration.zero, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } @@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc { if (state is ScheduleLoaded) { final currentState = state as ScheduleLoaded; emit(currentState.copyWith( + countdownSeconds: event.seconds, countdownHours: event.hours, countdownMinutes: event.minutes, inchingHours: 0, @@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc { inchingHours: event.hours, inchingMinutes: event.minutes, countdownRemaining: Duration.zero, + inchingSeconds: 0, // Add this )); } } @@ -257,7 +265,7 @@ class ScheduleBloc extends Bloc { category: event.category, deviceId: deviceId, time: getTimeStampWithoutSeconds(dateTime).toString(), - code: event.category, + code: event.code ?? event.category, value: event.functionOn, days: event.selectedDays); if (success) { @@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: countdownDuration.inMinutes % 60, countdownRemaining: countdownDuration, isCountdownActive: true, + countdownSeconds: countdownDuration.inSeconds, ), ); @@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc { countdownMinutes: 0, countdownRemaining: Duration.zero, isCountdownActive: false, + countdownSeconds: 0, ), ); } @@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc { inchingMinutes: inchingDuration.inMinutes % 60, isInchingActive: true, countdownRemaining: inchingDuration, + countdownSeconds: inchingDuration.inSeconds, ), ); } @@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc { } String extractTime(String isoDateTime) { - // Example input: "2025-06-19T15:45:00.000" - return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00" + return isoDateTime.split('T')[1].split('.')[0]; } int? getTimeStampWithoutSeconds(DateTime? dateTime) { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart index 7ec144fe..a28b8757 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_event.dart @@ -70,17 +70,19 @@ class ScheduleAddEvent extends ScheduleEvent { final String category; final String time; final List selectedDays; - final bool functionOn; + final dynamic functionOn; + final String? code; const ScheduleAddEvent({ required this.category, required this.time, required this.selectedDays, required this.functionOn, + required this.code, }); @override - List get props => [category, time, selectedDays, functionOn]; + List get props => [category, time, selectedDays, functionOn, code]; } class ScheduleEditEvent extends ScheduleEvent { @@ -146,14 +148,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent { class UpdateCountdownTimeEvent extends ScheduleEvent { final int hours; final int minutes; + final int seconds; const UpdateCountdownTimeEvent({ required this.hours, required this.minutes, + required this.seconds, }); @override - List get props => [hours, minutes]; + List get props => [hours, minutes, seconds]; } class UpdateInchingTimeEvent extends ScheduleEvent { diff --git a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart index 10cd7611..63551c3a 100644 --- a/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart +++ b/lib/pages/device_managment/schedule_device/bloc/schedule_state.dart @@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState { final bool isCountdownActive; final int inchingHours; final int inchingMinutes; + final int inchingSeconds; final bool isInchingActive; final ScheduleModes scheduleMode; final Duration? countdownRemaining; + final int? countdownSeconds; const ScheduleLoaded({ + this.countdownSeconds = 0, + this.inchingSeconds = 0, required this.schedules, this.selectedTime, required this.selectedDays, @@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState { bool? isInchingActive, ScheduleModes? scheduleMode, Duration? countdownRemaining, + String? deviceId, + int? countdownSeconds, + int? inchingSeconds, }) { return ScheduleLoaded( schedules: schedules ?? this.schedules, @@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState { selectedDays: selectedDays ?? this.selectedDays, functionOn: functionOn ?? this.functionOn, isEditing: isEditing ?? this.isEditing, - deviceId: deviceId, + deviceId: deviceId ?? this.deviceId, countdownHours: countdownHours ?? this.countdownHours, countdownMinutes: countdownMinutes ?? this.countdownMinutes, isCountdownActive: isCountdownActive ?? this.isCountdownActive, @@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive: isInchingActive ?? this.isInchingActive, scheduleMode: scheduleMode ?? this.scheduleMode, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + countdownSeconds: countdownSeconds ?? this.countdownSeconds, + inchingSeconds: inchingSeconds ?? this.inchingSeconds, ); } @@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState { isInchingActive, scheduleMode, countdownRemaining, + countdownSeconds, + inchingSeconds, ]; } 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 index d45073ec..418bab6c 100644 --- 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 @@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class CountdownInchingView extends StatefulWidget { - const CountdownInchingView({super.key}); + final String deviceId; + const CountdownInchingView({super.key, required this.deviceId}); @override State createState() => _CountdownInchingViewState(); @@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget { 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) { + void _updateControllers( + int displayHours, int displayMinutes, int displaySeconds) { if (_lastHours != displayHours) { WidgetsBinding.instance.addPostFrameCallback((_) { if (_hoursController.hasClients) { @@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State { }); _lastMinutes = displayMinutes; } + // Update seconds controller + if (_lastSeconds != displaySeconds) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_secondsController.hasClients) { + _secondsController.jumpToItem(displaySeconds); + } + }); + _lastSeconds = displaySeconds; + } } @override @@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State { 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; @@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State { 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, + ), + ); + } - _updateControllers(displayHours, displayMinutes); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: value, minutes: displayMinutes)); + hours: value, + minutes: displayMinutes, + seconds: displaySeconds, + )); } }, isActive: isActive, @@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State { (value) { if (!isActive) { context.read().add(UpdateCountdownTimeEvent( - hours: displayHours, minutes: value)); + hours: displayHours, + minutes: value, + seconds: displaySeconds, + )); } }, isActive: isActive, ), + const SizedBox(width: 10), + if (isActive) + _buildPickerColumn( + context, + 's', + displaySeconds, + 60, + _secondsController, + (value) { + if (!isActive) { + context + .read() + .add(UpdateCountdownTimeEvent( + hours: displayHours, + minutes: displayMinutes, + seconds: value, + )); + } + }, + isActive: isActive, + ), ], ), ], 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 index 2fa34559..c511b8bd 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart'; @@ -9,13 +10,19 @@ import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widg import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart'; import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; class BuildScheduleView extends StatelessWidget { - const BuildScheduleView( - {super.key, required this.deviceUuid, required this.category}); + const BuildScheduleView({ + super.key, + required this.deviceUuid, + required this.category, + this.code, + }); final String deviceUuid; final String category; + final String? code; @override Widget build(BuildContext context) { @@ -57,13 +64,21 @@ class BuildScheduleView extends StatelessWidget { final entry = await ScheduleDialogHelper .showAddScheduleDialog( context, - schedule: null, + schedule: ScheduleEntry( + category: category, + time: '', + function: Status( + code: code.toString(), value: null), + days: [], + ), isEdit: false, + code: code, ); if (entry != null) { context.read().add( ScheduleAddEvent( - category: entry.category, + category: category, + code: entry.function.code, time: entry.time, functionOn: entry.function.value, selectedDays: entry.days, @@ -74,7 +89,9 @@ class BuildScheduleView extends StatelessWidget { ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - const CountdownInchingView(), + CountdownInchingView( + deviceId: deviceUuid, + ), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.countdown) CountdownModeButtons( 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 index 98ae0515..21f404ff 100644 --- a/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart +++ b/lib/pages/device_managment/schedule_device/schedule_widgets/schedule_table.dart @@ -162,11 +162,18 @@ class _ScheduleTableView extends StatelessWidget { child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + bool temp; + if (schedule.category == 'CUR_2') { + temp = schedule.function.value == 'open' ? true : false; + } else { + temp = schedule.function.value as bool; + } context.read().add( ScheduleUpdateEntryEvent( category: schedule.category, scheduleId: schedule.scheduleId, - functionOn: schedule.function.value, + functionOn: temp, + // schedule.function.value, enable: !schedule.enable, ), ); @@ -188,7 +195,10 @@ class _ScheduleTableView extends StatelessWidget { child: Text(_getSelectedDays( ScheduleModel.parseSelectedDays(schedule.days)))), Center(child: Text(formatIsoStringToTime(schedule.time, context))), - Center(child: Text(schedule.function.value ? 'On' : 'Off')), + if (schedule.category == 'CUR_2') + Center(child: Text(schedule.function.value)) + else + Center(child: Text(schedule.function.value ? 'On' : 'Off')), Center( child: Wrap( runAlignment: WrapAlignment.center, diff --git a/lib/pages/device_managment/shared/device_batch_control_dialog.dart b/lib/pages/device_managment/shared/device_batch_control_dialog.dart index f2dc68f5..c7ea6c71 100644 --- a/lib/pages/device_managment/shared/device_batch_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_batch_control_dialog.dart @@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode { +class DeviceBatchControlDialog extends StatelessWidget + with RouteControlsBasedCode { final List devices; const DeviceBatchControlDialog({super.key, required this.devices}); @@ -18,7 +19,7 @@ class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCo borderRadius: BorderRadius.circular(20), ), child: SizedBox( - width: devices.length < 2 ? 500 : 800, + width: devices.length < 2 ? 600 : 800, // height: context.screenHeight * 0.7, child: SingleChildScrollView( child: Padding( 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/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index ae7feac9..f55b32ab 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 @@ -17,14 +17,21 @@ class ScheduleDialogHelper { BuildContext context, { ScheduleEntry? schedule, bool isEdit = false, + String? code, }) { + bool temp; + if (schedule?.category == 'CUR_2') { + temp = schedule!.function.value == 'open' ? true : false; + } else { + temp = schedule!.function.value; + } final initialTime = schedule != null ? _convertStringToTimeOfDay(schedule.time) : TimeOfDay.now(); final initialDays = schedule != null ? _convertDaysStringToBooleans(schedule.days) : List.filled(7, false); - bool? functionOn = schedule?.function.value ?? true; + bool? functionOn = temp; TimeOfDay selectedTime = initialTime; List selectedDays = List.of(initialDays); @@ -96,7 +103,8 @@ class ScheduleDialogHelper { setState(() => selectedDays[i] = v); }), const SizedBox(height: 16), - _buildFunctionSwitch(ctx, functionOn!, (v) { + _buildFunctionSwitch(schedule!.category, ctx, functionOn!, + (v) { setState(() => functionOn = v); }), ], @@ -115,10 +123,21 @@ class ScheduleDialogHelper { width: 100, child: ElevatedButton( onPressed: () { + dynamic temp; + if (schedule?.category == 'CUR_2') { + temp = functionOn! ? 'open' : 'close'; + } else { + temp = functionOn; + } + print(temp); final entry = ScheduleEntry( category: schedule?.category ?? 'switch_1', time: _formatTimeOfDayToISO(selectedTime), - function: Status(code: 'switch_1', value: functionOn), + function: Status( + code: code ?? 'switch_1', + value: temp, + // functionOn, + ), days: _convertSelectedDaysToStrings(selectedDays), scheduleId: schedule?.scheduleId, ); @@ -185,7 +204,7 @@ class ScheduleDialogHelper { } static Widget _buildFunctionSwitch( - BuildContext ctx, bool isOn, Function(bool) onChanged) { + String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) { return Row( children: [ Text( @@ -199,14 +218,14 @@ class ScheduleDialogHelper { groupValue: isOn, onChanged: (val) => onChanged(true), ), - const Text('On'), + Text(categor == 'CUR_2' ? 'open' : 'On'), const SizedBox(width: 10), Radio( value: false, groupValue: isOn, onChanged: (val) => onChanged(false), ), - const Text('Off'), + Text(categor == 'CUR_2' ? 'close' : 'Off'), ], ); } 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/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index 3fd07834..f38ea994 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -170,45 +170,45 @@ class RoutineBloc extends Bloc { } } -Future _onLoadScenes( - LoadScenes event, Emitter emit) async { - emit(state.copyWith(isLoading: true, errorMessage: null)); - List scenes = []; - try { - BuildContext context = NavigationService.navigatorKey.currentContext!; - var createRoutineBloc = context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && - createRoutineBloc.selectedCommunityId == '') { - var spaceBloc = context.read(); - for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = - spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - scenes.addAll( - await SceneApi.getScenes(spaceId, communityId, projectUuid)); + Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + List scenes = []; + try { + BuildContext context = NavigationService.navigatorKey.currentContext!; + var createRoutineBloc = context.read(); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { + var spaceBloc = context.read(); + for (var communityId in spaceBloc.state.selectedCommunities) { + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + for (var spaceId in spacesList) { + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); + } } + } else { + scenes.addAll(await SceneApi.getScenes( + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } - } else { - scenes.addAll(await SceneApi.getScenes( - createRoutineBloc.selectedSpaceId, - createRoutineBloc.selectedCommunityId, - projectUuid)); - } - emit(state.copyWith( - scenes: scenes, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( + emit(state.copyWith( + scenes: scenes, isLoading: false, - loadScenesErrorMessage: 'Failed to load scenes', - errorMessage: '', - loadAutomationErrorMessage: '', - scenes: scenes)); + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + scenes: scenes)); + } } -} Future _onLoadAutomation( LoadAutomation event, Emitter emit) async { @@ -936,16 +936,12 @@ Future _onLoadScenes( for (var communityId in spaceBloc.state.selectedCommunities) { List spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - devices.addAll(await DevicesManagementApi() - .fetchDevices(communityId, spaceId, projectUuid)); - } + devices.addAll(await DevicesManagementApi() + .fetchDevices(projectUuid, spacesId: spacesList)); } } else { - devices.addAll(await DevicesManagementApi().fetchDevices( - createRoutineBloc.selectedCommunityId, - createRoutineBloc.selectedSpaceId, - projectUuid)); + devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid, + spacesId: [createRoutineBloc.selectedSpaceId])); } emit(state.copyWith(isLoading: false, devices: devices)); diff --git a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart index bdf8660d..64295e2a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/curtain_dialog.dart @@ -58,7 +58,9 @@ class CurtainHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('AC Functions'), + DialogHeader(dialogType == 'THEN' + ? 'Curtain Functions' + : 'Curtain Conditions'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, 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/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 index 3a9aa3c8..5d81bffb 100644 --- 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 @@ -1,4 +1,8 @@ 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 { @@ -6,9 +10,21 @@ class SpaceManagementBody extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return Row( children: [ - SpaceManagementCommunitiesTree(), + 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/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 index bfc02f11..bdda04ee 100644 --- 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 @@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc ) { emit( CommunitiesTreeSelectionState( - selectedCommunity: null, + selectedCommunity: event.community, selectedSpace: event.space, ), ); 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 index 95ffe173..21088632 100644 --- 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 @@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable { } final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { - final CommunityModel? community; + final CommunityModel community; const SelectCommunityEvent({required this.community}); @override @@ -17,8 +17,9 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent { final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent { final SpaceModel? space; + final CommunityModel community; - const SelectSpaceEvent({required this.space}); + const SelectSpaceEvent({required this.space, required this.community}); @override List get props => [space]; 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 index dcd44ac8..795e2c3a 100644 --- 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 @@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget { initiallyExpanded: spaceIsExpanded, onExpansionChanged: (expanded) {}, onItemSelected: () => context.read().add( - SelectSpaceEvent(space: space), + SelectSpaceEvent(community: community, space: space), ), children: space.children .map( 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 index 25c094db..b5f2a1b7 100644 --- 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 @@ -1,9 +1,8 @@ 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/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/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.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'; @@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget { if (isSelected) { _clearSelection(context); } else { - _showCreateCommunityDialog(context); + SpaceManagementCommunityDialogHelper.showCreateDialog(context); } } @@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget { const ClearCommunitiesTreeSelectionEvent(), ); } - - void _showCreateCommunityDialog(BuildContext context) => showDialog( - context: context, - builder: (_) => CreateCommunityDialog( - title: const Text('Community Name'), - onCreateCommunity: (community) { - context.read().add( - InsertCommunity(community), - ); - context.read().add( - SelectCommunityEvent(community: community), - ); - }, - ), - ); } 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 bd91f6ce..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 @@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService { return _defaultErrorMessage; } final error = body['error'] as Map?; - final errorMessage = error?['error'] as String? ?? ''; + final errorMessage = error?['message'] as String? ?? ''; return errorMessage; } 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 index 8c1d474d..a9af44d6 100644 --- 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 @@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget { ); onCreateCommunity.call(community); break; - case CreateCommunityFailure(:final message): + case CreateCommunityFailure(): Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message)), - ); break; default: break; 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/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/device_model.dart b/lib/pages/visitor_password/model/device_model.dart index f9711eed..75d00350 100644 --- a/lib/pages/visitor_password/model/device_model.dart +++ b/lib/pages/visitor_password/model/device_model.dart @@ -80,6 +80,10 @@ class DeviceModel { tempIcon = Assets.openedDoor; } else if (type == DeviceType.WaterLeak) { tempIcon = Assets.waterLeakNormal; + } else if (type == DeviceType.Curtain2) { + tempIcon = Assets.curtainIcon; + } else if (type == DeviceType.Curtain) { + tempIcon = Assets.curtainIcon; } else { tempIcon = Assets.blackLogo; } 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/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4b5cb0e2..edf373dd 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -2,10 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/common/date_time_widget.dart'; -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'; @@ -23,8 +21,8 @@ class VisitorPasswordDialog extends StatelessWidget { @override Widget build(BuildContext context) { - Size size = MediaQuery.of(context).size; - var text = Theme.of(context) + final size = MediaQuery.of(context).size; + final text = Theme.of(context) .textTheme .bodySmall! .copyWith(color: Colors.black, fontSize: 13); @@ -41,8 +39,7 @@ 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'), @@ -56,22 +53,19 @@ class VisitorPasswordDialog extends StatelessWidget { .passwordStatus!.failedOperations.length, itemBuilder: (context, index) { return Container( - margin: EdgeInsets.all(5), + margin: const EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc - .passwordStatus! - .failedOperations[index] - .deviceUuid)), + child: Text(visitorBloc.passwordStatus! + .failedOperations[index].deviceName)), ); }, ), ), ], ), - if (visitorBloc - .passwordStatus!.successOperations.isNotEmpty) + if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) Column( children: [ const Text('Success Devices'), @@ -85,14 +79,12 @@ class VisitorPasswordDialog extends StatelessWidget { .passwordStatus!.successOperations.length, itemBuilder: (context, index) { return Container( - margin: EdgeInsets.all(5), + margin: const EdgeInsets.all(5), decoration: containerDecoration, height: 45, child: Center( - child: Text(visitorBloc - .passwordStatus! - .successOperations[index] - .deviceUuid)), + child: Text(visitorBloc.passwordStatus! + .successOperations[index].deviceName)), ); }, ), @@ -102,7 +94,7 @@ class VisitorPasswordDialog extends StatelessWidget { ], )) .then((v) { - Navigator.of(context).pop(true); + Navigator.of(context).pop(v); }); } else if (state is FailedState) { visitorBloc.stateDialog( @@ -115,16 +107,14 @@ class VisitorPasswordDialog extends StatelessWidget { child: BlocBuilder( builder: (BuildContext context, VisitorPasswordState state) { final visitorBloc = BlocProvider.of(context); - bool isRepeat = + final 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), + fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), ), content: state is LoadingInitialState ? const Center(child: CircularProgressIndicator()) @@ -310,14 +300,12 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(SelectTimeEvent( - context: context, - isEffective: false)); + context: context, isEffective: false)); } else { - visitorBloc.add( - SelectTimeVisitorPassword( - context: context, - isStart: false, - isRepeat: false)); + visitorBloc.add(SelectTimeVisitorPassword( + context: context, + isStart: false, + isRepeat: false)); } }, startTime: () { @@ -326,31 +314,28 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(SelectTimeEvent( - context: context, - isEffective: true)); + 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') - ? visitorBloc.effectiveTime - : visitorBloc.startTimeAccess - .toString(), + firstString: + (visitorBloc.usageFrequencySelected == + 'Periodic' && + visitorBloc.accessTypeSelected == + 'Offline Password') + ? visitorBloc.effectiveTime + : visitorBloc.startTimeAccess, secondString: (visitorBloc .usageFrequencySelected == 'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') ? visitorBloc.expirationTime - : visitorBloc.endTimeAccess.toString(), + : visitorBloc.endTimeAccess, icon: Assets.calendarIcon), const SizedBox( height: 10, @@ -410,8 +395,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: CupertinoSwitch( value: visitorBloc.repeat, onChanged: (value) { - visitorBloc - .add(ToggleRepeatEvent()); + visitorBloc.add(ToggleRepeatEvent()); }, applyTheme: true, ), @@ -442,8 +426,7 @@ class VisitorPasswordDialog extends StatelessWidget { }, ).then((listDevice) { if (listDevice != null) { - visitorBloc.selectedDevices = - listDevice; + visitorBloc.selectedDevices = listDevice; } }); }, @@ -455,8 +438,7 @@ class VisitorPasswordDialog extends StatelessWidget { .bodySmall! .copyWith( fontWeight: FontWeight.w400, - color: - ColorsManager.whiteColors, + color: ColorsManager.whiteColors, fontSize: 12), ), ), @@ -476,7 +458,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( @@ -495,37 +477,30 @@ 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') { + visitorBloc.accessTypeSelected == 'Offline Password') { if (visitorBloc.expirationTime != 'End Time' && visitorBloc.effectiveTime != 'Start Time') { setPasswordFunction(context, size, visitorBloc); } 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 != 'End Time' && + visitorBloc.startTimeAccess != '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, @@ -539,15 +514,13 @@ 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', + message: 'Please select Access Period to continue', title: 'Access Period'); } } else { @@ -593,9 +566,8 @@ class VisitorPasswordDialog extends StatelessWidget { alignment: Alignment.center, content: SizedBox( height: size.height * 0.25, - child: Center( - child: - CircularProgressIndicator(), // Display a loading spinner + child: const Center( + child: CircularProgressIndicator(), // Display a loading spinner ), ), ); @@ -619,14 +591,12 @@ class VisitorPasswordDialog extends StatelessWidget { ), Text( 'Set Password', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith( - fontSize: 30, - fontWeight: FontWeight.w400, - color: Colors.black, - ), + style: + Theme.of(context).textTheme.headlineLarge!.copyWith( + fontSize: 30, + fontWeight: FontWeight.w400, + color: Colors.black, + ), ), ], ), @@ -651,7 +621,7 @@ class VisitorPasswordDialog extends StatelessWidget { child: DefaultButton( borderRadius: 8, onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(null); }, backgroundColor: Colors.white, child: Text( @@ -672,8 +642,7 @@ 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, @@ -681,8 +650,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == - 'Online Password') { + visitorBloc.accessTypeSelected == 'Online Password') { visitorBloc.add(OnlineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, @@ -693,8 +661,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'One-Time' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(OfflineOneTimePasswordEvent( context: context, passwordName: visitorBloc.userNameController.text, @@ -702,8 +669,7 @@ class VisitorPasswordDialog extends StatelessWidget { )); } else if (visitorBloc.usageFrequencySelected == 'Periodic' && - visitorBloc.accessTypeSelected == - 'Offline Password') { + visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.add(OfflineMultipleTimePasswordEvent( passwordName: visitorBloc.userNameController.text, email: visitorBloc.emailController.text, diff --git a/lib/services/batch_control_devices_service.dart b/lib/services/batch_control_devices_service.dart index f78cdef4..16542c8c 100644 --- a/lib/services/batch_control_devices_service.dart +++ b/lib/services/batch_control_devices_service.dart @@ -11,7 +11,8 @@ abstract interface class BatchControlDevicesService { }); } -final class RemoteBatchControlDevicesService implements BatchControlDevicesService { +final class RemoteBatchControlDevicesService + implements BatchControlDevicesService { @override Future batchControlDevices({ required List uuids, diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 6fb27daf..dd54cfef 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -13,15 +13,13 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class DevicesManagementApi { Future> fetchDevices( - String communityId, String spaceId, String projectId) async { + String projectId, { + List? spacesId, + }) async { try { final response = await HTTPService().get( - path: communityId.isNotEmpty && spaceId.isNotEmpty - ? ApiEndpoints.getSpaceDevices - .replaceAll('{spaceUuid}', spaceId) - .replaceAll('{communityUuid}', communityId) - .replaceAll('{projectId}', projectId) - : ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), + queryParameters: {if (spacesId != null) 'spaces': spacesId}, + path: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), showServerMessage: true, expectedResponseModel: (json) { List jsonData = json['data']; @@ -393,7 +391,7 @@ class DevicesManagementApi { required String deviceId, required String time, required String code, - required bool value, + required dynamic value, required List days, }) async { final response = await HTTPService().post( @@ -416,5 +414,4 @@ class DevicesManagementApi { ); return response; } - } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index eb7b6a3e..6dda1108 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -18,7 +18,7 @@ abstract class ApiEndpoints { static const String getAllDevices = '/projects/{projectId}/devices'; static const String getSpaceDevices = - '/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices'; + '/projects/{projectId}/devices'; static const String getDeviceStatus = '/devices/{uuid}/functions/status'; static const String getBatchStatus = '/devices/batch'; diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 6a0ef799..8979c446 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -125,6 +125,10 @@ class Assets { 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'; + static const String pauseCurtain = 'assets/icons/pause_curtain.svg'; + 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'; //assets/icons/Gateway.svg diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 9bfd322f..947e63aa 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -3,6 +3,7 @@ enum DeviceType { LightBulb, DoorLock, Curtain, + Curtain2, Blind, OneGang, TwoGang, @@ -44,6 +45,7 @@ enum DeviceType { Map devicesTypesMap = { "AC": DeviceType.AC, + "CUR_2": DeviceType.Curtain2, "GW": DeviceType.Gateway, "CPS": DeviceType.CeilingSensor, "DL": DeviceType.DoorLock,