Compare commits

..

35 Commits

Author SHA1 Message Date
0a424300aa Refactor ScheduleBloc and related components to use dynamic category handling for schedule events 2025-06-22 12:46:54 +03:00
ec12b970b0 Refactor schedule components and update imports for garage door and w… (#271)
…ater heater modules

<!--
  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-1620](https://syncrow.atlassian.net/browse/SP-1620)

## Description

<!--- Describe your changes in detail -->
Refactor schedule components and make this fetcher as a reusable and fix
bugs related to it

## 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-1620]:
https://syncrow.atlassian.net/browse/SP-1620?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 12:27:03 +03:00
d2713c5902 Add ScheduleControlButton widget and integrate it into water heater and wall light device controls 2025-06-22 12:23:09 +03:00
5da25d8ecb Sp 1612 fe user cannot see the horizontal scroll on any of the tables they have to hover over it but it s not obvious that they can do that (#274)
<!--
  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-1612](https://syncrow.atlassian.net/browse/SP-1612)

## Description

PROBLEM IS SOLVED before but i added comment to insure that 

## 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-1612]:
https://syncrow.atlassian.net/browse/SP-1612?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 08:43:13 +03:00
8cf73e3efc Enhance scheduling UI in glass switch control views with improved layout and dialog integration 2025-06-19 16:38:45 +03:00
0b774a6dfc Add scheduling category parameter to BuildScheduleView and update device control dialogs 2025-06-19 16:20:46 +03:00
2267d95795 Add schedule saving functionality and update schedule events 2025-06-19 15:46:40 +03:00
5b5a94cf65 analytics hotfixes. (#275)
<!--
  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

analytics hotfixes.

## 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-19 11:20:18 +03:00
d45fa4c957 analytics hotfixes. 2025-06-19 11:12:12 +03:00
ed2a8f6ba2 Refactor border radius implementation in ScheduleGarageTableWidget for consistency 2025-06-19 11:02:23 +03:00
d895ed74d2 Add scheduling functionality to device control views with dialog integration 2025-06-19 10:49:06 +03:00
e39c6abd32 show curtain in devices and implement dialog for if and then (#263)
last integrate with backend

<!--
  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-1728](https://syncrow.atlassian.net/browse/SP-1728)

## Description

implement the dialog for CURTAIN and make it appears with devices in
making Routine
integrate it with backend and test it 
## Type of Change

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

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


[SP-1728]:
https://syncrow.atlassian.net/browse/SP-1728?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-19 10:45:06 +03:00
fc6ea640a7 Merge branch 'dev' into SP-1612-FE-User-cannot-see-the-horizontal-scroll-on-any-of-the-tables-they-have-to-hover-over-it-but-it-s-not-obvious-that-they-can-do-that 2025-06-19 10:40:46 +03:00
09c44f8a5f add comment for problem solve 2025-06-19 09:33:45 +03:00
c178c36824 remove duplicate feature (#272)
<!--
  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
there is no ticket for this hot edit

## Description

remove Duplicate Feature in space 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-19 09:05:03 +03:00
ce96afd7af PR fixes 2025-06-19 09:03:24 +03:00
27dfa0a05a remove duplicate feature 2025-06-19 08:56:41 +03:00
3d95f2bef0 Fix null safety issue by adding null check for functionOn in schedule dialog helper 2025-06-18 16:40:13 +03:00
db513f916f Refactor schedule components and update imports for garage door and water heater modules 2025-06-18 16:27:50 +03:00
78979a4375 SP-1661-fe-enhance-the-landing-page-to-be-responsive-and-look-like-design_again (#266)
<!--
  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:1661](https://syncrow.atlassian.net/browse/SP-1661)
## Description

enhance UI in landing page

## 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-18 15:54:46 +03:00
ea19387605 Hotfix/communities loading (#269)
<!--
  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

Hotfix/ communities loading v2.

## 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-18 15:29:18 +03:00
5b33a8617e Merge branch 'dev' of https://github.com/SyncrowIOT/web into hotfix/communities_loading 2025-06-18 15:25:43 +03:00
34565a7dab hotfix/communities_loading v2. 2025-06-18 15:25:32 +03:00
caf1ff5c7e Fix energy device condition and community and space dialog (#268)
<!--
  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 -->
fix energy dialog and fix reset value in text form and fix create dialog
routine

## 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-18 15:01:08 +03:00
01e8002c43 fix: adjust spacing in create new routines dialog for improved layout 2025-06-18 14:55:12 +03:00
63da660ece refactor: update function handling in routine dialogs 2025-06-18 14:40:25 +03:00
567d0e2d20 hotfix/communities_loading (#267)
<!--
  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

hotfix/ loading communities.
## Type of Change

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

- [ ]  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
2025-06-18 14:37:00 +03:00
45e6ea3259 hotfix/communities_loading 2025-06-18 14:33:39 +03:00
e942957a47 enhance it Done and yazan has watched it 2025-06-18 12:49:58 +03:00
b9a3b9c719 fix: update dropdown styles and dimensions for better UI consistency 2025-06-18 12:14:17 +03:00
f5500dfe50 bug fixed it is locally change the state now (#264)
<!--
  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-SP-1413](https://syncrow.atlassian.net/jira/software/projects/SP/boards/5?assignee=712020%3A71e88a7f-7752-44b3-8177-4ab51a950811&selectedIssue=SP-1413)

## Description

state now changes without fetching API it is locallly

## 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-18 09:51:06 +03:00
1ba1aba54e PR fixes and tested 2025-06-18 08:42:18 +03:00
09f2123946 bug fixed it is locally change the state now 2025-06-17 16:21:01 +03:00
056a1daadc show curtain in devices and implement dialog for if and then
last integrate with backend
2025-06-17 13:34:23 +03:00
584845ffdc fix horizontal scroll bar 2025-05-22 04:52:23 -05:00
114 changed files with 3515 additions and 2999 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

View File

@ -1,10 +0,0 @@
import 'package:flutter/material.dart';
class AppLoadingIndicator extends StatelessWidget {
const AppLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -59,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_dev.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -59,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -7,10 +7,10 @@ import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
@ -56,7 +56,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -64,7 +64,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(

View File

@ -31,6 +31,8 @@ import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_o
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
@ -130,9 +132,19 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
}
}
class AnalyticsPageForm extends StatelessWidget {
class AnalyticsPageForm extends StatefulWidget {
const AnalyticsPageForm({super.key});
@override
State<AnalyticsPageForm> createState() => _AnalyticsPageFormState();
}
class _AnalyticsPageFormState extends State<AnalyticsPageForm> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return WebScaffold(

View File

@ -46,7 +46,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
spacing: 32,
children: [
Expanded(
flex: 2,
flex: 7,
child: Column(
spacing: 20,
children: [
@ -55,7 +55,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
],
),
),
Expanded(child: PowerClampEnergyDataWidget()),
Expanded(flex: 4, child: PowerClampEnergyDataWidget()),
],
),
),

View File

@ -31,12 +31,12 @@ class AnalyticsOccupancyView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 0.9,
height: height * 1,
child: const Row(
spacing: 32,
children: [
Expanded(
flex: 5,
flex: 7,
child: Column(
spacing: 20,
children: [
@ -45,7 +45,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
],
),
),
Expanded(flex: 2, child: OccupancyEndSideBar()),
Expanded(flex: 4, child: OccupancyEndSideBar()),
],
),
),

View File

@ -24,37 +24,45 @@ class OccupancyEndSideBar extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
Expanded(
child: SizedBox(
// height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
),
unit: '',
),
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
),
),
),
const SizedBox(height: 20),
Expanded(
flex: 2,
child: FittedBox(
child: Image.asset(Assets.autocadOccupancyImage),
),
),
],
),
);

View File

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

View File

@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_tag
import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/flush/flush_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/one_gang_switch.dart';
@ -359,6 +360,14 @@ SOS
uuid: uuid ?? '',
name: name ?? '',
);
case 'CUR':
return [
ControlCurtainFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'BOTH',
)
];
case 'NCPS':
return [
FlushPresenceDelayFunction(
@ -441,15 +450,10 @@ SOS
VoltageCStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PowerFactorCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
];
default:
return [];
}

View File

@ -8,15 +8,28 @@ import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routi
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routines/view/routines_view.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
const DeviceManagementPage({super.key});
@override
State<DeviceManagementPage> createState() => _DeviceManagementPageState();
}
class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
class BuildScheduleView extends StatelessWidget {
const BuildScheduleView(
{super.key, required this.deviceUuid, required this.category});
final String deviceUuid;
final String category;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(deviceUuid)),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: 700,
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20),
child: BlocBuilder<ScheduleBloc, ScheduleState>(
builder: (context, state) {
if (state is ScheduleLoaded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ScheduleHeader(),
const SizedBox(height: 20),
ScheduleModeSelector(
currentMode: state.scheduleMode,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule)
ScheduleManagementUI(
category: category,
deviceUuid: deviceUuid,
onAddSchedule: () async {
final entry = await ScheduleDialogHelper
.showAddScheduleDialog(
context,
schedule: null,
isEdit: false,
);
if (entry != null) {
context.read<ScheduleBloc>().add(
ScheduleAddEvent(
category: entry.category,
time: entry.time,
functionOn: entry.function.value,
selectedDays: entry.days,
),
);
}
},
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
const CountdownInchingView(),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
isActive: state.isCountdownActive,
deviceId: deviceUuid,
hours: state.countdownHours,
minutes: state.countdownMinutes,
),
if (state.scheduleMode == ScheduleModes.inching)
InchingModeButtons(
isActive: state.isInchingActive,
deviceId: deviceUuid,
hours: state.inchingHours,
minutes: state.inchingMinutes,
),
if (state.scheduleMode != ScheduleModes.countdown &&
state.scheduleMode != ScheduleModes.inching)
ScheduleModeButtons(
onSave: () => Navigator.pop(context),
),
],
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,223 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CountdownInchingView extends StatelessWidget {
final WaterHeaterDeviceStatusLoaded state;
const CountdownInchingView({
super.key,
required this.state,
});
@override
Widget build(BuildContext context) {
final isCountDown =
state.scheduleMode?.name == ScheduleModes.countdown.name;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isCountDown ? 'Countdown:' : 'Inching:',
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
const SizedBox(height: 8),
Visibility(
visible: !isCountDown,
child: const Text(
'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'),
),
const SizedBox(height: 8),
_hourMinutesWheel(context, state),
],
);
}
Row _hourMinutesWheel(
BuildContext context, WaterHeaterDeviceStatusLoaded state) {
final isCountDown =
state.scheduleMode?.name == ScheduleModes.countdown.name;
late bool isActive;
if (isCountDown &&
state.countdownRemaining != null &&
state.isCountdownActive == true) {
isActive = true;
} else if (!isCountDown &&
state.countdownRemaining != null &&
state.isInchingActive == true) {
isActive = true;
} else {
isActive = false;
}
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildPickerColumn(
context,
'h',
isCountDown
? (state.countdownHours ?? 0)
: (state.inchingHours ?? 0),
24, (value) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: state.scheduleMode ?? ScheduleModes.countdown,
hours: value,
minutes: isCountDown
? (state.countdownMinutes ?? 0)
: (state.inchingMinutes ?? 0),
));
}, isActive: isActive),
const SizedBox(width: 10),
_buildPickerColumn(
context,
'm',
isCountDown
? (state.countdownMinutes ?? 0)
: (state.inchingMinutes ?? 0),
60, (value) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: state.scheduleMode ?? ScheduleModes.countdown,
hours: isCountDown
? (state.countdownHours ?? 0)
: (state.inchingHours ?? 0),
minutes: value,
));
}, isActive: isActive),
],
);
}
Row _hourMinutesSecondWheel(
BuildContext context, WaterHeaterDeviceStatusLoaded state) {
final isCountDown =
state.scheduleMode?.name == ScheduleModes.countdown.name;
late bool isActive;
if (isCountDown &&
state.countdownRemaining != null &&
state.isCountdownActive == true) {
isActive = true;
} else if (!isCountDown &&
state.countdownRemaining != null &&
state.isInchingActive == true) {
isActive = true;
} else {
isActive = false;
}
return Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
_buildPickerColumn(
context,
'h',
isCountDown
? (state.countdownHours ?? 0)
: (state.inchingHours ?? 0),
24, (value) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: state.scheduleMode ?? ScheduleModes.countdown,
hours: value,
minutes: isCountDown
? (state.countdownMinutes ?? 0)
: (state.inchingMinutes ?? 0),
));
}, isActive: isActive),
const SizedBox(width: 10),
_buildPickerColumn(
context,
'm',
isCountDown
? (state.countdownMinutes ?? 0)
: (state.inchingMinutes ?? 0),
60, (value) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: state.scheduleMode ?? ScheduleModes.countdown,
hours: isCountDown
? (state.countdownHours ?? 0)
: (state.inchingHours ?? 0),
minutes: value,
));
}, isActive: isActive),
const SizedBox(width: 10),
_buildPickerColumn(
context,
'S',
isCountDown
? (state.countdownMinutes ?? 0)
: (state.inchingMinutes ?? 0),
60, (value) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: state.scheduleMode ?? ScheduleModes.countdown,
hours: isCountDown
? (state.countdownHours ?? 0)
: (state.inchingHours ?? 0),
minutes: value,
));
}, isActive: isActive),
],
);
}
Widget _buildPickerColumn(
BuildContext context,
String label,
int initialValue,
int itemCount,
ValueChanged<int> onSelected, {
required bool isActive,
}) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 40,
width: 80,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(8),
),
child: ListWheelScrollView.useDelegate(
key: ValueKey('$label-$initialValue'),
controller: FixedExtentScrollController(
initialItem: initialValue,
),
itemExtent: 40.0,
physics: const FixedExtentScrollPhysics(),
onSelectedItemChanged: onSelected,
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
return Center(
child: Text(
index.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 24,
color: isActive ? ColorsManager.grayColor : Colors.black,
),
),
);
},
childCount: itemCount,
),
),
),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 18,
),
),
],
);
}
}

