Compare commits

..

21 Commits

Author SHA1 Message Date
3c9494963d Add generated configuration files for Flutter integration across platforms 2025-06-25 15:58:58 +03:00
f38ac58442 Add bloc closure handling and improve device status updates in AcBloc 2025-06-25 14:45:10 +03:00
487c5a894b Sp 1796 fe set the max on range of aqi chart based on selected pollutant s current highest value (#290)
<!--
  Thanks for contributing!

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

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

## Jira Ticket
[SP-1796](https://syncrow.atlassian.net/browse/SP-1796)

## Description

Added day of month labels to all analytics charts
Implemented ranges for each pollutant based on the highest value of each
pollutant in the range of aqi chart.

## Type of Change

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

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [x]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1796]:
https://syncrow.atlassian.net/browse/SP-1796?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 12:42:55 +03:00
7e0200aad8 SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when… (#294)
<!--
  Thanks for contributing!

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

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

## Jira Ticket
[SP-1770](https://syncrow.atlassian.net/browse/SP-1770)

## Description

Space Tree Selection state reads from the correct list, based on if the
user was filtering or not.

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1770]:
https://syncrow.atlassian.net/browse/SP-1770?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 12:42:34 +03:00
52b843d514 SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when-selecting-space-from-sidebar. 2025-06-25 09:53:09 +03:00
423ad6e687 Enhance navigation buttons in SmartPowerDeviceControl for better user… (#293)
… experience

<!--
  Thanks for contributing!

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

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



## Description

<!--- Describe your changes in detail -->
Enhance navigation buttons in SmartPowerDeviceControl 

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-25 09:13:33 +03:00
932e50f518 sp:1677 [FE] Device status in Control modal always shows "Online" regardless of actual status (#287)
<!--
  Thanks for contributing!

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

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

## Jira Ticket
[SP-1677](https://syncrow.atlassian.net/browse/SP-1677)

## Description

status depend on the real status of the device afterit was static 

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1677]:
https://syncrow.atlassian.net/browse/SP-1677?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 08:18:10 +03:00
c649044a1f Enhance navigation buttons in SmartPowerDeviceControl for better user experience 2025-06-24 16:40:42 +03:00
c46cfb04a7 Add countdown seconds to schedule management (#291)
<!--
  Thanks for contributing!

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

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


## Description

<!--- Describe your changes in detail -->
Add countdown seconds to schedule management
## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-24 16:21:43 +03:00
8754960713 Cancel-button-on-"Create-Visitor-Password"-modal-unnecessarily-triggers-visitor-passwords-API (#292)
… on pop

<!--
  Thanks for contributing!

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

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



## Description

<!--- Describe your changes in detail -->

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-24 16:21:03 +03:00
c6e98fa245 Refactor visitor password dialog navigation to return specific values on pop 2025-06-24 16:06:53 +03:00
277a9ce4f0 Add countdown seconds to schedule management 2025-06-24 15:38:16 +03:00
f901983aa5 Implemented ranges for the values in the AQI chart based on the selected pollutant. 2025-06-24 15:01:25 +03:00
010403f1fa Added day of month axis name to all charts. 2025-06-24 14:50:22 +03:00
ee1ebeae2e Changed energy management charts titles for a more clear name. 2025-06-24 14:45:15 +03:00
6e6ef79ed0 enhanced heat map tooltip's message. 2025-06-24 14:44:56 +03:00
7e5825de45 Fixed typo in occupancy sidebar. 2025-06-24 14:44:45 +03:00
db9e856bca Sp 1711 fe implement blank state (#288)
<!--
  Thanks for contributing!

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

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

## Jira Ticket
[SP-1711](https://syncrow.atlassian.net/browse/SP-1711)
[SP-1712](https://syncrow.atlassian.net/browse/SP-1712)

## Description

1. Shows tooltip on space cell.
2. navigates to space when a new space is selected
3. Fixed a thrown Exception because of an Expanded widget
4. Adjusted connections between spaces to select from and to nodes.
5. Created a `SpaceDetailsDialog` and a helper class to show it, because
a space can be created through different widgets, and this was done to
unify the logic and remove code duplication.

## Type of Change

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

- [x]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1711]:
https://syncrow.atlassian.net/browse/SP-1711?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[SP-1712]:
https://syncrow.atlassian.net/browse/SP-1712?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-24 12:29:25 +03:00
07435ec89e On access management page Create visitor password dialog is not responsive (#289)
<!--
  Thanks for contributing!

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

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

## Jira Ticket
[SP-1474](https://syncrow.atlassian.net/browse/SP-1474)

## Description

<!--- Describe your changes in detail -->
Add responsive input fields and radio groups for visitor password setup

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1474]:
https://syncrow.atlassian.net/browse/SP-1474?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-24 12:27:39 +03:00
2a2fb7ffca Add responsive input fields and radio groups for visitor password setup 2025-06-24 11:36:50 +03:00
95d6e1ecda if online go green with online status else red with offline status 2025-06-23 16:33:45 +03:00
24 changed files with 810 additions and 373 deletions

View File

@ -18,7 +18,11 @@ abstract final class RangeOfAqiChartsHelper {
(ColorsManager.hazardousPurple, 'Hazardous'), (ColorsManager.hazardousPurple, 'Hazardous'),
]; ];
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) { static FlTitlesData titlesData(
BuildContext context,
List<RangeOfAqi> data, {
double leftSideInterval = 50,
}) {
final titlesData = EnergyManagementChartsHelper.titlesData(context); final titlesData = EnergyManagementChartsHelper.titlesData(context);
return titlesData.copyWith( return titlesData.copyWith(
bottomTitles: titlesData.bottomTitles.copyWith( bottomTitles: titlesData.bottomTitles.copyWith(
@ -39,11 +43,11 @@ abstract final class RangeOfAqiChartsHelper {
leftTitles: titlesData.leftTitles.copyWith( leftTitles: titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith( sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70, reservedSize: 70,
interval: 50, interval: leftSideInterval,
maxIncluded: false, maxIncluded: false,
minIncluded: true, minIncluded: true,
getTitlesWidget: (value, meta) { getTitlesWidget: (value, meta) {
final text = value >= 300 ? '301+' : value.toInt().toString(); final text = value.toInt().toString();
return Padding( return Padding(
padding: const EdgeInsetsDirectional.only(end: 12), padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox( child: FittedBox(

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -149,6 +150,7 @@ class AqiDistributionChart extends StatelessWidget {
); );
final bottomTitles = AxisTitles( final bottomTitles = AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: chartData.isNotEmpty, showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox( getTitlesWidget: (value, _) => FittedBox(

View File

@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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/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/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class RangeOfAqiChart extends StatelessWidget { class RangeOfAqiChart extends StatelessWidget {
final List<RangeOfAqi> chartData; final List<RangeOfAqi> chartData;
final AqiType selectedAqiType;
const RangeOfAqiChart({ const RangeOfAqiChart({
super.key, super.key,
required this.chartData, required this.chartData,
required this.selectedAqiType,
}); });
List<(List<double> values, Color color, Color? dotColor)> get _lines { List<(List<double> values, Color color, Color? dotColor)> get _lines {
@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget {
]; ];
} }
(double maxY, double interval) get _maxYForAqiType {
const aqiMaxValues = <AqiType, (double maxY, double interval)>{
AqiType.aqi: (401, 100),
AqiType.pm25: (351, 50),
AqiType.pm10: (501, 100),
AqiType.hcho: (301, 50),
AqiType.tvoc: (501, 50),
AqiType.co2: (1251, 250),
};
return aqiMaxValues[selectedAqiType]!;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LineChart( return LineChart(
LineChartData( LineChartData(
minY: 0, minY: 0,
maxY: 301, maxY: _maxYForAqiType.$1,
clipData: const FlClipData.vertical(), clipData: const FlClipData.vertical(),
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), gridData: EnergyManagementChartsHelper.gridData(
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), horizontalInterval: _maxYForAqiType.$2,
),
titlesData: RangeOfAqiChartsHelper.titlesData(
context,
chartData,
leftSideInterval: _maxYForAqiType.$2,
),
borderData: EnergyManagementChartsHelper.borderData(), borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
betweenBarsData: [ betweenBarsData: [

View File

@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
),
),
], ],
), ),
); );

View File

@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper {
return FlTitlesData( return FlTitlesData(
show: true, show: true,
bottomTitles: AxisTitles( bottomTitles: AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
drawBelowEverything: true, drawBelowEverything: true,
sideTitles: SideTitles( sideTitles: SideTitles(
interval: 1, interval: 1,
@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper {
); );
} }
static String getToolTipLabel(num month, double value) { static String getToolTipLabel(double value) => value.formatNumberToKwh;
final monthLabel = month.toString();
final valueLabel = value.formatNumberToKwh;
final labels = [monthLabel, valueLabel];
return labels.where((element) => element.isNotEmpty).join(', ');
}
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) { static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) { return touchedSpots.map((spot) {
return LineTooltipItem( return LineTooltipItem(
getToolTipLabel(spot.x, spot.y), getToolTipLabel(spot.y),
const TextStyle( const TextStyle(
color: ColorsManager.textPrimaryColor, color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: ChartTitle( child: ChartTitle(
title: Text('Energy Consumption per Device'), title: Text('Device energy consumed'),
), ),
), ),
), ),

View File

@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
child: FittedBox( child: FittedBox(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Total Energy Consumption')), child: ChartTitle(title: Text('Space energy consumed')),
), ),
), ),
const Spacer(flex: 4), const Spacer(flex: 4),

View File

@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget {
), ),
const Divider(height: 2, thickness: 1), const Divider(height: 2, thickness: 1),
Text( Text(
'$value Occupants', 'Occupancy detected: $value',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,

View File

@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget {
}) { }) {
final data = chartData; final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy); final occupancyValue = double.parse(data[group.x].occupancy);
final percentage = '${(occupancyValue).toStringAsFixed(0)}%'; final percentage = '${occupancyValue.toStringAsFixed(0)}%';
return BarTooltipItem( return BarTooltipItem(
percentage, percentage,
@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget {
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
child: Text( child: Text(
'${(value).toStringAsFixed(0)}%', '${value.toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
fontSize: 12, fontSize: 12,
color: ColorsManager.greyColor, color: ColorsManager.greyColor,
@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget {
); );
final bottomTitles = AxisTitles( final bottomTitles = AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
getTitlesWidget: (value, _) => FittedBox( getTitlesWidget: (value, _) => FittedBox(

View File

@ -23,7 +23,7 @@ class OccupancyEndSideBar extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const AnalyticsSidebarHeader(title: 'Presnce Sensor'), const AnalyticsSidebarHeader(title: 'Presence Sensor'),
Expanded( Expanded(
child: SizedBox( child: SizedBox(
// height: MediaQuery.sizeOf(context).height * 0.2, // height: MediaQuery.sizeOf(context).height * 0.2,

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ChartsXAxisTitle extends StatelessWidget {
const ChartsXAxisTitle({
this.label = 'Day of month',
super.key,
});
final String label;
@override
Widget build(BuildContext context) {
return Text(
label,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
),
);
}
}

View File

@ -45,7 +45,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
) async { ) async {
emit(AcsLoadingState()); emit(AcsLoadingState());
try { try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status);
if (deviceStatus.countdown1 != 0) { if (deviceStatus.countdown1 != 0) {
final totalMinutes = deviceStatus.countdown1 * 6; final totalMinutes = deviceStatus.countdown1 * 6;
@ -68,12 +69,13 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
} }
} }
void _listenToChanges(deviceId) { StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
try { try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue; _deviceStatusSubscription =
ref.onValue.listen((DatabaseEvent event) async {
stream.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return; if (event.snapshot.value == null) return;
Map<dynamic, dynamic> usersMap = Map<dynamic, dynamic> usersMap =
@ -82,10 +84,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
List<Status> statusList = []; List<Status> statusList = [];
usersMap['status'].forEach((element) { usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value'])); 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) { if (!isClosed) {
add(AcStatusUpdated(deviceStatus)); add(AcStatusUpdated(deviceStatus));
} }
@ -106,15 +112,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
Emitter<AcsState> emit, Emitter<AcsState> emit,
) async { ) async {
emit(AcsLoadingState()); emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
try { try {
final success = await controlDeviceService.controlDevice( final success = await controlDeviceService.controlDevice(
deviceUuid: event.deviceId, deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value), status: Status(code: event.code, value: event.value),
); );
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
if (!success) { if (!success) {
emit(const AcsFailedState(error: 'Failed to control device')); emit(const AcsFailedState(error: 'Failed to control device'));
} }
@ -129,8 +134,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
) async { ) async {
emit(AcsLoadingState()); emit(AcsLoadingState());
try { try {
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); final status =
deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
AcStatusModel.fromJson(event.devicesIds.first, status.status);
emit(ACStatusLoaded(status: deviceStatus)); emit(ACStatusLoaded(status: deviceStatus));
} catch (e) { } catch (e) {
emit(AcsFailedState(error: e.toString())); emit(AcsFailedState(error: e.toString()));
@ -293,14 +300,18 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
totalSeconds--; totalSeconds--;
scheduledHours = totalSeconds ~/ 3600; scheduledHours = totalSeconds ~/ 3600;
scheduledMinutes = (totalSeconds % 3600) ~/ 60; scheduledMinutes = (totalSeconds % 3600) ~/ 60;
if (!isClosed) {
add(UpdateTimerEvent()); add(UpdateTimerEvent());
}
} else { } else {
_countdownTimer?.cancel(); _countdownTimer?.cancel();
timerActive = false; timerActive = false;
scheduledHours = 0; scheduledHours = 0;
scheduledMinutes = 0; scheduledMinutes = 0;
if (!isClosed) {
add(TimerCompletedEvent()); add(TimerCompletedEvent());
} }
}
}); });
} }
@ -326,9 +337,11 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
_startCountdownTimer( _startCountdownTimer(
emit, emit,
); );
if (!isClosed) {
add(UpdateTimerEvent()); add(UpdateTimerEvent());
} }
} }
}
void _updateDeviceFunctionFromCode(String code, dynamic value) { void _updateDeviceFunctionFromCode(String code, dynamic value) {
switch (code) { switch (code) {
@ -370,6 +383,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
@override @override
Future<void> close() { Future<void> close() {
add(OnClose()); add(OnClose());
_countdownTimer?.cancel();
_deviceStatusSubscription?.cancel();
return super.close(); return super.close();
} }
} }

View File

@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
//Smart Power Clamp //Smart Power Clamp
class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout { class SmartPowerDeviceControl extends StatelessWidget
with HelperResponsiveLayout {
final String deviceId; final String deviceId;
const SmartPowerDeviceControl({super.key, required this.deviceId}); const SmartPowerDeviceControl({super.key, required this.deviceId});
@ -145,8 +146,11 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.arrow_left), icon: const Icon(Icons.arrow_left),
onPressed: () { onPressed: blocProvider.currentPage <= 0
blocProvider.add(SmartPowerArrowPressedEvent(-1)); ? null
: () {
blocProvider
.add(SmartPowerArrowPressedEvent(-1));
pageController.previousPage( pageController.previousPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -165,8 +169,11 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
), ),
IconButton( IconButton(
icon: const Icon(Icons.arrow_right), icon: const Icon(Icons.arrow_right),
onPressed: () { onPressed: blocProvider.currentPage >= 3
blocProvider.add(SmartPowerArrowPressedEvent(1)); ? null
: () {
blocProvider
.add(SmartPowerArrowPressedEvent(1));
pageController.nextPage( pageController.nextPage(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
blocProvider.add(SelectDateEvent(context: context)); blocProvider.add(SelectDateEvent(context: context));
blocProvider.add(FilterRecordsByDateEvent( blocProvider.add(FilterRecordsByDateEvent(
selectedDate: blocProvider.dateTime!, selectedDate: blocProvider.dateTime!,
viewType: viewType: blocProvider
blocProvider.views[blocProvider.currentIndex])); .views[blocProvider.currentIndex]));
}, },
widget: blocProvider.dateSwitcher(), widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty chartData: blocProvider.energyDataList.isNotEmpty

View File

@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
emit(currentState.copyWith( emit(currentState.copyWith(
scheduleMode: event.scheduleMode, scheduleMode: event.scheduleMode,
countdownRemaining: Duration.zero, countdownRemaining: Duration.zero,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
)); ));
} }
} }
@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
if (state is ScheduleLoaded) { if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded; final currentState = state as ScheduleLoaded;
emit(currentState.copyWith( emit(currentState.copyWith(
countdownSeconds: event.seconds,
countdownHours: event.hours, countdownHours: event.hours,
countdownMinutes: event.minutes, countdownMinutes: event.minutes,
inchingHours: 0, inchingHours: 0,
@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
inchingHours: event.hours, inchingHours: event.hours,
inchingMinutes: event.minutes, inchingMinutes: event.minutes,
countdownRemaining: Duration.zero, countdownRemaining: Duration.zero,
inchingSeconds: 0, // Add this
)); ));
} }
} }
@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
countdownMinutes: countdownDuration.inMinutes % 60, countdownMinutes: countdownDuration.inMinutes % 60,
countdownRemaining: countdownDuration, countdownRemaining: countdownDuration,
isCountdownActive: true, isCountdownActive: true,
countdownSeconds: countdownDuration.inSeconds,
), ),
); );
@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
countdownMinutes: 0, countdownMinutes: 0,
countdownRemaining: Duration.zero, countdownRemaining: Duration.zero,
isCountdownActive: false, isCountdownActive: false,
countdownSeconds: 0,
), ),
); );
} }
@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
inchingMinutes: inchingDuration.inMinutes % 60, inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: true, isInchingActive: true,
countdownRemaining: inchingDuration, countdownRemaining: inchingDuration,
countdownSeconds: inchingDuration.inSeconds,
), ),
); );
} }
@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
} }
String extractTime(String isoDateTime) { String extractTime(String isoDateTime) {
// Example input: "2025-06-19T15:45:00.000" return isoDateTime.split('T')[1].split('.')[0];
return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00"
} }
int? getTimeStampWithoutSeconds(DateTime? dateTime) { int? getTimeStampWithoutSeconds(DateTime? dateTime) {

View File

@ -146,14 +146,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent {
class UpdateCountdownTimeEvent extends ScheduleEvent { class UpdateCountdownTimeEvent extends ScheduleEvent {
final int hours; final int hours;
final int minutes; final int minutes;
final int seconds;
const UpdateCountdownTimeEvent({ const UpdateCountdownTimeEvent({
required this.hours, required this.hours,
required this.minutes, required this.minutes,
required this.seconds,
}); });
@override @override
List<Object> get props => [hours, minutes]; List<Object> get props => [hours, minutes, seconds];
} }
class UpdateInchingTimeEvent extends ScheduleEvent { class UpdateInchingTimeEvent extends ScheduleEvent {

View File

@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState {
final bool isCountdownActive; final bool isCountdownActive;
final int inchingHours; final int inchingHours;
final int inchingMinutes; final int inchingMinutes;
final int inchingSeconds;
final bool isInchingActive; final bool isInchingActive;
final ScheduleModes scheduleMode; final ScheduleModes scheduleMode;
final Duration? countdownRemaining; final Duration? countdownRemaining;
final int? countdownSeconds;
const ScheduleLoaded({ const ScheduleLoaded({
this.countdownSeconds = 0,
this.inchingSeconds = 0,
required this.schedules, required this.schedules,
this.selectedTime, this.selectedTime,
required this.selectedDays, required this.selectedDays,
@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState {
bool? isInchingActive, bool? isInchingActive,
ScheduleModes? scheduleMode, ScheduleModes? scheduleMode,
Duration? countdownRemaining, Duration? countdownRemaining,
String? deviceId,
int? countdownSeconds,
int? inchingSeconds,
}) { }) {
return ScheduleLoaded( return ScheduleLoaded(
schedules: schedules ?? this.schedules, schedules: schedules ?? this.schedules,
@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState {
selectedDays: selectedDays ?? this.selectedDays, selectedDays: selectedDays ?? this.selectedDays,
functionOn: functionOn ?? this.functionOn, functionOn: functionOn ?? this.functionOn,
isEditing: isEditing ?? this.isEditing, isEditing: isEditing ?? this.isEditing,
deviceId: deviceId, deviceId: deviceId ?? this.deviceId,
countdownHours: countdownHours ?? this.countdownHours, countdownHours: countdownHours ?? this.countdownHours,
countdownMinutes: countdownMinutes ?? this.countdownMinutes, countdownMinutes: countdownMinutes ?? this.countdownMinutes,
isCountdownActive: isCountdownActive ?? this.isCountdownActive, isCountdownActive: isCountdownActive ?? this.isCountdownActive,
@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState {
isInchingActive: isInchingActive ?? this.isInchingActive, isInchingActive: isInchingActive ?? this.isInchingActive,
scheduleMode: scheduleMode ?? this.scheduleMode, scheduleMode: scheduleMode ?? this.scheduleMode,
countdownRemaining: countdownRemaining ?? this.countdownRemaining, countdownRemaining: countdownRemaining ?? this.countdownRemaining,
countdownSeconds: countdownSeconds ?? this.countdownSeconds,
inchingSeconds: inchingSeconds ?? this.inchingSeconds,
); );
} }
@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState {
isInchingActive, isInchingActive,
scheduleMode, scheduleMode,
countdownRemaining, countdownRemaining,
countdownSeconds,
inchingSeconds,
]; ];
} }

View File

@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CountdownInchingView extends StatefulWidget { class CountdownInchingView extends StatefulWidget {
const CountdownInchingView({super.key}); final String deviceId;
const CountdownInchingView({super.key, required this.deviceId});
@override @override
State<CountdownInchingView> createState() => _CountdownInchingViewState(); State<CountdownInchingView> createState() => _CountdownInchingViewState();
@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget {
class _CountdownInchingViewState extends State<CountdownInchingView> { class _CountdownInchingViewState extends State<CountdownInchingView> {
late FixedExtentScrollController _hoursController; late FixedExtentScrollController _hoursController;
late FixedExtentScrollController _minutesController; late FixedExtentScrollController _minutesController;
late FixedExtentScrollController _secondsController;
int _lastHours = -1; int _lastHours = -1;
int _lastMinutes = -1; int _lastMinutes = -1;
int _lastSeconds = -1;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_hoursController = FixedExtentScrollController(); _hoursController = FixedExtentScrollController();
_minutesController = FixedExtentScrollController(); _minutesController = FixedExtentScrollController();
_secondsController = FixedExtentScrollController();
} }
@override @override
void dispose() { void dispose() {
_hoursController.dispose(); _hoursController.dispose();
_minutesController.dispose(); _minutesController.dispose();
_secondsController.dispose();
super.dispose(); super.dispose();
} }
void _updateControllers(int displayHours, int displayMinutes) { void _updateControllers(
int displayHours, int displayMinutes, int displaySeconds) {
if (_lastHours != displayHours) { if (_lastHours != displayHours) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_hoursController.hasClients) { if (_hoursController.hasClients) {
@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
}); });
_lastMinutes = displayMinutes; _lastMinutes = displayMinutes;
} }
// Update seconds controller
if (_lastSeconds != displaySeconds) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_secondsController.hasClients) {
_secondsController.jumpToItem(displaySeconds);
}
});
_lastSeconds = displaySeconds;
}
} }
@override @override
@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
return BlocBuilder<ScheduleBloc, ScheduleState>( return BlocBuilder<ScheduleBloc, ScheduleState>(
builder: (context, state) { builder: (context, state) {
if (state is! ScheduleLoaded) return const SizedBox.shrink(); if (state is! ScheduleLoaded) return const SizedBox.shrink();
final isCountDown = state.scheduleMode == ScheduleModes.countdown; final isCountDown = state.scheduleMode == ScheduleModes.countdown;
final isActive = final isActive =
isCountDown ? state.isCountdownActive : state.isInchingActive; isCountDown ? state.isCountdownActive : state.isInchingActive;
@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
final displayMinutes = isActive && state.countdownRemaining != null final displayMinutes = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inMinutes.remainder(60) ? state.countdownRemaining!.inMinutes.remainder(60)
: (isCountDown ? state.countdownMinutes : state.inchingMinutes); : (isCountDown ? state.countdownMinutes : state.inchingMinutes);
final displaySeconds = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inSeconds.remainder(60)
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
_updateControllers(displayHours, displayMinutes, displaySeconds!);
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: widget.deviceId,
),
);
}
_updateControllers(displayHours, displayMinutes);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
(value) { (value) {
if (!isActive) { if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent( context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
hours: value, minutes: displayMinutes)); hours: value,
minutes: displayMinutes,
seconds: displaySeconds,
));
} }
}, },
isActive: isActive, isActive: isActive,
@ -115,7 +145,31 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
(value) { (value) {
if (!isActive) { if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent( context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
hours: displayHours, minutes: value)); hours: displayHours,
minutes: value,
seconds: displaySeconds,
));
}
},
isActive: isActive,
),
const SizedBox(width: 10),
if (isActive)
_buildPickerColumn(
context,
's',
displaySeconds,
60,
_secondsController,
(value) {
if (!isActive) {
context
.read<ScheduleBloc>()
.add(UpdateCountdownTimeEvent(
hours: displayHours,
minutes: displayMinutes,
seconds: value,
));
} }
}, },
isActive: isActive, isActive: isActive,

View File

@ -74,7 +74,9 @@ class BuildScheduleView extends StatelessWidget {
), ),
if (state.scheduleMode == ScheduleModes.countdown || if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching) state.scheduleMode == ScheduleModes.inching)
const CountdownInchingView(), CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20), const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown) if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons( CountdownModeButtons(

View File

@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
} }
Widget _buildDeviceInfoSection() { Widget _buildDeviceInfoSection() {
final isOnlineDevice = device.online != null && device.online!;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50), padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50),
child: Table( child: Table(
@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
'Installation Date and Time:', 'Installation Date and Time:',
formatDateTime( formatDateTime(
DateTime.fromMillisecondsSinceEpoch( DateTime.fromMillisecondsSinceEpoch(
((device.createTime ?? 0) * 1000), (device.createTime ?? 0) * 1000,
), ),
), ),
), ),
@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
), ),
TableRow( TableRow(
children: [ children: [
_buildInfoRow('Status:', 'Online', statusColor: Colors.green), _buildInfoRow(
'Status:',
isOnlineDevice ? 'Online' : 'offline',
statusColor: isOnlineDevice ? Colors.green : Colors.red,
),
_buildInfoRow( _buildInfoRow(
'Last Offline Date and Time:', 'Last Offline Date and Time:',
formatDateTime( formatDateTime(
DateTime.fromMillisecondsSinceEpoch( DateTime.fromMillisecondsSinceEpoch(
((device.updateTime ?? 0) * 1000), (device.updateTime ?? 0) * 1000,
), ),
), ),
), ),

View File

@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
selectedSpaces: updatedSelectedSpaces, selectedSpaces: updatedSelectedSpaces,
soldCheck: updatedSoldChecks, soldCheck: updatedSoldChecks,
selectedCommunityAndSpaces: communityAndSpaces)); selectedCommunityAndSpaces: communityAndSpaces));
emit(state.copyWith(selectedSpaces: updatedSelectedSpaces));
} catch (e) { } catch (e) {
emit(const SpaceTreeErrorState('Something went wrong')); emit(const SpaceTreeErrorState('Something went wrong'));
} }
@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
List<String> _getThePathToChild(String communityId, String selectedSpaceId) { List<String> _getThePathToChild(String communityId, String selectedSpaceId) {
List<String> ids = []; List<String> 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) { if (community.uuid == communityId) {
for (var space in community.spaces) { for (final space in community.spaces) {
List<String> list = []; final list = <String>[];
list.add(space.uuid!); list.add(space.uuid!);
ids = _getAllParentsIds(space, selectedSpaceId, List.from(list)); ids = _getAllParentsIds(space, selectedSpaceId, List.from(list));
if (ids.isNotEmpty) { if (ids.isNotEmpty) {

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
class AccessTypeRadioGroup extends StatelessWidget {
final String? selectedType;
final String? accessTypeSelected;
final Function(String) onTypeSelected;
final VisitorPasswordBloc visitorBloc;
const AccessTypeRadioGroup({
super.key,
required this.selectedType,
required this.accessTypeSelected,
required this.onTypeSelected,
required this.visitorBloc,
});
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
final text = Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.black, fontSize: 13);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
Text('Access Type', style: text),
],
),
const SizedBox(height: 8),
if (size.width < 800)
Column(
children: [
_buildRadioTile(
context,
'Online Password',
selectedType ?? accessTypeSelected,
onTypeSelected,
),
const SizedBox(height: 8),
_buildRadioTile(
context,
'Offline Password',
selectedType ?? accessTypeSelected,
onTypeSelected,
),
],
)
else
Row(
children: [
Expanded(
flex: 2,
child: Row(
children: [
_buildRadioTile(
context,
'Online Password',
selectedType ?? accessTypeSelected,
onTypeSelected,
width: size.width * 0.12,
),
_buildRadioTile(
context,
'Offline Password',
selectedType ?? accessTypeSelected,
onTypeSelected,
width: size.width * 0.12,
),
],
),
),
const Spacer(flex: 2),
],
),
],
);
}
Widget _buildRadioTile(
BuildContext context,
String value,
String? groupValue,
Function(String) onChanged, {
double? width,
}) {
return SizedBox(
width: width,
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text(value,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.black,
fontSize: 13,
)),
value: value,
groupValue: groupValue,
onChanged: (value) {
if (value != null) {
onChanged(value);
if (value == 'Dynamic Password') {
visitorBloc.usageFrequencySelected = '';
}
}
},
),
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
class NameAndEmailFields extends StatelessWidget {
final TextEditingController nameController;
final TextEditingController emailController;
final String? Function(String?)? nameValidator;
final String? Function(String?)? emailValidator;
const NameAndEmailFields({
super.key,
required this.nameController,
required this.emailController,
required this.nameValidator,
required this.emailValidator,
});
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container(
width: size.width,
child: size.width < 800
? Column(
children: [
CustomWebTextField(
validator: nameValidator,
controller: nameController,
isRequired: true,
textFieldName: 'Name',
description: '',
),
const SizedBox(height: 15),
CustomWebTextField(
validator: emailValidator,
controller: emailController,
isRequired: true,
textFieldName: 'Email Address',
description:
'The password will be sent to the visitors email address.',
),
],
)
: Row(
children: [
Expanded(
flex: 2,
child: CustomWebTextField(
validator: nameValidator,
controller: nameController,
isRequired: true,
textFieldName: 'Name',
description: '',
),
),
const Spacer(),
Expanded(
flex: 2,
child: CustomWebTextField(
validator: emailValidator,
controller: emailController,
isRequired: true,
textFieldName: 'Email Address',
description:
'The password will be sent to the visitors email address.',
),
),
const Spacer(),
],
),
);
}
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
class UsageFrequencyRadioGroup extends StatelessWidget {
final String? selectedFrequency;
final String? usageFrequencySelected;
final Function(String) onFrequencySelected;
const UsageFrequencyRadioGroup({
super.key,
required this.selectedFrequency,
required this.usageFrequencySelected,
required this.onFrequencySelected,
});
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
final text = Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.black, fontSize: 13);
return size.width < 600
? Column(
children: [
_buildRadioTile(
context,
'One-Time',
selectedFrequency ?? usageFrequencySelected,
onFrequencySelected,
text: text,
fullWidth: true,
),
const SizedBox(height: 8),
_buildRadioTile(
context,
'Periodic',
selectedFrequency ?? usageFrequencySelected,
onFrequencySelected,
text: text,
fullWidth: true,
),
],
)
: Row(
children: [
_buildRadioTile(
context,
'One-Time',
selectedFrequency ?? usageFrequencySelected,
onFrequencySelected,
width: size.width * 0.12,
text: text,
),
_buildRadioTile(
context,
'Periodic',
selectedFrequency ?? usageFrequencySelected,
onFrequencySelected,
width: size.width * 0.12,
text: text,
),
],
);
}
Widget _buildRadioTile(
BuildContext context,
String value,
String? groupValue,
Function(String) onChanged, {
double? width,
required TextStyle text,
bool fullWidth = false,
}) {
return SizedBox(
width: fullWidth ? double.infinity : width,
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text(value, style: text),
value: value,
groupValue: groupValue,
onChanged: (String? value) {
if (value != null) {
onChanged(value);
}
},
),
);
}
}

