Compare commits

..

63 Commits

Author SHA1 Message Date
7750290be4 Fix device status display in Control modal to reflect actual status 2025-07-02 10:14:54 +03:00
423ad6e687 Enhance navigation buttons in SmartPowerDeviceControl for better user… (#293)
… experience

<!--
  Thanks for contributing!

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

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



## Description

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

## Type of Change

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

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

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

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

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

## Description

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

## Type of Change

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

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


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

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

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


## Description

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

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

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

<!--
  Thanks for contributing!

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

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



## Description

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

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-24 16:21:03 +03:00
c6e98fa245 Refactor visitor password dialog navigation to return specific values on pop 2025-06-24 16:06:53 +03:00
277a9ce4f0 Add countdown seconds to schedule management 2025-06-24 15:38:16 +03:00
db9e856bca Sp 1711 fe implement blank state (#288)
<!--
  Thanks for contributing!

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

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

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

## Description

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

## Type of Change

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

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


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

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

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

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

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

## Description

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

## Type of Change

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

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


[SP-1474]:
https://syncrow.atlassian.net/browse/SP-1474?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-24 12:27:39 +03:00
2a2fb7ffca Add responsive input fields and radio groups for visitor password setup 2025-06-24 11:36:50 +03:00
5a2299ea2f navigates to initial create space dialog from the respective buttons. 2025-06-24 10:47:48 +03:00
90f8305aa1 shows tooltip on SpaceCell. 2025-06-24 10:45:13 +03:00
329b2ba472 selects the space from and to connection when selecting a space. 2025-06-24 10:36:13 +03:00
0fb9149613 Selects all children of a space when selecting a parent. 2025-06-24 10:30:56 +03:00
87b45fff1d removed expanded widget that caused a size exception. 2025-06-24 10:21:25 +03:00
95ae50d12d navigates to selected space when changed on sidebar in space management canvas. 2025-06-24 10:16:03 +03:00
95d6e1ecda if online go green with online status else red with offline status 2025-06-23 16:33:45 +03:00
479aa4a091 Sp 1713 implement empty state (#285)
<!--
  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-1713](https://syncrow.atlassian.net/browse/SP-1713)

## Description

Implemented non selected space state
Implemented an initial version of the canvas.

## 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-1713]:
https://syncrow.atlassian.net/browse/SP-1713?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 16:27:42 +03:00
75efc595b4 reverted to old import to avoid confusion with QA team. 2025-06-23 16:22:11 +03:00
8bc7a3daa2 Implemented space management canvas. 2025-06-23 15:45:49 +03:00
03a6c5474b SP-1768-FE-The-white-are-in-empty-devices-table-should-take-the-whole-table-size-not-just-the-top (#282)
<!--
  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-1768](https://syncrow.atlassian.net/browse/SP-1768)

## Description

<!--- Describe your changes in detail -->
fix white container take only a small part of the device table

## 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-1768]:
https://syncrow.atlassian.net/browse/SP-1768?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 11:14:32 +03:00
ada7daf179 Switched from using Text to SelectableText in CreateCommunityDialog. 2025-06-23 10:13:30 +03:00
4bdb487094 doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. 2025-06-23 10:11:23 +03:00
f8e4c89cdb uses correct error message that the api sends in RemoteCreateCommunityService. 2025-06-23 10:11:03 +03:00
7d4cdba0ef Connected templates view into SpaceManagementBody, while applying the correct UI principals if what to show what when? 2025-06-23 10:06:59 +03:00
a78b5993a9 Created SpaceManagementTemplatesView widget. 2025-06-23 10:05:53 +03:00
0e7109a19e Created CommunityTemplateCell widget. 2025-06-23 10:02:15 +03:00
ff3d5cd996 Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. 2025-06-23 10:02:02 +03:00
5f30a5a61b Refactor empty state widget to use a container for better layout control 2025-06-23 10:01:01 +03:00
0712e6d64b Sp 1593 reworks (#277)
<!--
  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-1593](https://syncrow.atlassian.net/browse/SP-1593)

## Description

1. AQI Distribution chart bars when all values are empty.
2. Min element in Y axis of Range of AQI chart is visible.
3. Matched AQI chart titles to have the same size for consistency.
4. Allowed `RangeOfAqiValue` model's values to be nullable, and they
fallback to `0` when null.
5. Implemented AQI Legend.
6. Increased the size of AQI Distribution chart's tooltip.
7. Improved alignment of location cell.
8. Doesn't fetch devices on date change.

## 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-1593]:
https://syncrow.atlassian.net/browse/SP-1593?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 09:48:17 +03:00
a493ae08ce SP-1710-FE-Create-Sidebar (#278)
<!--
  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-1710](https://syncrow.atlassian.net/browse/SP-1710)

## Description

1. Implemented Space Management Community Side Tree.
2. Implemented Creating a new community feature.

## 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-1710]:
https://syncrow.atlassian.net/browse/SP-1710?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 09:36:30 +03:00
27349a6cc0 Implemented PR notes by extracting widgets into their own classes. 2025-06-23 09:24:53 +03:00
d17d4184be fix it and add lock to open when press (as loved simple animation) (#280)
with adding the timer as circle

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

## Description

alll is goood it is now listening to changes and when unlockRequest up
to 0 and under 30 it is start to give permission

## 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-1567]:
https://syncrow.atlassian.net/browse/SP-1567?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 08:19:22 +03:00
41d4fbb555 Extracted pagination data into a generic DTO. 2025-06-22 16:00:20 +03:00
fccb5cbbab SP-1606-FE-Fix-Search-Function-on-Devices-Screen-to-Support-All-Device-Types-Flush-Mounted-Sensor (#281)
…ctionality

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

## Description

<!--- Describe your changes in detail -->
search by product name and device name 

## 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-1606]:
https://syncrow.atlassian.net/browse/SP-1606?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 15:49:19 +03:00
48d7ab430f refactor: rename productName to deviceNameOrProductName in search functionality 2025-06-22 15:35:46 +03:00
28ac911f3f Accomodated for null values in SpaceModel. 2025-06-22 15:30:47 +03:00
a793cc3967 fix it and add lock to open when press (as loved simple animation)
with adding the timer as circle
2025-06-22 15:24:53 +03:00
09446844b0 reverted initializing the new space management page in the router, to avoid any confusion with the QA team. 2025-06-22 15:11:38 +03:00
f02788eaa5 implemented create community feature. 2025-06-22 14:58:38 +03:00
614db4333c Refactor ScheduleBloc and related components to use dynamic category … (#279)
<!--
  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 scheduling Bugs

## 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-22 14:44:18 +03:00
b79ab06d95 shows a loading indicator when loading. 2025-06-22 12:58:45 +03:00
0a424300aa Refactor ScheduleBloc and related components to use dynamic category handling for schedule events 2025-06-22 12:46:54 +03:00
8494f0a8f1 matched community and space models with API. 2025-06-22 12:38: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
65ed94eb08 debounce and refactored CommunitiesBloc. 2025-06-22 12:01:32 +03:00
51c088d998 made communities paginatable. 2025-06-22 11:11:25 +03:00
2f233db332 implemented space management side bar. 2025-06-22 11:04:39 +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
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
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
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
20d044f2e5 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1710-FE-Create-Sidebar 2025-06-18 09:44:35 +03:00
8caee32822 Initialized new SpaceManagementPage. 2025-06-18 09:39:49 +03:00
584845ffdc fix horizontal scroll bar 2025-05-22 04:52:23 -05:00
98 changed files with 5387 additions and 1770 deletions

View File

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

View File

@ -50,20 +50,14 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
late ScrollController _horizontalHeaderScrollController;
late ScrollController _horizontalBodyScrollController;
static const double _fixedRowHeight = 60;
static const double _checkboxColumnWidth = 50;
static const double _settingsColumnWidth = 100;
@override
void initState() {
super.initState();
_initializeSelection();
_horizontalHeaderScrollController = ScrollController();
_horizontalBodyScrollController = ScrollController();
// Synchronize horizontal scrolling
_horizontalBodyScrollController.addListener(() {
_horizontalHeaderScrollController
.jumpTo(_horizontalBodyScrollController.offset);
});
}
@override
@ -76,7 +70,6 @@ class _DynamicTableState extends State<DynamicTable> {
bool _compareListOfLists(
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
// Check if the old and new lists are the same
if (oldList.length != newList.length) return false;
for (int i = 0; i < oldList.length; i++) {
@ -113,97 +106,173 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
@override
void dispose() {
_horizontalHeaderScrollController.dispose();
_horizontalBodyScrollController.dispose();
super.dispose();
double get _totalTableWidth {
final hasSettings = widget.headers.contains('Settings');
final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) +
(hasSettings ? _settingsColumnWidth : 0);
final regularCount = widget.headers.length - (hasSettings ? 1 : 0);
final regularWidth = (widget.size.width - base) / regularCount;
return base + regularCount * regularWidth;
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.size.width,
height: widget.size.height,
decoration: widget.cellDecoration,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
}),
],
),
),
),
),
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(),
],
);
}),
),
),
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: Scrollbar(
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) =>
notif.metrics.axis == Axis.horizontal,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: _totalTableWidth,
child: Column(
children: [
Container(
height: _fixedRowHeight,
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: Row(
children: [
if (widget.withCheckBox)
_buildSelectAllCheckbox(_checkboxColumnWidth),
for (var i = 0; i < widget.headers.length; i++)
_buildTableHeaderCell(
widget.headers[i],
widget.headers[i] == 'Settings'
? _settingsColumnWidth
: (_totalTableWidth -
(widget.withCheckBox
? _checkboxColumnWidth
: 0) -
(widget.headers.contains('Settings')
? _settingsColumnWidth
: 0)) /
(widget.headers.length -
(widget.headers.contains('Settings')
? 1
: 0)),
),
],
),
),
),
Expanded(
child: widget.isEmpty
? _buildEmptyState()
: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) =>
notif.metrics.axis == Axis.vertical,
child: ListView.builder(
controller: _verticalScrollController,
itemCount: widget.data.length,
itemBuilder: (_, rowIndex) {
final row = widget.data[rowIndex];
return SizedBox(
height: _fixedRowHeight,
child: Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex,
_checkboxColumnWidth,
),
for (var colIndex = 0;
colIndex < row.length;
colIndex++)
widget.headers[colIndex] == 'Settings'
? buildSettingsIcon(
width: _settingsColumnWidth,
onTap: () => widget
.onSettingsPressed
?.call(rowIndex),
)
: _buildTableCell(
row[colIndex].toString(),
width: widget.headers[
colIndex] ==
'Settings'
? _settingsColumnWidth
: (_totalTableWidth -
(widget.withCheckBox
? _checkboxColumnWidth
: 0) -
(widget.headers
.contains(
'Settings')
? _settingsColumnWidth
: 0)) /
(widget.headers.length -
(widget.headers
.contains(
'Settings')
? 1
: 0)),
rowIndex: rowIndex,
columnIndex: colIndex,
),
],
),
);
},
),
),
),
],
),
),
),
],
),
),
);
}
Widget _buildSelectAllCheckbox() {
Widget _buildEmptyState() => Container(
height: widget.size.height,
color: ColorsManager.whiteColors,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
SvgPicture.asset(Assets.emptyTable),
const SizedBox(height: 15),
Text(
widget.tableName == 'AccessManagement'
? 'No Password '
: 'No Devices',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.grayColor),
)
],
),
],
),
SizedBox(height: widget.size.height * 0.5),
],
),
);
Widget _buildSelectAllCheckbox(double width) {
return Container(
width: 50,
width: width,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
@ -218,37 +287,11 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildEmptyState() => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
SvgPicture.asset(Assets.emptyTable),
const SizedBox(height: 15),
Text(
widget.tableName == 'AccessManagement'
? 'No Password '
: 'No Devices',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.grayColor),
)
],
),
],
),
],
);
Widget _buildRowCheckbox(int index, double size) {
Widget _buildRowCheckbox(int index, double width) {
return Container(
width: 50,
width: width,
padding: const EdgeInsets.all(8.0),
height: size,
height: _fixedRowHeight,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
@ -270,55 +313,47 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableHeaderCell(String title, int index) {
return Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
Widget _buildTableHeaderCell(String title, double width) {
return Container(
width: width,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
constraints: const BoxConstraints.expand(height: 40),
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
color: ColorsManager.grayColor,
fontSize: 12,
fontWeight: FontWeight.w400,
),
maxLines: 2,
),
constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight),
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
color: ColorsManager.grayColor,
fontSize: 12,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _buildTableCell(
String content,
double size, {
required int rowIndex,
required int columnIndex,
}) {
Widget _buildTableCell(String content,
{required double width,
required int rowIndex,
required int columnIndex}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
if (isBatteryLevel) {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
height: 60,
iconSize: 40,
onTap: () => widget.onSettingsPressed?.call(rowIndex),
);
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
}
Color? statusColor;
@ -342,93 +377,82 @@ class _DynamicTableState extends State<DynamicTable> {
statusColor = Colors.black;
}
return Expanded(
child: Container(
height: size,
padding: const EdgeInsets.all(5.0),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
return Container(
width: width,
height: _fixedRowHeight,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
color: Colors.white,
),
alignment: Alignment.centerLeft,
child: Text(
content,
style: TextStyle(
color: (batteryLevel != null && batteryLevel < 20)
? ColorsManager.red
: (batteryLevel != null && batteryLevel > 20)
? ColorsManager.green
: statusColor,
fontSize: 13,
fontWeight: FontWeight.w400),
maxLines: 2,
color: Colors.white,
),
alignment: Alignment.centerLeft,
child: Text(
content,
style: TextStyle(
color: (batteryLevel != null && batteryLevel < 20)
? ColorsManager.red
: (batteryLevel != null && batteryLevel > 20)
? ColorsManager.green
: statusColor,
fontSize: 13,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
Widget buildSettingsIcon(
{double width = 120,
double height = 60,
double iconSize = 40,
VoidCallback? onTap}) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
margin: const EdgeInsets.only(right: 15),
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
),
Widget buildSettingsIcon({required double width, VoidCallback? onTap}) {
return Container(
width: width,
height: _fixedRowHeight,
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
width: width,
child: Padding(
padding: const EdgeInsets.only(
right: 16.0,
left: 17.0,
),
child: Container(
width: 50,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(height / 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.17),
blurRadius: 14,
offset: const Offset(0, 4),
),
],
),
),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: 50,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.17),
blurRadius: 14,
offset: const Offset(0, 4),
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings, // ضع المسار الصحيح هنا
width: 40,
height: 22,
color: ColorsManager
.primaryColor, // نفس لون الأيقونة في الصورة
),
),
],
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings,
width: 40,
height: 20,
color: ColorsManager.primaryColor,
),
),
),
),
),
],
),
);
}
}