View File

@ -1,117 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart';
class BuildScheduleView extends StatefulWidget {
const BuildScheduleView({super.key, required this.status});
final WaterHeaterStatusModel status;
@override
State<BuildScheduleView> createState() => _BuildScheduleViewState();
}
class _BuildScheduleViewState extends State<BuildScheduleView> {
@override
Widget build(BuildContext context) {
final bloc = BlocProvider.of<WaterHeaterBloc>(context);
return BlocProvider.value(
value: bloc,
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: 700,
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20),
child: BlocBuilder<WaterHeaterBloc, WaterHeaterState>(
builder: (context, state) {
if (state is WaterHeaterDeviceStatusLoaded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ScheduleHeader(),
const SizedBox(height: 20),
ScheduleModeSelector(state: state),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule)
ScheduleManagementUI(
state: state,
onAddSchedule: () {
ScheduleDialogHelper.showAddScheduleDialog(
context,
schedule: null,
index: null,
isEdit: false);
},
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(state: state),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
isActive: state.isCountdownActive ?? false,
deviceId: widget.status.uuid,
hours: state.countdownHours ?? 0,
minutes: state.countdownMinutes ?? 0,
),
if (state.scheduleMode == ScheduleModes.inching)
InchingModeButtons(
isActive: state.isInchingActive ?? false,
deviceId: widget.status.uuid,
hours: state.inchingHours ?? 0,
minutes: state.inchingMinutes ?? 0,
),
if (state.scheduleMode != ScheduleModes.countdown &&
state.scheduleMode != ScheduleModes.inching)
ScheduleModeButtons(
onSave: () {
Navigator.pop(context);
},
),
],
);
}
if (state is WaterHeaterLoadingState) {
return const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ScheduleHeader(),
SizedBox(
height: 20,
),
Center(child: CircularProgressIndicator()),
],
));
}
return const SizedBox(
height: 200,
child: ScheduleHeader(),
);
},
),
),
),
),
),
);
}
}