View File

@ -9,8 +9,11 @@ import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart'; import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_state.dart';
import 'package:syncrow_web/pages/visitor_password/view/access_type_radio_group.dart';
import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/add_device_dialog.dart';
import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart'; import 'package:syncrow_web/pages/visitor_password/view/repeat_widget.dart';
import 'package:syncrow_web/pages/visitor_password/view/responsive_fields_row.dart';
import 'package:syncrow_web/pages/visitor_password/view/usage_frequency_radio_group.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart'; import 'package:syncrow_web/utils/style.dart';
@ -21,7 +24,10 @@ class VisitorPasswordDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size; Size size = MediaQuery.of(context).size;
var text = Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.black, fontSize: 13); var text = Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Colors.black, fontSize: 13);
return BlocProvider( return BlocProvider(
create: (context) => VisitorPasswordBloc(), create: (context) => VisitorPasswordBloc(),
child: BlocListener<VisitorPasswordBloc, VisitorPasswordState>( child: BlocListener<VisitorPasswordBloc, VisitorPasswordState>(
@ -35,7 +41,8 @@ class VisitorPasswordDialog extends StatelessWidget {
title: 'Sent Successfully', title: 'Sent Successfully',
widgeta: Column( widgeta: Column(
children: [ children: [
if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) if (visitorBloc
.passwordStatus!.failedOperations.isNotEmpty)
Column( Column(
children: [ children: [
const Text('Failed Devices'), const Text('Failed Devices'),
@ -45,7 +52,8 @@ class VisitorPasswordDialog extends StatelessWidget {
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
itemCount: visitorBloc.passwordStatus!.failedOperations.length, itemCount: visitorBloc
.passwordStatus!.failedOperations.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( return Container(
margin: EdgeInsets.all(5), margin: EdgeInsets.all(5),
@ -53,14 +61,17 @@ class VisitorPasswordDialog extends StatelessWidget {
height: 45, height: 45,
child: Center( child: Center(
child: Text(visitorBloc child: Text(visitorBloc
.passwordStatus!.failedOperations[index].deviceUuid)), .passwordStatus!
.failedOperations[index]
.deviceUuid)),
); );
}, },
), ),
), ),
], ],
), ),
if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) if (visitorBloc
.passwordStatus!.successOperations.isNotEmpty)
Column( Column(
children: [ children: [
const Text('Success Devices'), const Text('Success Devices'),
@ -70,15 +81,18 @@ class VisitorPasswordDialog extends StatelessWidget {
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
itemCount: visitorBloc.passwordStatus!.successOperations.length, itemCount: visitorBloc
.passwordStatus!.successOperations.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Container( return Container(
margin: EdgeInsets.all(5), margin: EdgeInsets.all(5),
decoration: containerDecoration, decoration: containerDecoration,
height: 45, height: 45,
child: Center( child: Center(
child: Text(visitorBloc.passwordStatus! child: Text(visitorBloc
.successOperations[index].deviceUuid)), .passwordStatus!
.successOperations[index]
.deviceUuid)),
); );
}, },
), ),
@ -88,8 +102,7 @@ class VisitorPasswordDialog extends StatelessWidget {
], ],
)) ))
.then((v) { .then((v) {
Navigator.of(context).pop(true); Navigator.of(context).pop(v);
}); });
} else if (state is FailedState) { } else if (state is FailedState) {
visitorBloc.stateDialog( visitorBloc.stateDialog(
@ -102,15 +115,16 @@ class VisitorPasswordDialog extends StatelessWidget {
child: BlocBuilder<VisitorPasswordBloc, VisitorPasswordState>( child: BlocBuilder<VisitorPasswordBloc, VisitorPasswordState>(
builder: (BuildContext context, VisitorPasswordState state) { builder: (BuildContext context, VisitorPasswordState state) {
final visitorBloc = BlocProvider.of<VisitorPasswordBloc>(context); final visitorBloc = BlocProvider.of<VisitorPasswordBloc>(context);
bool isRepeat = state is IsRepeatState ? state.repeat : visitorBloc.repeat; bool isRepeat =
state is IsRepeatState ? state.repeat : visitorBloc.repeat;
return AlertDialog( return AlertDialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
title: Text( title: Text(
'Create visitor password', 'Create visitor password',
style: Theme.of(context) style: Theme.of(context).textTheme.headlineLarge!.copyWith(
.textTheme fontWeight: FontWeight.w400,
.headlineLarge! fontSize: 24,
.copyWith(fontWeight: FontWeight.w400, fontSize: 24, color: Colors.black), color: Colors.black),
), ),
content: state is LoadingInitialState content: state is LoadingInitialState
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
@ -121,34 +135,11 @@ class VisitorPasswordDialog extends StatelessWidget {
padding: const EdgeInsets.all(5.0), padding: const EdgeInsets.all(5.0),
child: ListBody( child: ListBody(
children: <Widget>[ children: <Widget>[
Container( NameAndEmailFields(
child: Row( nameController: visitorBloc.userNameController,
children: [ emailController: visitorBloc.emailController,
Expanded( nameValidator: visitorBloc.validate,
flex: 2, emailValidator: visitorBloc.validateEmail,
child: CustomWebTextField(
validator: visitorBloc.validate,
controller: visitorBloc.userNameController,
isRequired: true,
textFieldName: 'Name',
description: '',
),
),
const Spacer(),
Expanded(
flex: 2,
child: CustomWebTextField(
validator: visitorBloc.validateEmail,
controller: visitorBloc.emailController,
isRequired: true,
textFieldName: 'Email Address',
description:
'The password will be sent to the visitors email address.',
),
),
const Spacer(),
],
),
), ),
const SizedBox( const SizedBox(
height: 15, height: 15,
@ -156,104 +147,40 @@ class VisitorPasswordDialog extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( AccessTypeRadioGroup(
children: [ selectedType: state is PasswordTypeSelected
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
Text('Access Type', style: text),
],
),
Row(
children: <Widget>[
Expanded(
flex: 2,
child: Row(
children: [
SizedBox(
width: size.width * 0.12,
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text(
'Online Password',
style: text,
),
value: 'Online Password',
groupValue: (state is PasswordTypeSelected)
? state.selectedType ? state.selectedType
: visitorBloc.accessTypeSelected, : null,
onChanged: (String? value) { accessTypeSelected:
if (value != null) { visitorBloc.accessTypeSelected,
onTypeSelected: (value) {
context context
.read<VisitorPasswordBloc>() .read<VisitorPasswordBloc>()
.add(SelectPasswordType(value)); .add(SelectPasswordType(value));
}
}, },
visitorBloc: visitorBloc,
), ),
),
SizedBox( if (visitorBloc.accessTypeSelected ==
width: size.width * 0.12, 'Online Password')
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text('Offline Password', style: text),
value: 'Offline Password',
groupValue: (state is PasswordTypeSelected)
? state.selectedType
: visitorBloc.accessTypeSelected,
onChanged: (String? value) {
if (value != null) {
context
.read<VisitorPasswordBloc>()
.add(SelectPasswordType(value));
}
},
),
),
// SizedBox(
// width: size.width * 0.12,
// child: RadioListTile<String>(
// contentPadding: EdgeInsets.zero,
// title: Text(
// 'Dynamic Password',
// style: text,
// ),
// value: 'Dynamic Password',
// groupValue: (state is PasswordTypeSelected)
// ? state.selectedType
// : visitorBloc.accessTypeSelected,
// onChanged: (String? value) {
// if (value != null) {
// context
// .read<VisitorPasswordBloc>()
// .add(SelectPasswordType(value));
// visitorBloc.usageFrequencySelected = '';
// }
// },
// ),
// ),
],
)),
const Spacer(
flex: 2,
),
],
),
if (visitorBloc.accessTypeSelected == 'Online Password')
Text( Text(
'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password', 'Only currently online devices can be selected. It is recommended to use when the device network is stable, and the system randomly generates a digital password',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 9), fontSize: 9),
), ),
if (visitorBloc.accessTypeSelected == 'Offline Password') if (visitorBloc.accessTypeSelected ==
'Offline Password')
Text( Text(
'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password', 'Unaffected by the online status of the device, you can select online or offline device, and the system randomly generates a digital password',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 9), fontSize: 9),
@ -271,9 +198,11 @@ class VisitorPasswordDialog extends StatelessWidget {
) )
], ],
), ),
visitorBloc.accessTypeSelected == 'Dynamic Password' if (visitorBloc.accessTypeSelected ==
? const SizedBox() 'Dynamic Password')
: Column( const SizedBox()
else
Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
@ -291,123 +220,148 @@ class VisitorPasswordDialog extends StatelessWidget {
), ),
], ],
), ),
Row( UsageFrequencyRadioGroup(
children: <Widget>[ selectedFrequency:
SizedBox( state is UsageFrequencySelected
width: size.width * 0.12,
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text(
'One-Time',
style: text,
),
value: 'One-Time',
groupValue: (state is UsageFrequencySelected)
? state.selectedFrequency ? state.selectedFrequency
: visitorBloc.usageFrequencySelected, : null,
onChanged: (String? value) { usageFrequencySelected:
if (value != null) { visitorBloc.usageFrequencySelected,
onFrequencySelected: (value) {
context context
.read<VisitorPasswordBloc>() .read<VisitorPasswordBloc>()
.add(SelectUsageFrequency(value)); .add(SelectUsageFrequency(value));
}
}, },
), ),
),
SizedBox(
width: size.width * 0.12,
child: RadioListTile<String>(
contentPadding: EdgeInsets.zero,
title: Text('Periodic', style: text),
value: 'Periodic',
groupValue: (state is UsageFrequencySelected)
? state.selectedFrequency
: visitorBloc.usageFrequencySelected,
onChanged: (String? value) {
if (value != null) {
context.read<VisitorPasswordBloc>()
.add(SelectUsageFrequency(value));
}
},
),
),
],
),
//One-Time //One-Time
if (visitorBloc.usageFrequencySelected == 'One-Time' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Online Password') 'One-Time' &&
visitorBloc.accessTypeSelected ==
'Online Password')
Text( Text(
'Within the validity period, each device can be unlocked only once.', 'Within the validity period, each device can be unlocked only once.',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
color: ColorsManager.grayColor, fontSize: 9), .textTheme
.bodySmall!
.copyWith(
color: ColorsManager.grayColor,
fontSize: 9),
), ),
if (visitorBloc.usageFrequencySelected == 'One-Time' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') 'One-Time' &&
visitorBloc.accessTypeSelected ==
'Offline Password')
Text( Text(
'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours', 'Within the validity period, each device can be unlocked only once, and the maximum validity period is 6 hours',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
color: ColorsManager.grayColor, fontSize: 9), .textTheme
.bodySmall!
.copyWith(
color: ColorsManager.grayColor,
fontSize: 9),
), ),
// Periodic // Periodic
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') 'Periodic' &&
visitorBloc.accessTypeSelected ==
'Offline Password')
Text( Text(
'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.', 'Within the validity period, there is no limit to the number of times each device can be unlocked, and it should be used at least once within 24 hours after the entry into force.',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
color: ColorsManager.grayColor, fontSize: 9), .textTheme
.bodySmall!
.copyWith(
color: ColorsManager.grayColor,
fontSize: 9),
), ),
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Online Password') 'Periodic' &&
visitorBloc.accessTypeSelected ==
'Online Password')
Text( Text(
'Within the validity period, there is no limit to the number of times each device can be unlocked.', 'Within the validity period, there is no limit to the number of times each device can be unlocked.',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
color: ColorsManager.grayColor, fontSize: 9), .textTheme
.bodySmall!
.copyWith(
color: ColorsManager.grayColor,
fontSize: 9),
), ),
], ],
), ),
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
if ((visitorBloc.usageFrequencySelected != 'One-Time' || if ((visitorBloc.usageFrequencySelected !=
visitorBloc.accessTypeSelected != 'Offline Password') && 'One-Time' ||
visitorBloc.accessTypeSelected !=
'Offline Password') &&
(visitorBloc.usageFrequencySelected != '')) (visitorBloc.usageFrequencySelected != ''))
DateTimeWebWidget( DateTimeWebWidget(
isRequired: true, isRequired: true,
title: 'Access Period', title: 'Access Period',
size: size, size: size,
endTime: () { endTime: () {
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') { 'Periodic' &&
visitorBloc.add(SelectTimeEvent(context: context, isEffective: false)); visitorBloc.accessTypeSelected ==
'Offline Password') {
visitorBloc.add(SelectTimeEvent(
context: context,
isEffective: false));
} else { } else {
visitorBloc.add(SelectTimeVisitorPassword(context: context, isStart: false, isRepeat: false)); visitorBloc.add(
SelectTimeVisitorPassword(
context: context,
isStart: false,
isRepeat: false));
} }
}, },
startTime: () { startTime: () {
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') { 'Periodic' &&
visitorBloc.add( visitorBloc.accessTypeSelected ==
SelectTimeEvent(context: context, isEffective: true)); 'Offline Password') {
visitorBloc.add(SelectTimeEvent(
context: context,
isEffective: true));
} else { } else {
visitorBloc.add(SelectTimeVisitorPassword( visitorBloc.add(
context: context, isStart: true, isRepeat: false)); SelectTimeVisitorPassword(
context: context,
isStart: true,
isRepeat: false));
} }
}, },
firstString: (visitorBloc.usageFrequencySelected == firstString: (visitorBloc
'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') .usageFrequencySelected ==
'Periodic' &&
visitorBloc.accessTypeSelected ==
'Offline Password')
? visitorBloc.effectiveTime ? visitorBloc.effectiveTime
: visitorBloc.startTimeAccess.toString(), : visitorBloc.startTimeAccess
secondString: (visitorBloc.usageFrequencySelected == .toString(),
'Periodic' && visitorBloc.accessTypeSelected == 'Offline Password') secondString: (visitorBloc
.usageFrequencySelected ==
'Periodic' &&
visitorBloc.accessTypeSelected ==
'Offline Password')
? visitorBloc.expirationTime ? visitorBloc.expirationTime
: visitorBloc.endTimeAccess.toString(), : visitorBloc.endTimeAccess.toString(),
icon: Assets.calendarIcon), icon: Assets.calendarIcon),
const SizedBox(height: 10,), const SizedBox(
Text(visitorBloc.accessPeriodValidate, height: 10,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: ColorsManager.red),), ),
Text(
visitorBloc.accessPeriodValidate,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: ColorsManager.red),
),
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
@ -431,7 +385,10 @@ class VisitorPasswordDialog extends StatelessWidget {
), ),
Text( Text(
'Within the validity period, each device can be unlocked only once.', 'Within the validity period, each device can be unlocked only once.',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 9), fontSize: 9),
@ -439,8 +396,10 @@ class VisitorPasswordDialog extends StatelessWidget {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Online Password') 'Periodic' &&
visitorBloc.accessTypeSelected ==
'Online Password')
SizedBox( SizedBox(
width: 100, width: 100,
child: Column( child: Column(
@ -451,7 +410,8 @@ class VisitorPasswordDialog extends StatelessWidget {
child: CupertinoSwitch( child: CupertinoSwitch(
value: visitorBloc.repeat, value: visitorBloc.repeat,
onChanged: (value) { onChanged: (value) {
visitorBloc.add(ToggleRepeatEvent()); visitorBloc
.add(ToggleRepeatEvent());
}, },
applyTheme: true, applyTheme: true,
), ),
@ -459,12 +419,16 @@ class VisitorPasswordDialog extends StatelessWidget {
], ],
), ),
), ),
if (visitorBloc.usageFrequencySelected == 'Periodic' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Online Password') 'Periodic' &&
isRepeat ? const RepeatWidget() : const SizedBox(), visitorBloc.accessTypeSelected ==
'Online Password')
isRepeat
? const RepeatWidget()
: const SizedBox(),
Container( Container(
decoration: containerDecoration, decoration: containerDecoration,
width: size.width / 9, width: size.width / 6,
child: DefaultButton( child: DefaultButton(
onPressed: () { onPressed: () {
showDialog( showDialog(
@ -472,21 +436,27 @@ class VisitorPasswordDialog extends StatelessWidget {
barrierDismissible: false, barrierDismissible: false,
builder: (BuildContext context) { builder: (BuildContext context) {
return AddDeviceDialog( return AddDeviceDialog(
selectedDeviceIds: visitorBloc.selectedDevices, selectedDeviceIds:
visitorBloc.selectedDevices,
); );
}, },
).then((listDevice) { ).then((listDevice) {
if (listDevice != null) { if (listDevice != null) {
visitorBloc.selectedDevices = listDevice; visitorBloc.selectedDevices =
listDevice;
} }
}); });
}, },
borderRadius: 8, borderRadius: 8,
child: Text( child: Text(
'+ Add Device', '+ Add Device',
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.whiteColors, color:
ColorsManager.whiteColors,
fontSize: 12), fontSize: 12),
), ),
), ),
@ -506,7 +476,7 @@ class VisitorPasswordDialog extends StatelessWidget {
child: DefaultButton( child: DefaultButton(
borderRadius: 8, borderRadius: 8,
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(null);
}, },
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: Text( child: Text(
@ -525,30 +495,37 @@ class VisitorPasswordDialog extends StatelessWidget {
onPressed: () { onPressed: () {
if (visitorBloc.forgetFormKey.currentState!.validate()) { if (visitorBloc.forgetFormKey.currentState!.validate()) {
if (visitorBloc.selectedDevices.isNotEmpty) { if (visitorBloc.selectedDevices.isNotEmpty) {
if (visitorBloc.usageFrequencySelected == 'One-Time' && if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') { 'One-Time' &&
visitorBloc.accessTypeSelected ==
'Offline Password') {
setPasswordFunction(context, size, visitorBloc); setPasswordFunction(context, size, visitorBloc);
} else if (visitorBloc.usageFrequencySelected == 'Periodic' && } else if (visitorBloc.usageFrequencySelected ==
visitorBloc.accessTypeSelected == 'Offline Password') { 'Periodic' &&
visitorBloc.accessTypeSelected ==
'Offline Password') {
if (visitorBloc.expirationTime != 'End Time' && if (visitorBloc.expirationTime != 'End Time' &&
visitorBloc.effectiveTime != 'Start Time') { visitorBloc.effectiveTime != 'Start Time') {
setPasswordFunction(context, size, visitorBloc); setPasswordFunction(context, size, visitorBloc);
} else { } else {
visitorBloc.stateDialog( visitorBloc.stateDialog(
context: context, context: context,
message: 'Please select Access Period to continue', message:
'Please select Access Period to continue',
title: 'Access Period'); title: 'Access Period');
} }
} else if( } else if (visitorBloc.endTimeAccess.toString() !=
visitorBloc.endTimeAccess.toString()!='End Time' 'End Time' &&
&&visitorBloc.startTimeAccess.toString()!='Start Time') { visitorBloc.startTimeAccess.toString() !=
'Start Time') {
if (visitorBloc.effectiveTimeTimeStamp != null && if (visitorBloc.effectiveTimeTimeStamp != null &&
visitorBloc.expirationTimeTimeStamp != null) { visitorBloc.expirationTimeTimeStamp != null) {
if (isRepeat == true) { if (isRepeat == true) {
if (visitorBloc.expirationTime != 'End Time' && if (visitorBloc.expirationTime != 'End Time' &&
visitorBloc.effectiveTime != 'Start Time' && visitorBloc.effectiveTime != 'Start Time' &&
visitorBloc.selectedDays.isNotEmpty) { visitorBloc.selectedDays.isNotEmpty) {
setPasswordFunction(context, size, visitorBloc); setPasswordFunction(
context, size, visitorBloc);
} else { } else {
visitorBloc.stateDialog( visitorBloc.stateDialog(
context: context, context: context,
@ -562,13 +539,15 @@ class VisitorPasswordDialog extends StatelessWidget {
} else { } else {
visitorBloc.stateDialog( visitorBloc.stateDialog(
context: context, context: context,
message: 'Please select Access Period to continue', message:
'Please select Access Period to continue',
title: 'Access Period'); title: 'Access Period');
} }
} else { } else {
visitorBloc.stateDialog( visitorBloc.stateDialog(
context: context, context: context,
message: 'Please select Access Period to continue', message:
'Please select Access Period to continue',
title: 'Access Period'); title: 'Access Period');
} }
} else { } else {
@ -615,7 +594,8 @@ class VisitorPasswordDialog extends StatelessWidget {
content: SizedBox( content: SizedBox(
height: size.height * 0.25, height: size.height * 0.25,
child: Center( child: Center(
child: CircularProgressIndicator(), // Display a loading spinner child:
CircularProgressIndicator(), // Display a loading spinner
), ),
), ),
); );
@ -639,7 +619,10 @@ class VisitorPasswordDialog extends StatelessWidget {
), ),
Text( Text(
'Set Password', 'Set Password',
style: Theme.of(context).textTheme.headlineLarge!.copyWith( style: Theme.of(context)
.textTheme
.headlineLarge!
.copyWith(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Colors.black, color: Colors.black,
@ -668,7 +651,7 @@ class VisitorPasswordDialog extends StatelessWidget {
child: DefaultButton( child: DefaultButton(
borderRadius: 8, borderRadius: 8,
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop(null);
}, },
backgroundColor: Colors.white, backgroundColor: Colors.white,
child: Text( child: Text(
@ -689,37 +672,45 @@ class VisitorPasswordDialog extends StatelessWidget {
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
if (visitorBloc.usageFrequencySelected == 'One-Time' && if (visitorBloc.usageFrequencySelected == 'One-Time' &&
visitorBloc.accessTypeSelected == 'Online Password') { visitorBloc.accessTypeSelected ==
'Online Password') {
visitorBloc.add(OnlineOneTimePasswordEvent( visitorBloc.add(OnlineOneTimePasswordEvent(
context: context, context: context,
passwordName: visitorBloc.userNameController.text, passwordName: visitorBloc.userNameController.text,
email: visitorBloc.emailController.text, email: visitorBloc.emailController.text,
)); ));
} } else if (visitorBloc.usageFrequencySelected ==
else if (visitorBloc.usageFrequencySelected == 'Periodic' && 'Periodic' &&
visitorBloc.accessTypeSelected == 'Online Password') { visitorBloc.accessTypeSelected ==
'Online Password') {
visitorBloc.add(OnlineMultipleTimePasswordEvent( visitorBloc.add(OnlineMultipleTimePasswordEvent(
passwordName: visitorBloc.userNameController.text, passwordName: visitorBloc.userNameController.text,
email: visitorBloc.emailController.text, email: visitorBloc.emailController.text,
effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), effectiveTime:
invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), visitorBloc.effectiveTimeTimeStamp.toString(),
invalidTime:
visitorBloc.expirationTimeTimeStamp.toString(),
)); ));
} } else if (visitorBloc.usageFrequencySelected ==
else if (visitorBloc.usageFrequencySelected == 'One-Time' && 'One-Time' &&
visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.accessTypeSelected ==
'Offline Password') {
visitorBloc.add(OfflineOneTimePasswordEvent( visitorBloc.add(OfflineOneTimePasswordEvent(
context: context, context: context,
passwordName: visitorBloc.userNameController.text, passwordName: visitorBloc.userNameController.text,
email: visitorBloc.emailController.text, email: visitorBloc.emailController.text,
)); ));
} } else if (visitorBloc.usageFrequencySelected ==
else if (visitorBloc.usageFrequencySelected == 'Periodic' && 'Periodic' &&
visitorBloc.accessTypeSelected == 'Offline Password') { visitorBloc.accessTypeSelected ==
'Offline Password') {
visitorBloc.add(OfflineMultipleTimePasswordEvent( visitorBloc.add(OfflineMultipleTimePasswordEvent(
passwordName: visitorBloc.userNameController.text, passwordName: visitorBloc.userNameController.text,
email: visitorBloc.emailController.text, email: visitorBloc.emailController.text,
effectiveTime: visitorBloc.effectiveTimeTimeStamp.toString(), effectiveTime:
invalidTime: visitorBloc.expirationTimeTimeStamp.toString(), visitorBloc.effectiveTimeTimeStamp.toString(),
invalidTime:
visitorBloc.expirationTimeTimeStamp.toString(),
)); ));
} }
}, },