View File

@ -40,17 +40,18 @@ class DeviceManagementBloc
List<AllDevicesModel> devices = [];
_devices.clear();
var spaceBloc = event.context.read<SpaceTreeBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
devices =
await DevicesManagementApi().fetchDevices('', '', projectUuid);
} else {
for (var community in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (var space in spacesList) {
devices.addAll(await DevicesManagementApi().fetchDevices(
community, space, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(community, space, projectUuid));
}
}
}
@ -100,7 +101,7 @@ class DeviceManagementBloc
));
if (currentProductName.isNotEmpty) {
add(SearchDevices(productName: currentProductName));
add(SearchDevices(deviceNameOrProductName: currentProductName));
}
}
}
@ -269,34 +270,41 @@ class DeviceManagementBloc
return 'All';
}
}
void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> emit) {
if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) &&
(event.productName == null || event.productName!.isEmpty)) {
(event.deviceNameOrProductName == null ||
event.deviceNameOrProductName!.isEmpty)) {
currentProductName = '';
if (state is DeviceManagementFiltered) {
add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
} else {
return;
}
_filteredDevices = List.from(_devices);
emit(DeviceManagementLoaded(
devices: _devices,
selectedIndex: _selectedIndex,
onlineCount: _onlineCount,
offlineCount: _offlineCount,
lowBatteryCount: _lowBatteryCount,
selectedDevice: null,
isControlButtonEnabled: false,
));
return;
}
if (event.productName == currentProductName &&
if (event.deviceNameOrProductName == currentProductName &&
event.community == currentCommunity &&
event.unitName == currentUnitName &&
event.searchField) {
return;
}
currentProductName = event.productName ?? '';
currentProductName = event.deviceNameOrProductName ?? '';
currentCommunity = event.community;
currentUnitName = event.unitName;
List<AllDevicesModel> devicesToSearch = _filteredDevices;
List<AllDevicesModel> devicesToSearch = _devices;
if (devicesToSearch.isNotEmpty) {
final searchText = event.deviceNameOrProductName?.toLowerCase() ?? '';
final filteredDevices = devicesToSearch.where((device) {
final matchesCommunity = event.community == null ||
event.community!.isEmpty ||
@ -304,31 +312,25 @@ class DeviceManagementBloc
?.toLowerCase()
.contains(event.community!.toLowerCase()) ??
false);
final matchesUnit = event.unitName == null ||
event.unitName!.isEmpty ||
(device.spaces != null &&
device.spaces!.isNotEmpty &&
device.spaces![0].spaceName!
.toLowerCase()
.contains(event.unitName!.toLowerCase()));
final matchesProductName = event.productName == null ||
event.productName!.isEmpty ||
(device.name
?.toLowerCase()
.contains(event.productName!.toLowerCase()) ??
false);
final matchesDeviceName = event.productName == null ||
event.productName!.isEmpty ||
(device.categoryName
?.toLowerCase()
.contains(event.productName!.toLowerCase()) ??
false);
device.spaces!.any((space) =>
space.spaceName != null &&
space.spaceName!
.toLowerCase()
.contains(event.unitName!.toLowerCase())));
return matchesCommunity &&
matchesUnit &&
(matchesProductName || matchesDeviceName);
final matchesSearchText = searchText.isEmpty ||
(device.name?.toLowerCase().contains(searchText) ?? false) ||
(device.productName?.toLowerCase().contains(searchText) ?? false);
return matchesCommunity && matchesUnit && matchesSearchText;
}).toList();
_filteredDevices = filteredDevices;
emit(DeviceManagementFiltered(
filteredDevices: filteredDevices,
selectedIndex: _selectedIndex,

View File

@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent {
class SearchDevices extends DeviceManagementEvent {
final String? community;
final String? unitName;
final String? productName;
final String? deviceNameOrProductName;
final bool searchField;
const SearchDevices({
this.community,
this.unitName,
this.productName,
this.deviceNameOrProductName,
this.searchField = false,
});
@override
List<Object?> get props => [community, unitName, productName];
List<Object?> get props => [community, unitName, deviceNameOrProductName];
}
class SelectDevice extends DeviceManagementEvent {

View File

@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
controller: controller,
onSubmitted: () {
final searchDevicesEvent = SearchDevices(
productName: _productNameController.text,
deviceNameOrProductName: _productNameController.text,
unitName: _unitNameController.text,
searchField: true,
);
@ -68,7 +68,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
onSearch: () => context.read<DeviceManagementBloc>().add(
SearchDevices(
unitName: _unitNameController.text,
productName: _productNameController.text,
deviceNameOrProductName: _productNameController.text,
searchField: true,
),
),

View File

@ -1,5 +1,3 @@
// ignore_for_file: invalid_use_of_visible_for_testing_member
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@ -16,45 +14,38 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) {
on<DoorLockFetchStatus>(_onFetchDeviceStatus);
//on<DoorLockControl>(_onDoorLockControl);
on<UpdateLockEvent>(_updateLock);
on<DoorLockFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
}
_listenToChanges(deviceId) {
void _listenToChanges(String deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((event) {
final data = event.snapshot.value;
if (data is Map) {
final statusData = data['status'] as List<dynamic>? ?? [];
final statusList = statusData.map((item) {
return Status(code: item['code'], value: item['value']);
}).toList();
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
DoorLockStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
final model =
DoorLockStatusModel.fromJson(data['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(model));
}
}
});
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<DoorLockState> emit) {
emit(DoorLockStatusLoading());
deviceStatus = event.deviceStatus;
emit(DoorLockStatusLoaded(deviceStatus));
}
FutureOr<void> _onFetchDeviceStatus(
Future<void> _onFetchDeviceStatus(
DoorLockFetchStatus event, Emitter<DoorLockState> emit) async {
emit(DoorLockStatusLoading());
try {
@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
deviceStatus =
DoorLockStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(DoorLockStatusLoaded(deviceStatus));
} catch (e) {
emit(DoorLockControlError(e.toString()));
}
}
FutureOr<void> _updateLock(
Future<void> _updateLock(
UpdateLockEvent event, Emitter<DoorLockState> emit) async {
final oldValue = deviceStatus.normalOpenSwitch;
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue);
@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
try {
final response = await DevicesManagementApi.openDoorLock(deviceId);
if (!response) {
_revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit);
}
@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
}
}
Future<void> _runDebounce({
required String deviceId,
required String code,
required dynamic value,
required dynamic oldValue,
required Emitter<DoorLockState> emit,
}) async {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
final response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
if (!response) {
_revertValueAndEmit(deviceId, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(deviceId, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(
String deviceId,
String code,
dynamic oldValue,
Emitter<DoorLockState> emit,
) {
void _revertValueAndEmit(String deviceId, String code, dynamic oldValue,
Emitter<DoorLockState> emit) {
_updateLocalValue(code, oldValue);
emit(DoorLockStatusLoaded(deviceStatus));
emit(const DoorLockControlError('Failed to control the device.'));
@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
void _updateLocalValue(String code, dynamic value) {
switch (code) {
case 'reverse_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(reverseLock: value);
}
break;
case 'normal_open_switch':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: value);
}
break;
case 'reverse_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(reverseLock: value);
}
break;
default:
break;
}
emit(DoorLockStatusLoaded(deviceStatus));
}
dynamic _getValueByCode(String code) {
switch (code) {
case 'reverse_lock':
return deviceStatus.reverseLock;
case 'normal_open_switch':
return deviceStatus.normalOpenSwitch;
default:
return null;
}
}
FutureOr<void> _onFactoryReset(
Future<void> _onFactoryReset(
DoorLockFactoryReset event, Emitter<DoorLockState> emit) async {
emit(DoorLockStatusLoading());
try {

View File

@ -8,7 +8,7 @@ import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_st
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class DoorLockButton extends StatefulWidget {
class DoorLockButton extends StatelessWidget {
const DoorLockButton({
super.key,
required this.doorLock,
@ -18,70 +18,28 @@ class DoorLockButton extends StatefulWidget {
final AllDevicesModel doorLock;
final DoorLockStatusModel smartDoorModel;
@override
State<DoorLockButton> createState() =>
_DoorLockButtonState(smartDoorModel: smartDoorModel);
}
class _DoorLockButtonState extends State<DoorLockButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
DoorLockStatusModel smartDoorModel;
_DoorLockButtonState({required this.smartDoorModel});
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 0, end: 1).animate(_animationController)
..addListener(() {
setState(() {});
});
if (smartDoorModel.unlockRequest > 0) {
_animationController.reverse(from: 1);
}
}
@override
void didUpdateWidget(covariant DoorLockButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.smartDoorModel.normalOpenSwitch !=
widget.smartDoorModel.normalOpenSwitch) {
setState(() {
smartDoorModel = widget.smartDoorModel;
});
if (smartDoorModel.unlockRequest > 0) {
_animationController.forward(from: 0);
} else {
_animationController.reverse(from: 1);
}
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
double _calculateProgress() {
final value = smartDoorModel.unlockRequest;
if (value <= 0 || value > 30) return 0;
return value / 30.0;
}
@override
Widget build(BuildContext context) {
final progress = _calculateProgress();
final isEnabled = smartDoorModel.unlockRequest > 0;
return SizedBox(
width: 255,
height: 255,
child: InkWell(
onTap: () {
_animationController.forward(from: 0);
BlocProvider.of<DoorLockBloc>(context)
.add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch));
},
onTap: isEnabled
? () {
BlocProvider.of<DoorLockBloc>(context).add(
UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch),
);
}
: null,
child: Container(
width: 255,
height: 255,
@ -115,15 +73,16 @@ class _DoorLockButtonState extends State<DoorLockButton>
),
),
),
SizedBox.expand(
child: CircularProgressIndicator(
value: _animation.value,
strokeWidth: 8,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
ColorsManager.primaryColor),
if (progress > 0)
SizedBox.expand(
child: CircularProgressIndicator(
value: progress,
strokeWidth: 8,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
ColorsManager.primaryColor),
),
),
),
],
),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,234 @@
part of 'schedule_bloc.dart';
abstract class ScheduleEvent extends Equatable {
const ScheduleEvent();
}
class ScheduleInitializeAddEvent extends ScheduleEvent {
final bool isEditing;
final ScheduleModes scheduleMode;
final TimeOfDay? selectedTime;
final List<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;
final int seconds;
const UpdateCountdownTimeEvent({
required this.hours,
required this.minutes,
required this.seconds,
});
@override
List<Object> get props => [hours, minutes, seconds];
}
class UpdateInchingTimeEvent extends ScheduleEvent {
final int hours;
final int minutes;
const UpdateInchingTimeEvent({
required this.hours,
required this.minutes,
});
@override
List<Object> get props => [hours, minutes];
}
class StartScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final int hours;
final int minutes;
const StartScheduleEvent({
required this.mode,
required this.hours,
required this.minutes,
});
@override
List<Object?> get props => [mode, hours, minutes];
}
class StopScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final String deviceId;
const StopScheduleEvent({
required this.mode,
required this.deviceId,
});
@override
List<Object?> get props => [mode, deviceId];
}
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
const ScheduleDecrementCountdownEvent();
@override
List<Object> get props => [];
}
class ScheduleFetchStatusEvent extends ScheduleEvent {
final String deviceId;
const ScheduleFetchStatusEvent(this.deviceId);
@override
List<Object> get props => [deviceId];
}
class DeleteScheduleEvent extends ScheduleEvent {
final String scheduleId;
const DeleteScheduleEvent(this.scheduleId);
@override
List<Object> get props => [scheduleId];
}
class StatusUpdatedScheduleEvent extends ScheduleEvent {
final String id;
const StatusUpdatedScheduleEvent(this.id);
@override
List<Object> get props => [id];
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/inching_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_header.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_managment_ui.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
class BuildScheduleView extends StatelessWidget {
const BuildScheduleView(
{super.key, required this.deviceUuid, required this.category});
final String deviceUuid;
final String category;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(deviceUuid)),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: 700,
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20),
child: BlocBuilder<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)
CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
isActive: state.isCountdownActive,
deviceId: deviceUuid,
hours: state.countdownHours,
minutes: state.countdownMinutes,
),
if (state.scheduleMode == ScheduleModes.inching)
InchingModeButtons(
isActive: state.isInchingActive,
deviceId: deviceUuid,
hours: state.inchingHours,
minutes: state.inchingMinutes,
),
if (state.scheduleMode != ScheduleModes.countdown &&
state.scheduleMode != ScheduleModes.inching)
ScheduleModeButtons(
onSave: () => Navigator.pop(context),
),
],
);
}
return const Center(child: CircularProgressIndicator());
},
),
),
),
),
),
);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,6 @@
class SpaceConnectionModel {
final String from;
final String to;
const SpaceConnectionModel({required this.from, required this.to});
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions;
final double cardWidth = 150.0;
final double cardHeight = 90.0;
final Set<String> highlightedUuids;
SpacesConnectionsArrowPainter({
required this.connections,
required this.positions,
required this.highlightedUuids,
});
@override
void paint(Canvas canvas, Size size) {
for (final connection in connections) {
final isSelected = highlightedUuids.contains(connection.from) ||
highlightedUuids.contains(connection.to);
final paint = Paint()
..color = isSelected
? ColorsManager.blackColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final from = positions[connection.from];
final to = positions[connection.to];
if (from != null && to != null) {
final startPoint =
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
controlPoint2.dy, endPoint.dx, endPoint.dy);
canvas.drawPath(path, paint);
final circlePaint = Paint()
..color = isSelected
? ColorsManager.blackColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 4, circlePaint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
abstract final class SpaceManagementCommunityDialogHelper {
static void showCreateDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const SelectableText('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}
}

View File

@ -0,0 +1,43 @@
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

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

View File

@ -0,0 +1,280 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
required this.community,
required this.selectedSpace,
super.key,
});
final CommunityModel community;
final SpaceModel? selectedSpace;
@override
State<CommunityStructureCanvas> createState() => _CommunityStructureCanvasState();
}
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0;
final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
late TransformationController _transformationController;
late AnimationController _animationController;
@override
void initState() {
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
super.initState();
}
@override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_animateToSpace(widget.selectedSpace);
}
});
}
}
@override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
Set<String> _getAllDescendantUuids(SpaceModel space) {
final uuids = <String>{};
for (final child in space.children) {
uuids.add(child.uuid);
uuids.addAll(_getAllDescendantUuids(child));
}
return uuids;
}
void _runAnimation(Matrix4 target) {
final animation = Matrix4Tween(
begin: _transformationController.value,
end: target,
).animate(_animationController);
void listener() {
_transformationController.value = animation.value;
}
animation.addListener(listener);
_animationController.forward(from: 0).whenCompleteOrCancel(() {
animation.removeListener(listener);
});
}
void _animateToSpace(SpaceModel? space) {
if (space == null) {
_runAnimation(Matrix4.identity());
return;
}
final position = _positions[space.uuid];
if (position == null) return;
const scale = 1.5;
final viewSize = context.size;
if (viewSize == null) return;
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
final matrix = Matrix4.identity()
..translate(x, y)
..scale(scale);
_runAnimation(matrix);
}
void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space),
);
}
void _resetSelectionAndZoom() {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
community: widget.community,
space: null,
),
);
}
void _calculateLayout(
List<SpaceModel> spaces,
int depth,
Map<int, double> levelXOffset,
) {
for (final space in spaces) {
double childSubtreeWidth = 0;
if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) {
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
}
}
final currentX = levelXOffset.putIfAbsent(depth, () => 0.0);
double? x;
if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!;
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
} else {
x = currentX;
}
if (x < currentX) {
final shiftX = currentX - x;
_shiftSubtree(space, shiftX);
final keysToShift = levelXOffset.keys.where((d) => d > depth).toList();
for (final key in keysToShift) {
levelXOffset[key] = levelXOffset[key]! + shiftX;
}
x += shiftX;
}
final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y);
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
}
}
void _shiftSubtree(SpaceModel space, double shiftX) {
if (_positions.containsKey(space.uuid)) {
_positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0);
}
for (final child in space.children) {
_shiftSubtree(child, shiftX);
}
}
List<Widget> _buildTreeWidgets() {
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{};
if (selectedSpace != null) {
highlightedUuids.add(selectedSpace.uuid);
highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace));
}
final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
return [
CustomPaint(
painter: SpacesConnectionsArrowPainter(
connections: connections,
positions: _positions,
highlightedUuids: highlightedUuids,
),
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
),
];
}
void _generateWidgets(
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
Set<String> highlightedUuids,
) {
for (final space in spaces) {
final position = _positions[space.uuid];
if (position == null) continue;
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
),
),
);
for (final child in space.children) {
connections.add(
SpaceConnectionModel(from: space.uuid, to: child.uuid),
);
}
_generateWidgets(space.children, widgets, connections, highlightedUuids);
}
}
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5,
height: MediaQuery.sizeOf(context).height * 5,
child: Stack(children: treeWidgets),
),
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityTemplateCell extends StatelessWidget {
const CommunityTemplateCell({
super.key,
required this.onTap,
required this.title,
});
final void Function() onTap;
final Widget title;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: AspectRatio(
aspectRatio: 2.0,
child: Container(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 4,
strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor,
),
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
DefaultTextStyle(
style: context.textTheme.bodyLarge!.copyWith(
color: ColorsManager.blackColor,
),
child: title,
),
],
),
);
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget {
const CreateSpaceButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.blue,
),
),
),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget {
final Offset offset;
final void Function() onButtonTap;
const PlusButtonWidget({
super.key,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onButtonTap,
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
class SpaceCardWidget extends StatefulWidget {
final void Function() onTap;
final Widget Function() buildSpaceContainer;
const SpaceCardWidget({
required this.onTap,
required this.buildSpaceContainer,
super.key,
});
@override
State<SpaceCardWidget> createState() => _SpaceCardWidgetState();
}
class _SpaceCardWidgetState extends State<SpaceCardWidget> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceCell extends StatelessWidget {
final String icon;
final String name;
final VoidCallback? onTap;
const SpaceCell({
super.key,
required this.icon,
required this.name,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 150,
height: 70,
decoration: _containerDecoration(),
child: Row(
children: [
_buildIconContainer(),
const SizedBox(width: 10),
Expanded(
child: Text(
name,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
Widget _buildIconContainer() {
return Container(
width: 40,
height: double.infinity,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15),
),
),
child: Center(
child: SvgPicture.asset(
icon,
colorFilter: const ColorFilter.mode(
ColorsManager.whiteColors,
BlendMode.srcIn,
),
width: 24,
height: 24,
),
),
);
}
BoxDecoration _containerDecoration() {
return BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
class SpaceManagementBody extends StatelessWidget {
const SpaceManagementBody({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
const SpaceManagementCommunitiesTree(),
Expanded(
child: BlocBuilder<CommunitiesTreeSelectionBloc,
CommunitiesTreeSelectionState>(
buildWhen: (previous, current) =>
previous.selectedCommunity != current.selectedCommunity,
builder: (context, state) => Visibility(
visible: state.selectedCommunity == null,
replacement: const SpaceManagementCommunityStructure(),
child: const SpaceManagementTemplatesView(),
),
),
),
],
);
}
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget {
const SpaceManagementCommunityStructure({super.key});
@override
Widget build(BuildContext context) {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
),
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceManagementTemplatesView extends StatelessWidget {
const SpaceManagementTemplatesView({super.key});
@override
Widget build(BuildContext context) {
return ColoredBox(
color: ColorsManager.whiteColors,
child: GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.0,
),
itemCount: _gridItems(context).length,
itemBuilder: (context, index) {
final model = _gridItems(context)[index];
return CommunityTemplateCell(
onTap: model.onTap,
title: model.title,
);
},
),
);
}
List<_CommunityTemplateModel> _gridItems(BuildContext context) {
return [
_CommunityTemplateModel(
title: const Text('Blank'),
onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context),
),
];
}
}
class _CommunityTemplateModel {
final Widget title;
final void Function() onTap;
_CommunityTemplateModel({
required this.title,
required this.onTap,
});
}

View File

@ -0,0 +1,42 @@
import 'dart:async';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
final class DebouncedCommunitiesService implements CommunitiesService {
DebouncedCommunitiesService(
this._decoratee, {
this.debounceDuration = const Duration(milliseconds: 500),
});
final CommunitiesService _decoratee;
final Duration debounceDuration;
Timer? _debounceTimer;
late Completer<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,9 +1,11 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/services/communities_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteCommunitiesService implements CommunitiesService {
const RemoteCommunitiesService(this._httpService);
@ -13,14 +15,26 @@ class RemoteCommunitiesService implements CommunitiesService {
static const _defaultErrorMessage = 'Failed to load communities';
@override
Future<List<CommunityModel>> getCommunity(LoadCommunitiesParam param) async {
Future<CommunitiesPaginationModel> getCommunity(
LoadCommunitiesParam param,
) async {
try {
return _httpService.get(
path: '/api/communities/',
expectedResponseModel: (json) => (json as List<dynamic>)
.map((e) => CommunityModel.fromJson(e as Map<String, dynamic>))
.toList(),
final response = await _httpService.get(
path: await _makeUrl(),
queryParameters: {
'page': param.page,
'size': param.size,
'includeSpaces': param.includeSpaces,
if (param.search.isNotEmpty && param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) => CommunitiesPaginationModel.fromJson(
json as Map<String, dynamic>,
CommunityModel.fromJsonList,
),
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -31,4 +45,13 @@ 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,11 +4,19 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
class CommunityModel extends Equatable {
final String uuid;
final String name;
final DateTime createdAt;
final DateTime updatedAt;
final String description;
final String externalId;
final List<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,
});
@ -16,11 +24,20 @@ class CommunityModel extends Equatable {
return CommunityModel(
uuid: json['uuid'] as String,
name: json['name'] as String,
spaces: (json['spaces'] as List<dynamic>)
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>[])
.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,26 +2,37 @@ import 'package:equatable/equatable.dart';
class SpaceModel extends Equatable {
final String uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String spaceName;
final String icon;
final List<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,
spaceName: json['spaceName'] as String,
icon: json['icon'] as String,
uuid: json['uuid'] as String? ?? '',
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
updatedAt: DateTime.tryParse(json['updatedAt'] as String? ?? ''),
spaceName: json['spaceName'] as String? ?? '',
icon: json['icon'] as String? ?? 'assets/icons/location_icon.svg',
children: (json['children'] as List<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,3 +1,32 @@
class LoadCommunitiesParam {
const LoadCommunitiesParam();
import 'package:equatable/equatable.dart';
class LoadCommunitiesParam extends Equatable {
const LoadCommunitiesParam({
this.page = 1,
this.size = 25,
this.search = '',
this.includeSpaces = true,
});
final int page;
final int size;
final String search;
final bool includeSpaces;
LoadCommunitiesParam copyWith({
int? page,
int? size,
String? search,
bool? includeSpaces,
}) {
return LoadCommunitiesParam(
page: page ?? this.page,
size: size ?? this.size,
search: search ?? this.search,
includeSpaces: includeSpaces ?? this.includeSpaces,
);
}
@override
List<Object?> get props => [page, size, search, includeSpaces];
}

View File

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

View File

@ -14,6 +14,8 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
}) : _communitiesService = communitiesService,
super(const CommunitiesState()) {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<InsertCommunity>(_onInsertCommunity);
}
final CommunitiesService _communitiesService;
@ -23,28 +25,93 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
Emitter<CommunitiesState> emit,
) async {
try {
emit(const CommunitiesState(status: CommunitiesStatus.loading));
final communities = await _communitiesService.getCommunity(event.param);
emit(
state.copyWith(status: CommunitiesStatus.loading),
);
final paginationResponse = await _communitiesService.getCommunity(
event.param,
);
emit(
CommunitiesState(
status: CommunitiesStatus.success,
communities: communities,
communities: paginationResponse.data,
hasNext: paginationResponse.hasNext,
currentPage: paginationResponse.page,
searchQuery: event.param.search,
isLoadingMore: false,
),
);
} on APIException catch (e) {
emit(
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.message,
),
);
_onApiException(e, emit);
} catch (e) {
emit(
CommunitiesState(
status: CommunitiesStatus.failure,
errorMessage: e.toString(),
),
);
_onError(e, emit);
}
}
Future<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,
),
);
} on APIException catch (e) {
_onApiException(e, emit);
} catch (e) {
_onError(e, emit);
}
}
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,3 +15,19 @@ 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,12 +7,48 @@ final class CommunitiesState extends Equatable {
this.status = CommunitiesStatus.initial,
this.communities = const [],
this.errorMessage,
this.isLoadingMore = false,
this.hasNext = false,
this.currentPage = 1,
this.searchQuery = '',
});
final CommunitiesStatus status;
final List<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];
List<Object?> get props => [
status,
communities,
errorMessage,
isLoadingMore,
hasNext,
currentPage,
searchQuery,
];
}

View File

@ -0,0 +1,47 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
part 'communities_tree_selection_event.dart';
part 'communities_tree_selection_state.dart';
class CommunitiesTreeSelectionBloc
extends Bloc<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: event.community,
selectedSpace: event.space,
),
);
}
void _onClearSelection(
ClearCommunitiesTreeSelectionEvent event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
emit(const CommunitiesTreeSelectionState());
}
}

View File

@ -0,0 +1,31 @@
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;
final CommunityModel community;
const SelectSpaceEvent({required this.space, required this.community});
@override
List<Object?> get props => [space];
}
final class ClearCommunitiesTreeSelectionEvent
extends CommunitiesTreeSelectionEvent {
const ClearCommunitiesTreeSelectionEvent();
}

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class CommunityTile extends StatelessWidget {
final String title;
final List<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

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

View File

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/communities_tree_failure_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/empty_communities_tree_search_result_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree_community_tile.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_communities_list.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_header.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementCommunitiesTree extends StatefulWidget {
const SpaceManagementCommunitiesTree({super.key});
@override
State<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

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

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_tile.dart';
class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
const SpaceManagementCommunitiesTreeSpaceTile({
required this.space,
required this.community,
super.key,
});
final SpaceModel space;
final CommunityModel community;
@override
Widget build(BuildContext context) {
final spaceIsExpanded = _isSpaceOrChildSelected(context, space);
final isSelected =
context.watch<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(community: community, space: space),
),
children: space.children
.map(
(childSpace) => SpaceManagementCommunitiesTreeSpaceTile(
space: childSpace,
community: community,
),
)
.toList(),
),
);
}
bool _isSpaceOrChildSelected(BuildContext context, SpaceModel space) {
final selectedSpace =
context.read<CommunitiesTreeSelectionBloc>().state.selectedSpace;
final isSpaceSelected = selectedSpace?.uuid == space.uuid;
final anySubSpaceIsSelected = space.children.any(
(child) => _isSpaceOrChildSelected(context, child),
);
return isSpaceSelected || anySubSpaceIsSelected;
}
}