View File

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

View File

@ -1,222 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/format_date_time.dart';
import '../helper/add_schedule_dialog_helper.dart';
class ScheduleTableWidget extends StatelessWidget {
final WaterHeaterDeviceStatusLoaded state;
const ScheduleTableWidget({
super.key,
required this.state,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Table(
border: TableBorder.all(
color: ColorsManager.graysColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20), topRight: Radius.circular(20)),
),
children: [
TableRow(
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
children: [
_buildTableHeader('Active'),
_buildTableHeader('Days'),
_buildTableHeader('Time'),
_buildTableHeader('Function'),
_buildTableHeader('Action'),
],
),
],
),
BlocBuilder<WaterHeaterBloc, WaterHeaterState>(
builder: (context, state) {
if (state is ScheduleLoadingState) {
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()));
}
if (state is WaterHeaterDeviceStatusLoaded &&
state.schedules.isEmpty) {
return _buildEmptyState(context);
} else if (state is WaterHeaterDeviceStatusLoaded) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20)),
),
child: _buildTableBody(state, context));
}
return const SizedBox(
height: 200,
);
},
),
],
);
}
Widget _buildEmptyState(BuildContext context) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.graysColor),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)),
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'No schedules added yet',
style: context.textTheme.bodySmall!.copyWith(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
),
],
),
),
);
}
Widget _buildTableBody(
WaterHeaterDeviceStatusLoaded state, BuildContext context) {
return SizedBox(
height: 200,
child: SingleChildScrollView(
child: Table(
border: TableBorder.all(color: ColorsManager.graysColor),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
for (int i = 0; i < state.schedules.length; i++)
_buildScheduleRow(state.schedules[i], i, context, state),
],
),
),
);
}
Widget _buildTableHeader(String label) {
return TableCell(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
label,
style: const TextStyle(
fontSize: 13,
color: ColorsManager.grayColor,
),
),
),
);
}
TableRow _buildScheduleRow(ScheduleModel schedule, int index,
BuildContext context, WaterHeaterDeviceStatusLoaded state) {
return TableRow(
children: [
Center(
child: GestureDetector(
onTap: () {
context.read<WaterHeaterBloc>().add(UpdateScheduleEntryEvent(
index: index,
enable: !schedule.enable,
scheduleId: schedule.scheduleId,
deviceId: state.status.uuid,
functionOn: schedule.function.value,
));
},
child: SizedBox(
width: 24,
height: 24,
child: schedule.enable
? const Icon(Icons.radio_button_checked,
color: ColorsManager.blueColor)
: const Icon(
Icons.radio_button_unchecked,
color: ColorsManager.grayColor,
),
),
),
),
Center(
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
child: Wrap(
runAlignment: WrapAlignment.center,
children: [
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
ScheduleDialogHelper.showAddScheduleDialog(context,
schedule: schedule, index: index, isEdit: true);
},
child: Text(
'Edit',
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
context.read<WaterHeaterBloc>().add(DeleteScheduleEvent(
index: index,
scheduleId: schedule.scheduleId,
));
},
child: Text(
'Delete',
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
],
),
),
],
);
}
String _getSelectedDays(List<bool> selectedDays) {
final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
List<String> selectedDaysStr = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) {
selectedDaysStr.add(days[i]);
}
}
return selectedDaysStr.join(', ');
}
}

View File

@ -34,17 +34,9 @@ class HomeCard extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
name,
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Expanded(
child: SpliteNameHelperWidget(
name: name,
),
),
],
@ -63,3 +55,72 @@ class HomeCard extends StatelessWidget {
);
}
}
class SpliteNameHelperWidget extends StatelessWidget {
final String name;
const SpliteNameHelperWidget({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
List<String> parts = name.split(' ');
if (parts.length == 2) {
// Two-word string
return Padding(
padding: const EdgeInsetsGeometry.only(top: 10, left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
parts[0],
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
parts[1],
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
} else {
// One-word string
return Text(
name,
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}
}
}
// Text(
// name,
// style: const TextStyle(
// fontSize: 32,
// color: Colors.white,
// fontWeight: FontWeight.bold,
// ),
// )

View File

@ -13,19 +13,25 @@ class HomePage extends StatefulWidget {
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with HelperResponsiveLayout{
class _HomePageState extends State<HomePage> with HelperResponsiveLayout {
@override
void initState() {
context.read<HomeBloc>().add(const FetchUserInfo());
_fetchUserInfo();
super.initState();
}
@override
Widget build(BuildContext context) {
final isSmallScreen = isSmallScreenSize(context);
final isMediumScreen = isMediumScreenSize(context);
return isSmallScreen || isMediumScreen
? HomeMobilePage()
: const HomeWebPage();
if (isSmallScreenSize(context) || isMediumScreenSize(context)) {
return HomeMobilePage();
}
return const HomeWebPage();
}
void _fetchUserInfo() {
final bloc = context.read<HomeBloc>();
if (bloc.user == null) bloc.add(const FetchUserInfo());
}
}

View File

@ -92,7 +92,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
flex: 4,
child: SizedBox(
height: size.height * 0.6,
width: size.width * 0.68,
width: size.width * 0.8,
child: GridView.builder(
itemCount: homeBloc.homeItems.length,
gridDelegate:

View File

@ -13,6 +13,35 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
on<AddFunction>(_onAddFunction);
on<SelectFunction>(_onSelectFunction);
}
// void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
// final functions = List<DeviceFunctionData>.from(state.addedFunctions);
// final existingIndex = functions.indexWhere(
// (f) => f.functionCode == event.functionData.functionCode,
// );
// if (existingIndex != -1) {
// final existingData = functions[existingIndex];
// functions[existingIndex] = DeviceFunctionData(
// entityId: event.functionData.entityId,
// functionCode: event.functionData.functionCode,
// operationName: event.functionData.operationName,
// value: event.functionData.value ?? existingData.value,
// valueDescription: event.functionData.valueDescription ??
// existingData.valueDescription,
// condition: event.functionData.condition ?? existingData.condition,
// step: event.functionData.step ?? existingData.step,
// );
// } else {
// functions.clear();
// functions.add(event.functionData);
// }
// emit(state.copyWith(
// addedFunctions: functions,
// selectedFunction: event.functionData.functionCode,
// ));
// }
void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
final functions = List<DeviceFunctionData>.from(state.addedFunctions);
final existingIndex = functions.indexWhere(
@ -20,19 +49,10 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
);
if (existingIndex != -1) {
final existingData = functions[existingIndex];
functions[existingIndex] = DeviceFunctionData(
entityId: event.functionData.entityId,
functionCode: event.functionData.functionCode,
operationName: event.functionData.operationName,
value: event.functionData.value ?? existingData.value,
valueDescription: event.functionData.valueDescription ??
existingData.valueDescription,
condition: event.functionData.condition ?? existingData.condition,
step: event.functionData.step ?? existingData.step,
);
// Update the function value
functions[existingIndex] = event.functionData;
} else {
functions.clear();
// Add new function value
functions.add(event.functionData);
}

View File

@ -1419,15 +1419,17 @@ Future<void> _onLoadScenes(
event.automationId, event.automationStatusUpdate, projectId);
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// await SceneApi.getAutomationByUnitId(
// event.automationStatusUpdate.spaceUuid,
// event.communityId,
// projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
final updatedAutomations = changeItemStateOnToggelingSceen(
state.automations, event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
@ -1449,4 +1451,24 @@ Future<void> _onLoadScenes(
));
}
}
List<ScenesModel> changeItemStateOnToggelingSceen(
List<ScenesModel> oldSceen, String automationId) {
return oldSceen.map((scene) {
if (scene.id == automationId) {
return ScenesModel(
id: scene.id,
sceneTuyaId: scene.sceneTuyaId,
name: scene.name,
status: scene.status == 'enable' ? 'disable' : 'enable',
type: scene.type,
spaceName: scene.spaceName,
spaceId: scene.spaceId,
communityId: scene.communityId,
icon: scene.icon,
);
}
return scene;
}).toList();
}
}

View File

@ -117,7 +117,7 @@ class _DropdownContentState extends State<_DropdownContent> {
final selectedCommunity = _findCommunity(state, state.selectedSpaceId);
return Container(
height: 46,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
@ -149,7 +149,7 @@ class _DropdownContentState extends State<_DropdownContent> {
),
),
height: 45,
width: 33,
width: 44,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,

View File

@ -44,144 +44,156 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
_selectedSpace = null;
_selectedCommunity = _selectedId;
}
return AlertDialog(
return Dialog(
backgroundColor: Colors.white,
insetPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero,
insetPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
title: Text(
'Create New Routines',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
content: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Padding(
padding:
const EdgeInsets.only(left: 13, right: 8),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
child: Container(
width: 450,
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 20),
Text(
'Create New Routines',
textAlign: TextAlign.center,
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 13, right: 10),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
onChanged: (String? newValue) {
setState(
() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(
SpaceOnlyWithDevicesEvent(
_selectedId!));
}
},
),
],
)),
const SizedBox(height: 21),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(
_selectedId!));
}
setState(() {
_selectedSpace = newValue;
});
},
),
],
)),
const SizedBox(height: 5),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() {
_selectedSpace = newValue;
});
},
),
],
),
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
),
),
),
),
],
),
const SizedBox(height: 10),
],
),
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
],
),
const SizedBox(height: 10),
],
),
],
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
),
],
),
),
);
},

View File