View File

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

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceManagementSidebarCommunitiesList extends StatefulWidget {
const SpaceManagementSidebarCommunitiesList({
required this.communities,
required this.itemBuilder,
this.onLoadMore,
this.isLoadingMore = false,
this.hasNext = false,
super.key,
});
final List<CommunityModel> communities;
final Widget Function(BuildContext context, int index) itemBuilder;
final VoidCallback? onLoadMore;
final bool isLoadingMore;
final bool hasNext;
@override
State<SpaceManagementSidebarCommunitiesList> createState() =>
_SpaceManagementSidebarCommunitiesListState();
}
class _SpaceManagementSidebarCommunitiesListState
extends State<SpaceManagementSidebarCommunitiesList> {
late final ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
if (widget.hasNext && !widget.isLoadingMore && widget.onLoadMore != null) {
widget.onLoadMore!();
}
}
}
bool _onNotification(ScrollEndNotification notification) {
final hasReachedEnd = notification.metrics.extentAfter == 0;
if (hasReachedEnd &&
widget.hasNext &&
!widget.isLoadingMore &&
widget.onLoadMore != null) {
widget.onLoadMore!();
return true;
}
return false;
}
@override
void dispose() {
_scrollController
..removeListener(_onScroll)
..dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final itemCount = widget.communities.length + (widget.isLoadingMore ? 1 : 0);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: context.screenWidth * 0.5,
child: Scrollbar(
scrollbarOrientation: ScrollbarOrientation.left,
thumbVisibility: true,
controller: _scrollController,
child: NotificationListener<ScrollEndNotification>(
onNotification: _onNotification,
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsetsDirectional.only(start: 16),
itemCount: itemCount,
controller: _scrollController,
itemBuilder: (context, index) {
if (index == widget.communities.length) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(),
),
);
}
return widget.itemBuilder(context, index);
},
),
),
),
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class SpaceManagementSidebarHeader extends StatelessWidget {
const SpaceManagementSidebarHeader({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Communities',
style: context.textTheme.titleMedium?.copyWith(
color: ColorsManager.blackColor,
),
),
SpaceManagementSidebarAddCommunityButton(
onTap: () => _onAddCommunity(context),
),
],
),
);
}
void _onAddCommunity(BuildContext context) {
final bloc = context.read<CommunitiesTreeSelectionBloc>();
final selectedCommunity = bloc.state.selectedCommunity;
final isSelected = selectedCommunity?.uuid.isNotEmpty ?? false;
if (isSelected) {
_clearSelection(context);
} else {
SpaceManagementCommunityDialogHelper.showCreateDialog(context);
}
}
void _clearSelection(BuildContext context) {
context.read<CommunitiesTreeSelectionBloc>().add(
const ClearCommunitiesTreeSelectionEvent(),
);
}
}

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class SpaceTile extends StatefulWidget {
final String title;
final bool isSelected;
final bool initiallyExpanded;
final ValueChanged<bool> onExpansionChanged;
final List<Widget>? children;
final void Function() onItemSelected;
const SpaceTile({
super.key,
required this.title,
required this.initiallyExpanded,
required this.onExpansionChanged,
required this.onItemSelected,
required this.isSelected,
this.children,
});
@override
State<SpaceTile> createState() => _SpaceTileState();
}
class _SpaceTileState extends State<SpaceTile> {
late bool _isExpanded;
@override
void initState() {
super.initState();
_isExpanded = widget.initiallyExpanded;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
child: CustomExpansionTile(
isSelected: widget.isSelected,
title: widget.title,
initiallyExpanded: _isExpanded,
onItemSelected: widget.onItemSelected,
onExpansionChanged: (bool expanded) {
setState(() {
_isExpanded = expanded;
});
widget.onExpansionChanged(expanded);
},
children: widget.children ?? [],
),
);
}
}

View File

@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/services/create_community_service.dart';
@ -16,24 +17,51 @@ class RemoteCreateCommunityService implements CreateCommunityService {
Future<CommunityModel> createCommunity(CreateCommunityParam param) async {
try {
final response = await _httpService.post(
path: 'endpoint',
expectedResponseModel: (data) => CommunityModel.fromJson(
data as Map<String, dynamic>,
),
path: await _makeUrl(),
body: {
'name': param.name,
'description': param.description,
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>;
if (json['success'] == true) {
return CommunityModel.fromJson(
json['data'] as Map<String, dynamic>,
);
}
return null;
},
);
if (response == null) {
throw APIException(
_getErrorMessageFromBody(response as Map<String, dynamic>?),
);
}
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) {
return _defaultErrorMessage;
}
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
return '/projects/$projectUuid/communities';
}
}

View File

@ -1,9 +1,13 @@
import 'package:equatable/equatable.dart';
class CreateCommunityParam extends Equatable {
const CreateCommunityParam({required this.name});
const CreateCommunityParam({
required this.name,
this.description = '',
});
final String name;
final String description;
@override
List<Object> get props => [name];

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class CreateCommunityDialog extends StatelessWidget {
final void Function(CommunityModel community) onCreateCommunity;
final String? initialName;
final Widget title;
const CreateCommunityDialog({
super.key,
required this.onCreateCommunity,
required this.title,
this.initialName,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
listener: (context, state) {
switch (state) {
case CreateCommunityLoading():
showDialog<void>(
context: context,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
break;
case CreateCommunitySuccess(:final community):
Navigator.of(context).pop();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Community created successfully')),
);
onCreateCommunity.call(community);
break;
case CreateCommunityFailure():
Navigator.of(context).pop();
break;
default:
break;
}
},
child: CreateCommunityDialogWidget(
title: title,
initialName: initialName,
),
),
);
}
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateCommunityDialogWidget extends StatefulWidget {
final String? initialName;
final Widget title;
const CreateCommunityDialogWidget({
super.key,
required this.title,
this.initialName,
});
@override
State<CreateCommunityDialogWidget> createState() =>
_CreateCommunityDialogWidgetState();
}
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
late final TextEditingController _nameController;
@override
void initState() {
_nameController = TextEditingController(text: widget.initialName ?? '');
super.initState();
}
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
backgroundColor: ColorsManager.transparentColor,
child: Container(
width: MediaQuery.of(context).size.width * 0.3,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withValues(alpha: 0.25),
blurRadius: 20,
spreadRadius: 5,
offset: const Offset(0, 5),
),
],
),
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: Theme.of(context).textTheme.headlineMedium!,
child: widget.title,
),
const SizedBox(height: 18),
CreateCommunityNameTextField(
nameController: _nameController,
),
if (state case CreateCommunityFailure(:final message))
Padding(
padding: const EdgeInsets.only(top: 18),
child: SelectableText(
'* $message',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
const SizedBox(height: 24),
_buildActionButtons(context),
],
);
},
),
),
),
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: CancelButton(
label: 'Cancel',
onPressed: () => Navigator.of(context).pop(),
),
),
const SizedBox(width: 16),
_buildCreateCommunityButton(context),
],
);
}
Widget _buildCreateCommunityButton(BuildContext context) {
return Expanded(
child: DefaultButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_onSubmit(context);
}
},
borderRadius: 10,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
),
);
}
void _onSubmit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
context.read<CreateCommunityBloc>().add(
CreateCommunity(
CreateCommunityParam(
name: _nameController.text.trim(),
),
),
);
}
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CreateCommunityNameTextField extends StatelessWidget {
const CreateCommunityNameTextField({
required this.nameController,
super.key,
});
final TextEditingController nameController;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: nameController,
validator: _validator,
style: context.textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the community name',
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: _buildBorder(ColorsManager.boxColor),
focusedBorder: _buildBorder(),
focusedErrorBorder: _buildBorder(Theme.of(context).colorScheme.error),
errorBorder: _buildBorder(Theme.of(context).colorScheme.error),
),
);
}
String? _validator(String? value) {
if (value == null || value.isEmpty) {
return '*Name should not be empty.';
}
return null;
}
InputBorder _buildBorder([Color? color]) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: color ?? ColorsManager.vividBlue.withValues(alpha: 0.5),
width: 1,
),
);
}
}

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => const SpaceDetailsDialog(),
);
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class SpaceDetailsDialog extends StatelessWidget {
const SpaceDetailsDialog({super.key});
@override
Widget build(BuildContext context) {
return const Dialog(
child: Text('Create Space'),
);
}
}