@ -34,7 +34,9 @@ class SpaceDropdown extends StatelessWidget {
),
SizedBox(
child: Container(
height: 40,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
@ -45,7 +47,7 @@ class SpaceDropdown extends StatelessWidget {
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
@ -88,7 +90,7 @@ class SpaceDropdown extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 6,
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
@ -129,6 +131,7 @@ class SpaceDropdown extends StatelessWidget {
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/curtain_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart';
@ -26,7 +27,7 @@ class DeviceDialogHelper {
final result = await _getDialogForDeviceType(
dialogType: dialogType,
context: context,
productType: data['productType'],
productType: data['productType'] as String,
data: data,
functions: functions,
removeComparetors: removeComparetors,
@ -65,7 +66,14 @@ class DeviceDialogHelper {
removeComparetors: removeComparetors,
dialogType: dialogType,
);
case 'CUR':
return CurtainHelper.showControlDialog(
dialogType: dialogType,
context: context,
functions: functions,
uniqueCustomId: data['uniqueCustomId'],
device: data['device'],
);
case '1G':
return OneGangSwitchHelper.showSwitchFunctionsDialog(
dialogType: dialogType,

View File

@ -17,9 +17,10 @@ class SaveRoutineHelper {
builder: (context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
final selectedConditionLabel = state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
final selectedConditionLabel =
state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
return AlertDialog(
contentPadding: EdgeInsets.zero,
@ -37,10 +38,11 @@ class SaveRoutineHelper {
Text(
'Create a scene: ${state.routineName ?? ""}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
style:
Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 18),
_buildDivider(),
@ -58,7 +60,8 @@ class SaveRoutineHelper {
_buildIfConditions(state, context),
Container(
width: 1,
color: ColorsManager.greyColor.withValues(alpha: 0.8),
color: ColorsManager.greyColor
.withValues(alpha: 0.8),
),
_buildThenActions(state, context),
],
@ -97,7 +100,8 @@ class SaveRoutineHelper {
child: Row(
spacing: 16,
children: [
Expanded(child: Text('IF: $selectedConditionLabel', style: textStyle)),
Expanded(
child: Text('IF: $selectedConditionLabel', style: textStyle)),
const Expanded(child: Text('THEN:', style: textStyle)),
],
),
@ -109,7 +113,7 @@ class SaveRoutineHelper {
spacing: 16,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DialogFooterButton(
DialogFooterButton(
text: 'Back',
onTap: () => Navigator.pop(context),
),
@ -143,7 +147,8 @@ class SaveRoutineHelper {
child: ListView(
// shrinkWrap: true,
children: state.thenItems.map((item) {
final functions = state.selectedFunctions[item['uniqueCustomId']] ?? [];
final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? [];
return functionRow(item, context, functions);
}).toList(),
),
@ -203,19 +208,20 @@ class SaveRoutineHelper {
),
),
child: Center(
child: item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
child:
item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
),
),
Flexible(

View File

@ -0,0 +1,49 @@
import 'package:syncrow_web/pages/device_managment/curtain/model/curtain_model.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart'
show DeviceFunction;
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class CurtainFunction extends DeviceFunction<CurtainModel> {
final String type;
CurtainFunction({
required super.deviceId,
required super.deviceName,
required this.type,
required super.code,
required super.operationName,
required super.icon,
});
List<CurtainOperationalValue> getOperationalValues();
}
class ControlCurtainFunction extends CurtainFunction {
ControlCurtainFunction({
required super.deviceId,
required super.deviceName,
required super.type,
super.code = 'control',
super.operationName = 'Control',
super.icon = Assets.curtain,
});
@override
List<CurtainOperationalValue> getOperationalValues() => [
CurtainOperationalValue(
icon: Assets.curtain,
description: 'OPEN',
value: 'open',
),
CurtainOperationalValue(
icon: Assets.curtain,
description: 'STOP',
value: 'stop',
),
CurtainOperationalValue(
icon: Assets.curtain,
description: 'CLOSE',
value: 'close',
)
];
}

View File

@ -0,0 +1,11 @@
class CurtainOperationalValue {
final String icon;
final String description;
final String value;
CurtainOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}

View File

@ -405,8 +405,8 @@ class PowerFactorCStatusFunction extends EnergyClampFunctions {
code: 'PowerFactorC',
operationName: 'Power Factor C',
icon: Assets.speedoMeter,
min: 0.00,
max: 1.00,
min: 0.0,
max: 1.0,
step: 0.1,
unit: "",
);

View File

@ -148,6 +148,7 @@ class IfContainer extends StatelessWidget {
'NCPS',
'WH',
'PC',
'CUR',
].contains(mutableData['productType'])) {
context
.read<RoutineBloc>()

View File

@ -28,6 +28,7 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'NCPS',
'WH',
'PC',
'CUR',
};
@override

View File

@ -117,10 +117,22 @@ class ACHelper {
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -78,12 +78,22 @@ class _CeilingSensorDialogState extends State<CeilingSensorDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
final functions = _updateValuesForAddedFunctions(
state.addedFunctions,
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
functions,
[selectedFunctionData],
'${widget.uniqueCustomId}',
),
);

View File

@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CurtainHelper {
static Future<Map<String, dynamic>?> showControlDialog({
required String dialogType,
required BuildContext context,
required List<DeviceFunction> functions,
required String uniqueCustomId,
required AllDevicesModel? device,
}) async {
List<ControlCurtainFunction> curtainFunctions =
functions.whereType<ControlCurtainFunction>().where((function) {
if (dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
return function.type == 'IF' || function.type == 'BOTH';
}).toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => BlocProvider(
create: (_) => FunctionBloc()..add(const InitializeFunctions([])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData = state.addedFunctions
.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('AC Functions'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Function list
SizedBox(
width: selectedFunction != null ? 320 : 360,
child: _buildFunctionsList(
context: context,
curtainFunctions: curtainFunctions,
onFunctionSelected:
(functionCode, operationName) {
RoutineTapFunctionHelper.onTapFunction(
context,
functionCode: functionCode,
functionOperationName: operationName,
functionValueDescription:
selectedFunctionData.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'temp_set',
'temp_current',
],
defaultValue: 0);
}),
),
// Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
controlFunctions: curtainFunctions,
device: device,
operationName: selectedOperationName ?? '',
),
),
],
),
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
),
),
).then((value) {
return value;
});
}
static Widget _buildFunctionsList({
required BuildContext context,
required List<ControlCurtainFunction> curtainFunctions,
required Function(String, String) onFunctionSelected,
}) {
return ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: curtainFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(
color: ColorsManager.dividerColor,
),
),
itemBuilder: (context, index) {
final function = curtainFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => onFunctionSelected(
function.code,
function.operationName,
),
);
},
);
}
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<ControlCurtainFunction> controlFunctions,
AllDevicesModel? device,
required String operationName,
}) {
final selectedFn =
controlFunctions.firstWhere((f) => f.code == selectedFunction);
// Rest of your existing code for other value selectors
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<CurtainOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
// required Function(dynamic) onValueChanged,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 24,
color: isSelected
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription:
selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}

View File

@ -192,9 +192,18 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -115,9 +115,18 @@ class _GatewayDialogState extends State<GatewayDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId ?? '-1',
),
);

View File

@ -147,7 +147,7 @@ class OneGangSwitchHelper {
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -250,9 +250,18 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -27,17 +27,16 @@ class EnergyValueSelectorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final selectedFn =
functions.firstWhere((f) => f.code == selectedFunction);
final selectedFn = functions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
final step = selectedFn.step ?? 1.0;
final step = selectedFn.step;
final _unit = selectedFn.unit ?? '';
final (double, double) sliderRange =
(selectedFn.min ?? 0.0, selectedFn.max ?? 100.0);
if (_isSliderFunction(selectedFunction)) {
return CustomRoutinesTextbox(
withSpecialChar: false,
withSpecialChar: true,
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
@ -60,14 +59,14 @@ class EnergyValueSelectorWidget extends StatelessWidget {
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: value.toInt(),
value: value,
condition: functionData.condition,
),
),
),
unit: _unit,
dividendOfRange: 1,
stepIncreaseAmount: step,
stepIncreaseAmount: step!,
);
}

View File

@ -145,9 +145,22 @@ class TwoGangSwitchHelper {
// ),
// );
// }
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode ==
state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode:
state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);

View File

@ -210,9 +210,18 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -188,9 +188,18 @@ class _WaterHeaterDialogRoutinesState extends State<WaterHeaterDialogRoutines> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

View File

@ -30,123 +30,121 @@ class ThenContainer extends StatelessWidget {
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
state.isLoading && state.isUpdate == true
? const Center(
child: CircularProgressIndicator(),
)
: Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
state.thenItems.length,
(index) => GestureDetector(
onTap: () async {
if (state.thenItems[index]
['deviceId'] ==
'delay') {
final result = await DelayHelper
.showDelayPickerDialog(context,
state.thenItems[index]);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.delay,
'title': 'Delay',
}));
}
return;
}
if (state.thenItems[index]['type'] ==
'automation') {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) =>
AutomationDialog(
automationName:
state.thenItems[index]
['name'] ??
'Automation',
automationId:
state.thenItems[index]
['deviceId'] ??
'',
uniqueCustomId:
state.thenItems[index]
['uniqueCustomId'],
),
);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath':
Assets.automation,
'title':
state.thenItems[index]
['name'] ??
state.thenItems[index]
['title'],
}));
}
return;
}
final result = await DeviceDialogHelper
.showDeviceDialog(
context: context,
data: state.thenItems[index],
removeComparetors: true,
dialogType: "THEN");
if (state.isLoading && state.isUpdate == true)
const Center(
child: CircularProgressIndicator(),
)
else
Wrap(
spacing: 8,
runSpacing: 8,
children: List.generate(
state.thenItems.length,
(index) => GestureDetector(
onTap: () async {
if (state.thenItems[index]['deviceId'] ==
'delay') {
final result = await DelayHelper
.showDelayPickerDialog(context,
state.thenItems[index]);
if (result != null) {
context.read<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
} else if (![
'AC',
'1G',
'2G',
'3G',
'WPS',
'CPS',
"GW",
"NCPS",
'WH',
].contains(state.thenItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
context
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.delay,
'title': 'Delay',
}));
}
return;
}
if (state.thenItems[index]['type'] ==
'automation') {
final result = await showDialog<bool>(
context: context,
builder: (BuildContext context) =>
AutomationDialog(
automationName:
state.thenItems[index]['name']
as String? ??
'Automation',
automationId: state.thenItems[index]
['deviceId'] as String? ??
'',
uniqueCustomId: state
.thenItems[index]
['uniqueCustomId'] as String,
),
);
if (result != null) {
context
.read<RoutineBloc>()
.add(AddToThenContainer({
...state.thenItems[index],
'imagePath': Assets.automation,
'title': state.thenItems[index]
['name'] ??
state.thenItems[index]
['title'],
}));
}
return;
}
final result = await DeviceDialogHelper
.showDeviceDialog(
context: context,
data: state.thenItems[index],
removeComparetors: true,
dialogType: 'THEN');
if (result != null) {
context.read<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
} else if (![
'AC',
'1G',
'2G',
'3G',
'WPS',
'CPS',
'GW',
'NCPS',
'WH',
'CUR',
].contains(state.thenItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
AddToThenContainer(
state.thenItems[index]));
}
},
child: DraggableCard(
imagePath: state.thenItems[index]
['imagePath'] as String? ??
'',
title: state.thenItems[index]['title']
as String? ??
'',
deviceData: state.thenItems[index],
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 8),
isFromThen: true,
isFromIf: false,
onRemove: () {
context.read<RoutineBloc>().add(
RemoveDragCard(
index: index,
isFromThen: true,
key: state.thenItems[index]
['uniqueCustomId']
as String));
},
child: DraggableCard(
imagePath: state.thenItems[index]
['imagePath'] ??
'',
title: state.thenItems[index]
['title'] ??
'',
deviceData: state.thenItems[index],
padding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 8),
isFromThen: true,
isFromIf: false,
onRemove: () {
context.read<RoutineBloc>().add(
RemoveDragCard(
index: index,
isFromThen: true,
key: state.thenItems[index]
['uniqueCustomId']));
},
),
))),
),
))),
],
),
),
@ -230,7 +228,7 @@ class ThenContainer extends StatelessWidget {
context: context,
data: mutableData,
removeComparetors: true,
dialogType: "THEN");
dialogType: 'THEN');
if (result != null) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
} else if (![
@ -241,9 +239,10 @@ class ThenContainer extends StatelessWidget {
'WPS',
'GW',
'CPS',
"NCPS",
"WH",
'NCPS',
'WH',
'PC',
'CUR',
].contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
}

View File

@ -1,43 +0,0 @@
import 'package:equatable/equatable.dart';
class PaginatedDataModel<T> extends Equatable {
const PaginatedDataModel({
required this.data,
required this.page,
required this.size,
required this.hasNext,
required this.totalItems,
required this.totalPages,
});
final List<T> data;
final int page;
final int size;
final bool hasNext;
final int totalItems;
final int totalPages;
factory PaginatedDataModel.fromJson(
Map<String, dynamic> json,
List<T> Function(List<dynamic>) fromJsonList,
) {
return PaginatedDataModel<T>(
data: fromJsonList(json['data'] as List<dynamic>),
page: json['page'] as int? ?? 1,
size: json['size'] as int? ?? 25,
hasNext: json['hasNext'] as bool? ?? false,
totalItems: json['totalItem'] as int? ?? 0,
totalPages: json['totalPage'] as int? ?? 0,
);
}
@override
List<Object?> get props => [
data,
page,
size,
hasNext,
totalItems,
totalPages,
];
}

View File

@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_body.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/debounced_communities_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/services/remote_communities_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class SpaceManagementPage extends StatelessWidget {
const SpaceManagementPage({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => CommunitiesBloc(
communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()),
),
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
],
child: WebScaffold(
appBarTitle: Text(
'Space Management',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
enableMenuSidebar: false,
centerBody: Text(
'Community Structure',
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.bold,
),
),
rightBody: const NavigateHomeGridView(),
scaffoldBody: const SpaceManagementBody(),
),
);
}
}

View File

@ -1,15 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
class SpaceManagementBody extends StatelessWidget {
const SpaceManagementBody({super.key});
@override
Widget build(BuildContext context) {
return const Row(
children: [
SpaceManagementCommunitiesTree(),
],
);
}
}

View File

@ -1,42 +0,0 @@
import 'dart:async';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
final class DebouncedCommunitiesService implements CommunitiesService {
DebouncedCommunitiesService(
this._decoratee, {
this.debounceDuration = const Duration(milliseconds: 500),
});
final CommunitiesService _decoratee;
final Duration debounceDuration;
Timer? _debounceTimer;
late Completer<CommunitiesPaginationModel>? _completer;
@override
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
_debounceTimer?.cancel();
_completer = Completer<CommunitiesPaginationModel>();
final currentCompleter = _completer!;
_debounceTimer = Timer(debounceDuration, () async {
try {
final result = await _decoratee.getCommunity(param);
if (!currentCompleter.isCompleted) {
currentCompleter.complete(result);
}
} catch (error) {
if (!currentCompleter.isCompleted) {
currentCompleter.completeError(error);
}
}
});
return currentCompleter.future;
}
}

View File

@ -1,11 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteCommunitiesService implements CommunitiesService {
const RemoteCommunitiesService(this._httpService);
@ -15,26 +13,14 @@ class RemoteCommunitiesService implements CommunitiesService {
static const _defaultErrorMessage = 'Failed to load communities';
@override
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param) async {
try {
final response = await _httpService.get(
path: await _makeUrl(),
queryParameters: {
'page': param.page,
'size': param.size,
'includeSpaces': param.includeSpaces,
if (param.search.isNotEmpty && param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
json as Map<String, dynamic>,
CommunityModel.fromJsonList,
),
return _httpService.get(
path: '/api/communities/',
expectedResponseModel: (json) => (json as List<dynamic>)
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -45,13 +31,4 @@ class RemoteCommunitiesService implements CommunitiesService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) throw APIException('Project UUID is required');
return ApiEndpoints.getCommunityListv2.replaceAll(
'{projectId}',
projectUuid,
);
}
}

View File

@ -4,19 +4,11 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
class CommunityModel extends Equatable {
final String uuid;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
final String description;
final String externalId;
final List<SpaceModel> spaces;
const CommunityModel({
required this.uuid,
required this.name,
required this.createdAt,
required this.updatedAt,
required this.description,
required this.externalId,
required this.spaces,
});
@ -24,20 +16,11 @@ class CommunityModel extends Equatable {
return CommunityModel(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
description: json['description'] as String,
externalId: json['externalId']?.toString() ?? '',
spaces: (json['spaces'] as List<dynamic>? ?? <dynamic>[])
spaces: (json['spaces'] as List<dynamic>)
.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
static List<CommunityModel> fromJsonList(List<dynamic> json) {
return json
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList();
}
@override
List<Object?> get props => [uuid, name, spaces];

View File

@ -2,37 +2,26 @@ import 'package:equatable/equatable.dart';
class SpaceModel extends Equatable {
final String uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String spaceName;
final String icon;
final List<SpaceModel> children;
final SpaceModel? parent;
const SpaceModel({
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.spaceName,
required this.icon,
required this.children,
required this.parent,
});
factory SpaceModel.fromJson(Map<String, dynamic> json) {
return SpaceModel(
uuid: json['uuid'] as String? ?? '',
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
spaceName: json['spaceName'] as String? ?? '',
icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg',
uuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
icon: json['icon'] as String,
children: (json['children'] as List<dynamic>?)
?.map((e) => SpaceModel.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
parent: json['parent'] != null
? SpaceModel.fromJson(json['parent'] as Map<String, dynamic>)
: null,
);
}

View File

@ -1,32 +1,3 @@
import 'package:equatable/equatable.dart';
class LoadCommunitiesParam extends Equatable {
const LoadCommunitiesParam({
this.page = 1,
this.size = 25,
this.search = '',
this.includeSpaces = true,
});
final int page;
final int size;
final String search;
final bool includeSpaces;
LoadCommunitiesParam copyWith({
int? page,
int? size,
String? search,
bool? includeSpaces,
}) {
return LoadCommunitiesParam(
page: page ?? this.page,
size: size ?? this.size,
search: search ?? this.search,
includeSpaces: includeSpaces ?? this.includeSpaces,
);
}
@override
List<Object?> get props => [page, size, search, includeSpaces];
class LoadCommunitiesParam {
const LoadCommunitiesParam();
}

View File

@ -1,9 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
typedef CommunitiesPaginationModel = PaginatedDataModel<CommunityModel>;
abstract class CommunitiesService {
Future<CommunitiesPaginationModel> getCommunity(LoadCommunitiesParam param);
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param);
}

View File

@ -14,8 +14,6 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
}) : _communitiesService = communitiesService,
super(const CommunitiesState()) {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<InsertCommunity>(_onInsertCommunity);
}
final CommunitiesService _communitiesService;
@ -25,93 +23,28 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
Emitter<CommunitiesState> emit,
) async {
try {
emit(
state.copyWith(status: CommunitiesStatus.loading),
);
final paginationResponse = await _communitiesService.getCommunity(
event.param,
);
emit(const CommunitiesState(status: CommunitiesStatus.loading));
final communities = await _communitiesService.getCommunity(event.param);
emit(
CommunitiesState(
status: CommunitiesStatus.success,
communities: paginationResponse.data,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
searchQuery: event.param.search,
isLoadingMore: false,
communities: communities,
),
);
} on APIException catch (e) {
_onApiException(e, emit);
} catch (e) {
_onError(e, emit);
}
}
Future<void> _onLoadMoreCommunities(
LoadMoreCommunities event,
Emitter<CommunitiesState> emit,
) async {
if (!state.hasNext || state.isLoadingMore) return;
try {
emit(state.copyWith(isLoadingMore: true));
final param = LoadCommunitiesParam(
page: state.currentPage + 1,
search: state.searchQuery,
);
final paginationResponse = await _communitiesService.getCommunity(param);
final updatedCommunities = List<CommunityModel>.from(state.communities)
..addAll(paginationResponse.data);
emit(
state.copyWith(
status: CommunitiesStatus.success,
communities: updatedCommunities,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
isLoadingMore: false,
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.message,
),
);
} on APIException catch (e) {
_onApiException(e, emit);
} catch (e) {
_onError(e, emit);
emit(
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onApiException(
APIException e,
Emitter<CommunitiesState> emit,
) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.message,
),
);
}
void _onError(Object e, Emitter<CommunitiesState> emit) {
emit(
state.copyWith(
status: CommunitiesStatus.failure,
isLoadingMore: false,
errorMessage: e.toString(),
),
);
}
void _onInsertCommunity(
InsertCommunity event,
Emitter<CommunitiesState> emit,
) {
emit(state.copyWith(communities: [event.community, ...state.communities]));
}
}

View File

@ -15,19 +15,3 @@ class LoadCommunities extends CommunitiesEvent {
@override
List<Object?> get props => [param];
}
class LoadMoreCommunities extends CommunitiesEvent {
const LoadMoreCommunities();
@override
List<Object?> get props => [];
}
final class InsertCommunity extends CommunitiesEvent {
const InsertCommunity(this.community);
final CommunityModel community;
@override
List<Object?> get props => [community];
}

View File

@ -7,48 +7,12 @@ final class CommunitiesState extends Equatable {
this.status = CommunitiesStatus.initial,
this.communities = const [],
this.errorMessage,
this.isLoadingMore = false,
this.hasNext = false,
this.currentPage = 1,
this.searchQuery = '',
});
final CommunitiesStatus status;
final List<CommunityModel> communities;
final String? errorMessage;
final bool isLoadingMore;
final bool hasNext;
final int currentPage;
final String searchQuery;
CommunitiesState copyWith({
CommunitiesStatus? status,
List<CommunityModel>? communities,
String? errorMessage,
bool? isLoadingMore,
bool? hasNext,
int? currentPage,
String? searchQuery,
}) {
return CommunitiesState(
status: status ?? this.status,
communities: communities ?? this.communities,
errorMessage: errorMessage ?? this.errorMessage,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
hasNext: hasNext ?? this.hasNext,
currentPage: currentPage ?? this.currentPage,
searchQuery: searchQuery ?? this.searchQuery,
);
}
@override
List<Object?> get props => [
status,
communities,
errorMessage,
isLoadingMore,
hasNext,
currentPage,
searchQuery,
];
List<Object?> get props => [status, communities, errorMessage];
}

View File

@ -1,47 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
part 'communities_tree_selection_event.dart';
part 'communities_tree_selection_state.dart';
class CommunitiesTreeSelectionBloc
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
on<SelectCommunityEvent>(_onSelectCommunity);
on<SelectSpaceEvent>(_onSelectSpace);
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
}
void _onSelectCommunity(
SelectCommunityEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: event.community,
selectedSpace: null,
),
);
}
void _onSelectSpace(
SelectSpaceEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: null,
selectedSpace: event.space,
),
);
}
void _onClearSelection(
ClearCommunitiesTreeSelectionEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(const CommunitiesTreeSelectionState());
}
}