View File

@ -4,11 +4,10 @@ import 'package:syncrow_web/common/widgets/custom_expansion_tile.dart';
class SpaceTile extends StatefulWidget {
final String title;
final bool isSelected;
final bool initiallyExpanded;
final ValueChanged<bool> onExpansionChanged;
final List<Widget>? children;
final Function() onItemSelected;
final void Function() onItemSelected;
const SpaceTile({
super.key,

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart';
@ -386,4 +387,34 @@ class DevicesManagementApi {
);
return response;
}
Future<bool> postSchedule({
required String category,
required String deviceId,
required String time,
required String code,
required bool value,
required List<String> days,
}) async {
final response = await HTTPService().post(
path: ApiEndpoints.saveSchedule.replaceAll('{deviceUuid}', deviceId),
showServerMessage: false,
body: jsonEncode(
{
'category': category,
'time': time,
'function': {
'code': code,
'value': value,
},
'days': days
},
),
expectedResponseModel: (json) {
return json['success'] ?? false;
},
);
return response;
}
}

View File

@ -46,6 +46,7 @@ abstract class ApiEndpoints {
// Community Module
static const String createCommunity = '/projects/{projectId}/communities';
static const String getCommunityList = '/projects/{projectId}/communities';
static const String getCommunityListv2 = '/projects/{projectId}/communities/v2';
static const String getCommunityById =
'/projects/{projectId}/communities/{communityId}';
static const String updateCommunity =
@ -136,4 +137,5 @@ abstract class ApiEndpoints {
static const String assignDeviceToRoom =
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
static const String saveSchedule = '/schedule/{deviceUuid}';
}