View File

@ -1,30 +0,0 @@
part of 'communities_tree_selection_bloc.dart';
sealed class CommunitiesTreeSelectionEvent extends Equatable {
const CommunitiesTreeSelectionEvent();
@override
List<Object?> get props => [];
}
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
final CommunityModel? community;
const SelectCommunityEvent({required this.community});
@override
List<Object?> get props => [community];
}
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
final SpaceModel? space;
const SelectSpaceEvent({required this.space});
@override
List<Object?> get props => [space];
}
final class ClearCommunitiesTreeSelectionEvent
extends CommunitiesTreeSelectionEvent {
const ClearCommunitiesTreeSelectionEvent();
}

View File

@ -1,29 +0,0 @@
part of 'communities_tree_selection_bloc.dart';
final class CommunitiesTreeSelectionState extends Equatable {
const CommunitiesTreeSelectionState({
this.selectedCommunity,
this.selectedSpace,
});
final CommunityModel? selectedCommunity;
final SpaceModel? selectedSpace;
CommunitiesTreeSelectionState copyWith({
CommunityModel? selectedCommunity,
SpaceModel? selectedSpace,
List<CommunityModel>? expandedCommunities,
List<SpaceModel>? expandedSpaces,
}) {
return CommunitiesTreeSelectionState(
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
selectedSpace: selectedSpace ?? this.selectedSpace,
);
}
@override
List<Object?> get props => [
selectedCommunity,
selectedSpace,
];
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
class CommunitiesTreeFailureWidget extends StatelessWidget {
const CommunitiesTreeFailureWidget({super.key, this.errorMessage});
final String? errorMessage;
@override
Widget build(BuildContext context) {
return Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities(
LoadCommunitiesParam(
search: context.read<CommunitiesBloc>().state.searchQuery,
),
),
),
child: const Text('Retry'),
),
],
),
),
);
}
}

View File

@ -1,37 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class CommunityTile extends StatelessWidget {
final String title;
final List<Widget>? children;
final bool isExpanded;
final bool isSelected;
final void Function(String, bool isExpanded) onExpansionChanged;
final void Function() onItemSelected;
const CommunityTile({
super.key,
required this.title,
required this.isExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: CustomExpansionTile(
title: title,
initiallyExpanded: isExpanded,
isSelected: isSelected,
onExpansionChanged: (bool expanded) {
onExpansionChanged(title, expanded);
},
onItemSelected: onItemSelected,
children: children ?? [],
));
}
}

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
class EmptyCommunitiesTreeSearchResultWidget extends StatelessWidget {
const EmptyCommunitiesTreeSearchResultWidget({
required this.searchQuery,
super.key,
});
final String searchQuery;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
searchQuery.isEmpty
? 'No communities found'
: 'No communities found for "$searchQuery"',
textAlign: TextAlign.center,
),
);
}
}

View File

@ -1,112 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatefulWidget {
const SpaceManagementCommunitiesTree({super.key});
@override
State<SpaceManagementCommunitiesTree> createState() =>
_SpaceManagementCommunitiesTreeState();
}
class _SpaceManagementCommunitiesTreeState
extends State<SpaceManagementCommunitiesTree> {
@override
void initState() {
context.read<CommunitiesBloc>().add(
const LoadCommunities(LoadCommunitiesParam()),
);
super.initState();
}
void _onSearchChanged(String searchQuery) {
context
.read<CommunitiesBloc>()
.add(LoadCommunities(LoadCommunitiesParam(search: searchQuery.trim())));
}
void _onLoadMore() {
context.read<CommunitiesBloc>().add(const LoadMoreCommunities());
}
@override
Widget build(BuildContext context) {
return BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) => Container(
width: 320,
decoration: subSectionContainerDecoration,
child: Column(
children: [
const SpaceManagementSidebarHeader(),
CustomSearchBar(
onSearchChanged: _onSearchChanged,
),
const SizedBox(height: 16),
switch (state.status) {
CommunitiesStatus.initial => const AppLoadingIndicator(),
CommunitiesStatus.loading => state.communities.isEmpty
? const AppLoadingIndicator()
: _buildCommunitiesTree(context, state),
CommunitiesStatus.success => _buildCommunitiesTree(context, state),
CommunitiesStatus.failure => CommunitiesTreeFailureWidget(
errorMessage: state.errorMessage,
),
},
Visibility(
visible: state.isLoadingMore,
child: const AppLoadingIndicator(),
),
],
),
),
);
}
Widget _buildCommunitiesTree(
BuildContext context,
CommunitiesState state,
) {
final communitiesIsEmpty = state.communities.isEmpty;
final statusIsSuccess = state.status == CommunitiesStatus.success;
return Expanded(
child: Visibility(
visible: statusIsSuccess && communitiesIsEmpty,
replacement: Stack(
children: [
SpaceManagementSidebarCommunitiesList(
communities: state.communities,
onLoadMore: state.hasNext ? _onLoadMore : null,
isLoadingMore: state.isLoadingMore,
hasNext: state.hasNext,
itemBuilder: (context, index) {
return SpaceManagementCommunitiesTreeCommunityTile(
community: state.communities[index],
);
},
),
if (state.status == CommunitiesStatus.loading &&
state.communities.isNotEmpty)
ColoredBox(
color: Colors.white.withValues(alpha: 0.7),
child: const AppLoadingIndicator(),
),
],
),
child: EmptyCommunitiesTreeSearchResultWidget(
searchQuery: state.searchQuery,
),
),
);
}
}

View File

@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_space_tile.dart';
class SpaceManagementCommunitiesTreeCommunityTile extends StatelessWidget {
const SpaceManagementCommunitiesTreeCommunityTile({
required this.community,
super.key,
});
final CommunityModel community;
@override
Widget build(BuildContext context) {
final spaces = community.spaces
.map(
(space) => SpaceManagementCommunitiesTreeSpaceTile(
space: space,
community: community,
),
)
.toList();
return CommunityTile(
title: community.name,
key: ValueKey(community.uuid),
isSelected: context
.watch<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity
?.uuid ==
community.uuid,
isExpanded: false,
onItemSelected: () {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
onExpansionChanged: (title, expanded) {},
children: spaces,
);
}
}

View File

@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
const SpaceManagementCommunitiesTreeSpaceTile({
required this.space,
required this.community,
super.key,
});
final SpaceModel space;
final CommunityModel community;
@override
Widget build(BuildContext context) {
final spaceIsExpanded = _isSpaceOrChildSelected(context, space);
final isSelected =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace?.uuid ==
space.uuid;
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16.0),
child: SpaceTile(
title: space.spaceName,
key: ValueKey(space.uuid),
isSelected: isSelected,
initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (expanded) {},
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(space: space),
),
children: space.children
.map(
(childSpace) => SpaceManagementCommunitiesTreeSpaceTile(
space: childSpace,
community: community,
),
)
.toList(),
),
);
}
bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) {
final selectedSpace =
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
final isSpaceSelected = selectedSpace?.uuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(
(child) => _isSpaceOrChildSelected(context, child),
);
return isSpaceSelected || anySubSpaceIsSelected;
}
}

View File

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceManagementSidebarAddCommunityButton extends StatelessWidget {
const SpaceManagementSidebarAddCommunityButton({
required this.onTap,
super.key,
});
final void Function() onTap;
@override
Widget build(BuildContext context) {
return SizedBox.square(
dimension: 30,
child: IconButton(
style: IconButton.styleFrom(
iconSize: 20,
backgroundColor: ColorsManager.circleImageBackground,
shape: const CircleBorder(
side: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 3,
),
),
),
onPressed: onTap,
icon: SvgPicture.asset(Assets.addIcon),
),
);
}
}

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