Compare commits

..

17 Commits

Author SHA1 Message Date
04af38e169 Add step to install Node.js dependencies in production workflow 2025-07-14 00:32:57 -06:00
8bb609cab1 Update Flutter SDK version to 3.32.1 in production workflow 2025-07-14 00:26:35 -06:00
00529b102b add prod yml file 2025-07-14 00:22:30 -06:00
bb3aa0eac6 Add Flutter Web build and workflow 2025-07-14 00:10:30 -06:00
932e99c0a9 Add build script and update package dependencies for CDK deployment 2025-07-09 05:27:46 -06:00
99b13ee062 Adds CDK deployment instructions and updates region
Enhances the README with CDK deployment commands.
2025-07-07 09:38:09 +03:00
337e79b770 a working web stack hosted at app.syncrow.me 2025-06-29 20:47:00 -04:00
774a1533f5 Dev (#314)
<!--
  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

Release


## 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
2025-06-29 16:47:14 +03:00
403b45c826 show aqi tab on analytics page. 2025-06-29 10:58:16 +03:00
ed2b91d380 MVP release (#306)
<!--
  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

MVP Release

## 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
2025-06-29 10:56:48 +03:00
5bb3688a51 Dev (#276)
<!--
  Thanks for contributing!

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

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

analytics hotfixes.

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-19 11:26:43 +03:00
d6fcf051c6 Dev (#273)
<!--
  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
remove duplicate Feature from space Management  

## Description

no need for feature of duplicate now

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-19 09:25:51 +03:00
5544430efa Dev (#270)
<!--
  Thanks for contributing!

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

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

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

## Description

Includes a few critical hot fixes.

## 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-1661]:
https://syncrow.atlassian.net/browse/SP-1661?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-18 16:02:53 +03:00
ae89a24a4b Dev (#265)
<!--
  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: -->

- [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
2025-06-18 11:01:09 +03:00
ecc01a1eb3 hides air quality tab. (#262)
<!--
  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

Hides AQI tab in analytics dashboard for release purposes.

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [x]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-17 13:30:36 +03:00
921da20d3f hides air quality tab. 2025-06-17 13:28:22 +03:00
2fed2d9de3 Sprint 20 STG Release (#261)
<!--
  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

Sprint 20 staging release.
## 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
2025-06-17 13:26:01 +03:00
145 changed files with 2342 additions and 4815 deletions

View File

@ -1,2 +1,2 @@
ENV_NAME=production
BASE_URL=https://syncrow-staging.azurewebsites.net
BASE_URL=https://api.syncrow.me

48
.github/workflows/production.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: 🚀 Flutter Web Production Deployment
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
env:
AWS_DEFAULT_REGION: me-central-1
steps:
- name: ⬇️ Checkout Code
uses: actions/checkout@v4
- name: 🐢 Set up Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: "20"
- name: 📦 Install Node.js Dependencies (CDK + TypeScript)
run: npm install
- name: 🐳 Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 🔐 Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_DEFAULT_REGION }}
- name: 🎯 Set up Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: "3.32.1"
- name: 📦 Install Flutter Dependencies
run: flutter pub get
- name: 🛠️ Run Flutter Build & CDK Deploy
run: |
chmod +x ./build.sh
./build.sh

4
.gitignore vendored
View File

@ -42,3 +42,7 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
node_modules
cdk.out
web-cdk.out

View File

@ -24,4 +24,13 @@ samples, guidance on mobile development, and a full API reference.
- run command: `flutter run -d chrome --target=lib/main_dev.dart`
## CDK Deployment
• Bootstrap CDK (first time only): npx cdk bootstrap aws://482311766496/me-central-1
• List available stacks: npx cdk list
• Deploy web: npx cdk deploy --require-approval never
• View changes before deploy: npx cdk diff
• Generate CloudFormation template: npx cdk synth
• Destroy infrastructure: npx cdk destroy
• Web infrastructure is configured in infrastructure/web-stack.ts
• After code changes: build Flutter web app and deploy to hosting service

View File

@ -1,8 +0,0 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<rect x="16.0922" y="66.3794" width="28.1609" height="66.3793" fill="#C7CDD1"/>
<rect x="54.3105" y="24.1379" width="28.1609" height="108.621" fill="#ABB4BA"/>
<rect x="92.5288" y="78.4484" width="28.1609" height="54.3103" fill="#C7CDD1"/>
<rect x="130.747" y="48.2759" width="28.1609" height="84.4828" fill="#ABB4BA"/>
</svg>

Before

Width:  |  Height:  |  Size: 583 B

View File

@ -1,5 +0,0 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="3.05394e-05" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9349 68.5 24.5C94.2816 23.999 80.7136 78.5065 106.5 78.5C125.715 78.4952 131.5 48.5 145.5 48.5C159.5 48.5 156.5 96 171.5 96" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 520 B

View File

@ -1,7 +0,0 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9348 68.5 24.5C94.2816 23.999 80.7136 78.5064 106.5 78.5C125.715 78.4951 131.5 48.5 145.5 48.5C159.5 48.5 156.5 95.9999 171.5 95.9999" stroke="#ABB4BA" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
<path d="M1.5 132.5C13 132.5 6.58852 78.4999 29.5 78.4999C46.5 78.4999 45.6214 44.9349 68 44.5C93.7816 43.999 80.7136 27.0065 106.5 27C125.715 26.9952 131.5 63.5 145.5 63.5C159.5 63.5 156.5 113.5 171.5 113.5" stroke="#C7CDD1" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
<path d="M1.5 132.5C13 132.5 6.58852 85.9999 29.5 85.9999C46.5 85.9999 45.6214 11.9348 68 11.4999C93.7816 10.9989 80.7136 43.5064 106.5 43.4999C125.715 43.4951 131.5 35.4999 145.5 35.4999C159.5 35.4999 156.5 105.5 171.5 105.5" stroke="#D5D5D5" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,99 +0,0 @@
<svg width="181" height="121" viewBox="0 0 181 121" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="15.5" y1="-2.52181e-08" x2="15.5" y2="120" stroke="#B9B9B9"/>
<line x1="45.5" y1="-2.52181e-08" x2="45.5" y2="120" stroke="#B9B9B9"/>
<line x1="75.5" y1="-2.52181e-08" x2="75.5" y2="120" stroke="#B9B9B9"/>
<line x1="105.5" y1="-2.52181e-08" x2="105.5" y2="120" stroke="#B9B9B9"/>
<line x1="135.5" y1="-2.52181e-08" x2="135.5" y2="120" stroke="#B9B9B9"/>
<line x1="165.5" y1="-2.52181e-08" x2="165.5" y2="120" stroke="#B9B9B9"/>
<line x1="30.5" y1="-2.52181e-08" x2="30.5" y2="120" stroke="#B9B9B9"/>
<line x1="60.5" y1="-2.52181e-08" x2="60.5" y2="120" stroke="#B9B9B9"/>
<line x1="90.5" y1="-2.52181e-08" x2="90.5" y2="120" stroke="#B9B9B9"/>
<line x1="120.5" y1="-2.52181e-08" x2="120.5" y2="120" stroke="#B9B9B9"/>
<line x1="150.5" y1="-2.52181e-08" x2="150.5" y2="120" stroke="#B9B9B9"/>
<line x1="180.5" y1="-2.52181e-08" x2="180.5" y2="120" stroke="#B9B9B9"/>
<line x1="181" y1="120.5" x2="-4.52101e-08" y2="120.5" stroke="#B9B9B9"/>
<line x1="181" y1="60.5" x2="-4.52101e-08" y2="60.5" stroke="#B9B9B9"/>
<line x1="181" y1="90.5" x2="-4.52101e-08" y2="90.5" stroke="#B9B9B9"/>
<line x1="181" y1="30.5" x2="-4.52101e-08" y2="30.5" stroke="#B9B9B9"/>
<line x1="181" y1="105.5" x2="-4.52101e-08" y2="105.5" stroke="#B9B9B9"/>
<line x1="181" y1="45.5" x2="-4.52101e-08" y2="45.5" stroke="#B9B9B9"/>
<line x1="181" y1="75.5" x2="-4.52101e-08" y2="75.5" stroke="#B9B9B9"/>
<line x1="181" y1="15.5" x2="-4.52101e-08" y2="15.5" stroke="#B9B9B9"/>
<rect x="16" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="46" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="61" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="76" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="91" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="106" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="121" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="136" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="151" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="166" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="31" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="31" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="46" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="46" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="61" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="76" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="91" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="106" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="121" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="136" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="61" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="76" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="76" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="91" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="91" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="46" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="61" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="76" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="91" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="106" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="121" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="136" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="151" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="166" y="106" width="14" height="14" fill="#F2F2F2"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,7 +0,0 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 95.9999C13 95.9999 6.58853 66.4999 29.5 66.4999C46.5 66.4999 46.1214 34.9348 68.5 34.4999C94.2816 33.9989 80.7136 65.0065 106.5 65C125.715 64.9952 131.5 50.5 145.5 50.5C159.5 50.5 156.5 70.5 171.5 70.5" stroke="#C7CDD1" stroke-width="2" stroke-linecap="round"/>
<path d="M1.5 106C13 106 6.58853 76.4999 29.5 76.4999C46.5 76.4999 46.1214 44.9348 68.5 44.4999C94.2816 43.9989 80.7136 75.0065 106.5 75C125.715 74.9952 131.5 60.5 145.5 60.5C159.5 60.5 156.5 80.5 171.5 80.5" stroke="#F2F2F2" stroke-width="2" stroke-linecap="round"/>
<path d="M1.5 116C13 116 6.58853 86.4999 29.5 86.4999C46.5 86.4999 46.1214 54.9348 68.5 54.4999C94.2816 53.9989 80.7136 85.0065 106.5 85C125.715 84.9952 131.5 70.5 145.5 70.5C159.5 70.5 156.5 90.5 171.5 90.5" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,15 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9717_7433)">
<path d="M17.1131 10.6766H15.5664C15.7241 11.1083 15.8102 11.5741 15.8102 12.0596V17.9053C15.8102 18.1077 15.775 18.302 15.7109 18.4827H18.2679C19.2231 18.4827 20.0002 17.7056 20.0002 16.7505V13.5637C20.0002 11.9718 18.7051 10.6766 17.1131 10.6766Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M4.19005 12.0596C4.19005 11.5741 4.27618 11.1083 4.43384 10.6766H2.88712C1.29516 10.6766 0 11.9718 0 13.5637V16.7505C0 17.7057 0.777072 18.4828 1.73227 18.4828H4.28938C4.22528 18.302 4.19005 18.1077 4.19005 17.9053V12.0596Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M11.7679 9.17249H8.23184C6.63989 9.17249 5.34473 10.4676 5.34473 12.0596V17.9053C5.34473 18.2242 5.60324 18.4827 5.92215 18.4827H14.0776C14.3965 18.4827 14.655 18.2242 14.655 17.9053V12.0596C14.655 10.4676 13.3598 9.17249 11.7679 9.17249Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M9.99995 1.51721C8.08541 1.51721 6.52783 3.07479 6.52783 4.98937C6.52783 6.288 7.24459 7.42218 8.30311 8.01765C8.80518 8.30008 9.38401 8.46148 9.99995 8.46148C10.6159 8.46148 11.1947 8.30008 11.6968 8.01765C12.7553 7.42218 13.4721 6.28796 13.4721 4.98937C13.4721 3.07483 11.9145 1.51721 9.99995 1.51721Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M3.90284 4.75354C2.471 4.75354 1.30615 5.91839 1.30615 7.35022C1.30615 8.78206 2.471 9.94691 3.90284 9.94691C4.26604 9.94691 4.6119 9.87168 4.92608 9.73644C5.46929 9.50257 5.91718 9.08859 6.19433 8.57003C6.38886 8.20609 6.49952 7.79089 6.49952 7.35022C6.49952 5.91843 5.33468 4.75354 3.90284 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M16.0972 4.75354C14.6653 4.75354 13.5005 5.91839 13.5005 7.35022C13.5005 7.79093 13.6112 8.20612 13.8057 8.57003C14.0828 9.08863 14.5307 9.50261 15.0739 9.73644C15.3881 9.87168 15.734 9.94691 16.0972 9.94691C17.529 9.94691 18.6939 8.78206 18.6939 7.35022C18.6939 5.91839 17.529 4.75354 16.0972 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_9717_7433">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0002 5.97498L3.12109 11.2683V18.3601H8.64871V13.163H11.5852V18.3601H16.8794V11.2683L10.0002 5.97498Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M17.1673 7.15356V3.52759H14.2702V4.92485L10 1.63989L0 9.33274L1.38043 11.1271L10 4.49458L18.6196 11.1272L20 9.33278L17.1673 7.15356Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@ -1,4 +0,0 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.8033 16.3567C37.2313 15.7848 36.3039 15.7847 35.7318 16.3568L21.5532 30.5353L14.2681 23.2504C13.6962 22.6784 12.7686 22.6783 12.1966 23.2505C11.6246 23.8226 11.6246 24.75 12.1966 25.3221L20.5174 33.6427C20.8034 33.9287 21.1783 34.0717 21.5531 34.0717C21.928 34.0717 22.3029 33.9287 22.5888 33.6426L37.8033 18.4283C38.3754 17.8563 38.3754 16.9288 37.8033 16.3567Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M42.6776 7.32236C37.9558 2.60049 31.6776 0 25 0C18.3223 0 12.0442 2.60049 7.32236 7.32236C2.60039 12.0443 0 18.3224 0 25C0 31.6778 2.60039 37.9559 7.32236 42.6777C12.0441 47.3996 18.3223 50 25 50C31.6777 50 37.9558 47.3996 42.6776 42.6777C47.3995 37.9559 50 31.6778 50 25C50 18.3224 47.3995 12.0443 42.6776 7.32236ZM25 47.0703C12.8304 47.0703 2.92969 37.1696 2.92969 25C2.92969 12.8304 12.8304 2.92969 25 2.92969C37.1696 2.92969 47.0703 12.8304 47.0703 25C47.0703 37.1696 37.1696 47.0703 25 47.0703Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

14
bin/web-stack.ts Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { WebStack } from '../infrastructure/web-stack';
const app = new cdk.App();
new WebStack(app, 'SyncrowWebStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'me-central-1',
},
certificateArn: app.node.tryGetContext('certificateArn'),
});

17
build.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
set -e
REGION=${AWS_DEFAULT_REGION:-me-central-1}
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
STACK_NAME=SyncrowWebStack
CERTIFICATE_ARN="arn:aws:acm:us-east-1:$ACCOUNT_ID:certificate/b3ea57be-9bf0-4c66-8b01-9672ef1e8530"
echo "🧱 Building Flutter Web for PRODUCTION..."
flutter build web --target=lib/main.dart --release
echo "🚀 Deploying CDK stack..."
npx cdk deploy $STACK_NAME \
--context certificateArn=$CERTIFICATE_ARN \
--require-approval never
echo "✅ Deployment complete. Access the app at: https://app.syncrow.me"

10
cdk.context.json Normal file
View File

@ -0,0 +1,10 @@
{
"hosted-zone:account=482311766496:domainName=syncrow.me:region=us-east-2": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"hosted-zone:account=482311766496:domainName=syncrow.me:region=me-central-1": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
}
}

63
cdk.json Normal file
View File

@ -0,0 +1,63 @@
{
"app": "npx ts-node --prefer-ts-exts bin/web-stack.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableLogging": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableLogging": true,
"@aws-cdk/aws-nordicapis-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
}
}

123
infrastructure/web-stack.ts Normal file
View File

@ -0,0 +1,123 @@
import * as cdk from "aws-cdk-lib";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
export interface WebStackProps extends cdk.StackProps {
certificateArn?: string;
}
export class WebStack extends cdk.Stack {
public readonly distributionUrl: string;
public readonly bucketName: string;
constructor(scope: Construct, id: string, props?: WebStackProps) {
super(scope, id, props);
const bucketName = `syncrow-web-${this.account}-${this.region}`;
const webBucket = new s3.Bucket(this, "SyncrowWebBucket", {
bucketName,
websiteIndexDocument: "index.html",
websiteErrorDocument: "index.html",
publicReadAccess: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Use existing wildcard certificate in us-east-1 (required for CloudFront)
const webCertificate = props?.certificateArn
? acm.Certificate.fromCertificateArn(
this,
"WildcardCertificate",
props.certificateArn
)
: acm.Certificate.fromCertificateArn(
this,
"WildcardCertificate",
"arn:aws:acm:us-east-1:482311766496:certificate/b3ea57be-9bf0-4c66-8b01-9672ef1e8530"
);
// Get the hosted zone
const hostedZone = route53.HostedZone.fromLookup(this, "SyncrowZone", {
domainName: "syncrow.me",
});
const distribution = new cloudfront.Distribution(
this,
"SyncrowWebDistribution",
{
defaultBehavior: {
origin: new origins.S3Origin(webBucket),
viewerProtocolPolicy:
cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
},
domainNames: ["app.syncrow.me"],
certificate: webCertificate,
defaultRootObject: "index.html",
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
],
}
);
// Create Route 53 record for app.syncrow.me
new route53.ARecord(this, "WebAliasRecord", {
zone: hostedZone,
recordName: "app",
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(distribution)
),
});
new s3deploy.BucketDeployment(this, "SyncrowWebDeployment", {
sources: [s3deploy.Source.asset("./build/web")],
destinationBucket: webBucket,
distribution,
distributionPaths: ["/*"],
});
this.distributionUrl = "https://app.syncrow.me";
this.bucketName = bucketName;
new cdk.CfnOutput(this, "WebsiteUrl", {
value: this.distributionUrl,
description: "Web Application URL",
exportName: `${this.stackName}-WebsiteUrl`,
});
new cdk.CfnOutput(this, "CloudFrontUrl", {
value: `https://${distribution.distributionDomainName}`,
description: "CloudFront Distribution URL",
exportName: `${this.stackName}-CloudFrontUrl`,
});
new cdk.CfnOutput(this, "BucketName", {
value: this.bucketName,
description: "S3 Bucket Name",
exportName: `${this.stackName}-BucketName`,
});
new cdk.CfnOutput(this, "WildcardCertificateArn", {
value: webCertificate.certificateArn,
description: "Wildcard SSL Certificate ARN (us-east-1)",
exportName: `${this.stackName}-WildcardCertificateArn`,
});
}
}

View File

@ -1,52 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatelessWidget {
const BookingPage({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.blueGrey[100],
child: const Center(
child: Text(
'Side bar',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
)),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SizedBox(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {}),
SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {})
],
)
],
),
),
))
],
),
);
}
}

View File

@ -1,73 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SvgTextButton extends StatelessWidget {
final String svgAsset;
final String label;
final VoidCallback onPressed;
final Color backgroundColor;
final Color svgColor;
final Color labelColor;
final double borderRadius;
final List<BoxShadow> boxShadow;
final double svgSize;
const SvgTextButton({
super.key,
required this.svgAsset,
required this.label,
required this.onPressed,
this.backgroundColor = ColorsManager.circleRolesBackground,
this.svgColor = const Color(0xFF496EFF),
this.labelColor = Colors.black87,
this.borderRadius = 10.0,
this.boxShadow = const [
BoxShadow(
color: ColorsManager.textGray,
blurRadius: 12,
offset: Offset(0, 4),
),
],
this.svgSize = 24.0,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: boxShadow,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
svgAsset,
width: svgSize,
height: svgSize,
color: svgColor,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
color: labelColor,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}

View File

@ -2,86 +2,302 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
import 'package:syncrow_web/pages/common/custom_table.dart';
import 'package:syncrow_web/pages/common/date_time_widget.dart';
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
// import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/style.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AccessManagementPage extends StatefulWidget {
class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout {
const AccessManagementPage({super.key});
@override
State<AccessManagementPage> createState() => _AccessManagementPageState();
}
class _AccessManagementPageState extends State<AccessManagementPage>
with HelperResponsiveLayout {
final PageController _pageController = PageController(initialPage: 0);
int _currentPageIndex = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
child: WebScaffold(
final isLargeScreen = isLargeScreenSize(context);
final isSmallScreen = isSmallScreenSize(context);
final isHalfMediumScreen = isHafMediumScreenSize(context);
final padding =
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
return WebScaffold(
enableMenuSidebar: false,
appBarTitle: Text(
'Access Management',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
centerBody: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _switchPage(0),
child: Text(
'Access Overview',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
fontWeight: _currentPageIndex == 0
? FontWeight.w700
: FontWeight.w400,
),
),
),
TextButton(
onPressed: () => _switchPage(1),
child: Text(
'Booking System',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
fontWeight: _currentPageIndex == 1
? FontWeight.w700
: FontWeight.w400,
),
),
),
],
),
rightBody: const NavigateHomeGridView(),
scaffoldBody: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
AccessOverviewContent(),
BookingPage(),
],
scaffoldBody: BlocProvider(
create: (BuildContext context) =>
AccessBloc()..add(FetchTableData()),
child: BlocConsumer<AccessBloc, AccessState>(
listener: (context, state) {},
builder: (context, state) {
final accessBloc = BlocProvider.of<AccessBloc>(context);
final filteredData = accessBloc.filteredData;
return state is AccessLoaded
? const Center(child: CircularProgressIndicator())
: Container(
padding: padding,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilterWidget(
size: MediaQuery.of(context).size,
tabs: accessBloc.tabs,
selectedIndex: accessBloc.selectedIndex,
onTabChanged: (index) {
accessBloc.add(TabChangedEvent(index));
},
),
const SizedBox(height: 20),
if (isSmallScreen || isHalfMediumScreen)
_buildSmallSearchFilters(context, accessBloc)
else
_buildNormalSearchWidgets(context, accessBloc),
const SizedBox(height: 20),
_buildVisitorAdminPasswords(context, accessBloc),
const SizedBox(height: 20),
Expanded(
child: DynamicTable(
tableName: 'AccessManagement',
uuidIndex: 1,
withSelectAll: true,
isEmpty: filteredData.isEmpty,
withCheckBox: false,
size: MediaQuery.of(context).size,
cellDecoration: containerDecoration,
headers: const [
'Name',
'Access Type',
'Access Start',
'Access End',
'Accessible Device',
'Authorizer',
'Authorization Date & Time',
'Access Status'
],
data: filteredData.map((item) {
return [
item.passwordName,
item.passwordType.value,
accessBloc
.timestampToDate(item.effectiveTime),
accessBloc
.timestampToDate(item.invalidTime),
item.deviceName.toString(),
item.authorizerEmail.toString(),
accessBloc
.timestampToDate(item.invalidTime),
item.passwordStatus.value,
];
}).toList(),
)),
],
),
);
})));
}
Wrap _buildVisitorAdminPasswords(
BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: [
Container(
width: 205,
height: 42,
decoration: containerDecoration,
child: DefaultButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const VisitorPasswordDialog();
},
).then((v) {
if (v != null) {
accessBloc.add(FetchTableData());
}
});
},
borderRadius: 8,
child: Text(
'Create Visitor Password ',
style: context.textTheme.titleSmall!
.copyWith(color: Colors.white, fontSize: 12),
)),
),
),
// Container(
// width: 133,
// height: 42,
// decoration: containerDecoration,
// child: DefaultButton(
// borderRadius: 8,
// backgroundColor: ColorsManager.whiteColors,
// child: Text(
// 'Admin Password',
// style: context.textTheme.titleSmall!
// .copyWith(color: Colors.black, fontSize: 12),
// )),
// ),
],
);
}
void _switchPage(int index) {
setState(() => _currentPageIndex = index);
_pageController.jumpToPage(index);
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
// TimeOfDay _selectedTime = TimeOfDay.now();
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
textBaseline: TextBaseline.ideographic,
children: [
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.passwordName,
height: 43,
isRequired: false,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.emailAuthorizer,
height: 43,
isRequired: false,
textFieldName: 'Authorizer',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
child: DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
),
const SizedBox(width: 15),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 20,
runSpacing: 10,
children: [
SizedBox(
width: 300,
child: CustomWebTextField(
controller: accessBloc.passwordName,
isRequired: true,
height: 40,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
}),
),
DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
}

View File

@ -1,289 +0,0 @@
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
import 'package:syncrow_web/pages/common/custom_table.dart';
import 'package:syncrow_web/pages/common/date_time_widget.dart';
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/style.dart';
class AccessOverviewContent extends StatelessWidget
with HelperResponsiveLayout {
const AccessOverviewContent({super.key});
@override
Widget build(BuildContext context) {
final isLargeScreen = isLargeScreenSize(context);
final isSmallScreen = isSmallScreenSize(context);
final isHalfMediumScreen = isHafMediumScreenSize(context);
final padding =
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
return BlocProvider(
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
child: BlocConsumer<AccessBloc, AccessState>(
listener: (context, state) {},
builder: (context, state) {
final accessBloc = BlocProvider.of<AccessBloc>(context);
final filteredData = accessBloc.filteredData;
return state is AccessLoaded
? const Center(child: CircularProgressIndicator())
: Container(
padding: padding,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilterWidget(
size: MediaQuery.of(context).size,
tabs: accessBloc.tabs,
selectedIndex: accessBloc.selectedIndex,
onTabChanged: (index) {
accessBloc.add(TabChangedEvent(index));
},
),
const SizedBox(height: 20),
if (isSmallScreen || isHalfMediumScreen)
_buildSmallSearchFilters(context, accessBloc)
else
_buildNormalSearchWidgets(context, accessBloc),
const SizedBox(height: 20),
_buildVisitorAdminPasswords(context, accessBloc),
const SizedBox(height: 20),
Expanded(
child: DynamicTable(
tableName: 'AccessManagement',
uuidIndex: 1,
withSelectAll: true,
isEmpty: filteredData.isEmpty,
withCheckBox: false,
size: MediaQuery.of(context).size,
cellDecoration: containerDecoration,
headers: const [
'Name',
'Access Type',
'Access Start',
'Access End',
'Accessible Device',
'Authorizer',
'Authorization Date & Time',
'Access Status'
],
data: filteredData.map((item) {
return [
item.passwordName,
item.passwordType.value,
accessBloc.timestampToDate(item.effectiveTime),
accessBloc.timestampToDate(item.invalidTime),
item.deviceName.toString(),
item.authorizerEmail.toString(),
accessBloc.timestampToDate(item.invalidTime),
item.passwordStatus.value,
];
}).toList(),
)),
],
),
);
}));
}
Wrap _buildVisitorAdminPasswords(
BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: [
Container(
width: 205,
height: 42,
decoration: containerDecoration,
child: DefaultButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const VisitorPasswordDialog();
},
).then((v) {
if (v != null) {
accessBloc.add(FetchTableData());
}
});
},
borderRadius: 8,
child: Text(
'Create Visitor Password ',
style: context.textTheme.titleSmall!
.copyWith(color: Colors.white, fontSize: 12),
)),
),
// Container(
// width: 133,
// height: 42,
// decoration: containerDecoration,
// child: DefaultButton(
// borderRadius: 8,
// backgroundColor: ColorsManager.whiteColors,
// child: Text(
// 'Admin Password',
// style: context.textTheme.titleSmall!
// .copyWith(color: Colors.black, fontSize: 12),
// )),
// ),
],
);
}
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
// TimeOfDay _selectedTime = TimeOfDay.now();
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
textBaseline: TextBaseline.ideographic,
children: [
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.passwordName,
height: 43,
isRequired: false,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.emailAuthorizer,
height: 43,
isRequired: false,
textFieldName: 'Authorizer',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
child: DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
),
const SizedBox(width: 15),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 20,
runSpacing: 10,
children: [
SizedBox(
width: 300,
child: CustomWebTextField(
controller: accessBloc.passwordName,
isRequired: true,
height: 40,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
}),
),
DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
}

View File

@ -39,12 +39,8 @@ class AnalyticsDevice {
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: json['spaceUuid'] as String?,
latitude: json['lat'] != null && json['lat'] != ''
? double.tryParse(json['lat']?.toString() ?? '0.0')
: null,
longitude: json['lon'] != null && json['lon'] != ''
? double.tryParse(json['lon']?.toString() ?? '0.0')
: null,
latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null,
longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null,
);
}
}

View File

@ -20,7 +20,6 @@ class AqiDistributionChart extends StatelessWidget {
return BarChart(
BarChartData(
maxY: 100.1,
alignment: BarChartAlignment.start,
gridData: EnergyManagementChartsHelper.gridData(
horizontalInterval: 20,
),

View File

@ -3,9 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiDistributionChartBox extends StatelessWidget {
@ -34,20 +32,8 @@ class AqiDistributionChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Visibility(
visible: state.chartData.isNotEmpty,
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == AirQualityDistributionStatus.loading,
isError: state.status == AirQualityDistributionStatus.failure,
isInitial: state.status == AirQualityDistributionStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyBarredChart,
),
child: Expanded(
child: AqiDistributionChart(
chartData: state.chartData,
),
),
Expanded(
child: AqiDistributionChart(chartData: state.chartData),
),
],
),

View File

@ -3,9 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class RangeOfAqiChartBox extends StatelessWidget {
@ -34,20 +32,10 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Visibility(
visible: state.filteredRangeOfAqi.isNotEmpty,
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == RangeOfAqiStatus.loading,
isError: state.status == RangeOfAqiStatus.failure,
isInitial: state.status == RangeOfAqiStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyRangeOfAqi,
),
child: Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
),
Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
),
),
],

View File

@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: MediaQuery.sizeOf(context).height * 1.05,
height: MediaQuery.sizeOf(context).height * 1,
child: const Column(
children: [
Expanded(

View File

@ -5,10 +5,8 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
@ -56,24 +54,8 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(height: 0),
const SizedBox(height: 20),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData
.every((e) => e.energy.every((e) => e.value != 0)),
replacement: AnalyticsChartEmptyStateWidget(
isLoading:
state.status == EnergyConsumptionPerDeviceStatus.loading,
isError: state.status == EnergyConsumptionPerDeviceStatus.failure,
isInitial:
state.status == EnergyConsumptionPerDeviceStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyEnergyManagementPerDevice,
),
child: Expanded(
child: EnergyConsumptionPerDeviceChart(
chartData: state.chartData,
),
),
Expanded(
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
),
],
),

View File

@ -3,10 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class TotalEnergyConsumptionChartBox extends StatelessWidget {
@ -43,18 +41,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData.every((e) => e.value != 0),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
isError: state.status == TotalEnergyConsumptionStatus.failure,
isInitial: state.status == TotalEnergyConsumptionStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyEnergyManagementChart,
),
child: TotalEnergyConsumptionChart(chartData: state.chartData),
),
TotalEnergyConsumptionChart(chartData: state.chartData),
],
),
),

View File

@ -18,7 +18,6 @@ class OccupancyChart extends StatelessWidget {
return BarChart(
BarChartData(
maxY: 100.001,
alignment: BarChartAlignment.start,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 20,

View File

@ -6,10 +6,8 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyChartBox extends StatelessWidget {
@ -69,24 +67,7 @@ class OccupancyChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData.every(
(e) => e.occupancy.isNotEmpty,
),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == OccupancyStatus.loading,
isError: state.status == OccupancyStatus.failure,
isInitial: state.status == OccupancyStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyBarredChart,
),
child: Expanded(
child: OccupancyChart(
chartData: state.chartData,
),
),
),
Expanded(child: OccupancyChart(chartData: state.chartData)),
],
),
);

View File

@ -6,10 +6,8 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyHeatMapBox extends StatelessWidget {
@ -70,29 +68,16 @@ class OccupancyHeatMapBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Visibility(
visible: state.heatMapData.isNotEmpty &&
state.heatMapData.every(
(e) => e.countTotalPresenceDetected != 0,
),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == OccupancyHeatMapStatus.loading,
isError: state.status == OccupancyHeatMapStatus.failure,
isInitial: state.status == OccupancyHeatMapStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyHeatmap,
),
child: Expanded(
child: OccupancyHeatMap(
selectedDate:
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
Expanded(
child: OccupancyHeatMap(
selectedDate:
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
),
),
),
),
],

View File

@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
'reverse',
queryParameters: {
'format': 'json',
'lat': 25.1880567,
'lon': 55.266608,
'lat': param.latitude,
'lon': param.longitude,
},
);

View File

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsChartEmptyStateWidget extends StatelessWidget {
const AnalyticsChartEmptyStateWidget({
required this.iconPath,
this.isLoading = false,
this.isError = false,
this.isInitial = false,
this.errorMessage,
this.noDataMessage = 'No data to display',
this.initialMessage = 'Please select a space to see data',
super.key,
});
final bool isLoading;
final bool isError;
final bool isInitial;
final String? errorMessage;
final String noDataMessage;
final String initialMessage;
final String iconPath;
@override
Widget build(BuildContext context) {
return Expanded(
child: _buildWidgetBasedOnState(context),
);
}
Widget _buildWidgetBasedOnState(BuildContext context) {
final widgetsMap = {
isLoading: const AppLoadingIndicator(),
isInitial: _buildState(context, initialMessage),
isError: _buildState(context, errorMessage ?? 'Something went wrong'),
};
return widgetsMap[true] ?? _buildState(context, noDataMessage);
}
Widget _buildState(BuildContext context, String message) {
return Center(
child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 16),
Expanded(child: SvgPicture.asset(iconPath, fit: BoxFit.contain)),
SelectableText(
message,
style: isError
? context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.red,
fontSize: 16,
fontWeight: FontWeight.w700,
)
: null,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@ -50,9 +50,6 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
static const double _fixedRowHeight = 60;
static const double _checkboxColumnWidth = 50;
static const double _settingsColumnWidth = 100;
@override
void initState() {
@ -70,6 +67,7 @@ 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++) {
@ -106,130 +104,73 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
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: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: Scrollbar(
//fixed the horizontal scrollbar issue
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) =>
notif.metrics.axis == Axis.horizontal,
notificationPredicate: (notif) => notif.depth == 1,
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)),
controller: _verticalScrollController,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: widget.size.width,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(
color: ColorsManager.boxColor,
),
],
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
})
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
],
),
),
),
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) {
SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (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,
),
],
),
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(),
],
);
},
}),
),
),
),
],
),
],
),
),
),
),
@ -269,10 +210,9 @@ class _DynamicTableState extends State<DynamicTable> {
],
),
);
Widget _buildSelectAllCheckbox(double width) {
Widget _buildSelectAllCheckbox() {
return Container(
width: width,
width: 50,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
@ -287,11 +227,11 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildRowCheckbox(int index, double width) {
Widget _buildRowCheckbox(int index, double size) {
return Container(
width: width,
width: 50,
padding: const EdgeInsets.all(8.0),
height: _fixedRowHeight,
height: size,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
@ -313,47 +253,50 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableHeaderCell(String title, double width) {
return Container(
width: width,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
),
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,
Widget _buildTableHeaderCell(String title, int index) {
return Expanded(
child: Container(
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,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _buildTableCell(String content,
{required double width,
required int rowIndex,
required int columnIndex}) {
Widget _buildTableCell(String content, double size,
{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';
if (isSettingsColumn) {
return buildSettingsIcon(
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
width: 120,
height: 60,
iconSize: 40,
onTap: () => widget.onSettingsPressed?.call(rowIndex),
);
}
Color? statusColor;
@ -377,82 +320,92 @@ class _DynamicTableState extends State<DynamicTable> {
statusColor = Colors.black;
}
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,
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,
),
),
color: Colors.white,
),
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,
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,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
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,
),
),
),
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),
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,
),
],
),
),
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,
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: 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,
),
),
),
),
),
),
),
),
],
);
}
}

View File

@ -15,8 +15,7 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class AcDeviceBatchControlView extends StatelessWidget
with HelperResponsiveLayout {
class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout {
const AcDeviceBatchControlView({super.key, required this.devicesIds});
final List<String> devicesIds;
@ -52,7 +51,7 @@ class AcDeviceBatchControlView extends StatelessWidget
deviceId: devicesIds.first,
code: 'switch',
value: state.status.acSwitch,
label: 'Thermostat',
label: 'ThermoState',
icon: Assets.ac,
onChange: (value) {
context.read<AcBloc>().add(AcBatchControlEvent(
@ -101,8 +100,8 @@ class AcDeviceBatchControlView extends StatelessWidget
),
Text(
'h',
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blackColor),
style:
context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor),
),
Text(
'30',
@ -149,8 +148,7 @@ class AcDeviceBatchControlView extends StatelessWidget
callFactoryReset: () {
context.read<AcBloc>().add(AcFactoryResetEvent(
deviceId: state.status.uuid,
factoryResetModel:
FactoryResetModel(devicesUuid: devicesIds),
factoryResetModel: FactoryResetModel(devicesUuid: devicesIds),
));
},
),

View File

@ -60,13 +60,4 @@ class Status {
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
String toJson() => json.encode(toMap());
Status copyWith({
String? code,
dynamic value,
}) {
return Status(
code: code ?? this.code,
value: value ?? this.value,
);
}
}

View File

@ -68,7 +68,6 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
children: [
Expanded(child: SpaceTreeView(
onSelect: () {
context.read<DeviceManagementBloc>().add(ResetFilters());
context.read<DeviceManagementBloc>().add(FetchDevices(context));
},
)),

View File

@ -62,10 +62,9 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
BlocProvider.of<CurtainModuleBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'Timer',
category: 'CUR_2',
code: 'control',
countdownCode: 'Timer',
deviceType: 'CUR_2',
),
));
},

View File

@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AccurteCalibratingDialog extends StatelessWidget {
final String deviceId;
@ -18,7 +17,6 @@ class AccurteCalibratingDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AccurateCalibrationDialog extends StatelessWidget {
final String deviceId;
@ -16,7 +15,6 @@ class AccurateCalibrationDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Accurate Calibration',

View File

@ -22,7 +22,6 @@ class AccurateDialogWidget extends StatelessWidget {
child: Column(
children: [
Expanded(
flex: 3,
child: Column(
children: [
Padding(
@ -44,22 +43,17 @@ class AccurateDialogWidget extends StatelessWidget {
),
),
Expanded(
flex: 5,
child: body,
),
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Expanded(child: Divider()),
const Divider(),
Row(
children: [
Expanded(
child: InkWell(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(26),
),
onTap: leftOnTap,
child: Container(
height: 40,
@ -70,9 +64,6 @@ class AccurateDialogWidget extends StatelessWidget {
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(26),
),
),
child: const Text(
'Cancel',
@ -83,9 +74,6 @@ class AccurateDialogWidget extends StatelessWidget {
),
Expanded(
child: InkWell(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(26),
),
onTap: rightOnTap,
child: Container(
height: 40,
@ -96,9 +84,6 @@ class AccurateDialogWidget extends StatelessWidget {
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(26),
),
),
child: const Text(
'Next',

View File

@ -17,7 +17,6 @@ class CalibrateCompletedDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: SizedBox(
height: 250,

View File

@ -28,7 +28,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
title,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 15,
fontSize: 17,
),
),
),
@ -42,7 +42,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
const Text('1. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 15,
fontSize: 17,
)),
SizedBox(
width: 450,
@ -50,7 +50,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
step1,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 15,
fontSize: 17,
),
),
),
@ -67,13 +67,13 @@ class NormalTextBodyForDialog extends StatelessWidget {
const Text('2. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 15,
fontSize: 17,
)),
Text(
step2,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 15,
fontSize: 17,
),
),
],

View File

@ -63,7 +63,6 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',
@ -72,7 +71,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
children: [
const Expanded(
child: Align(
alignment: Alignment.topCenter,
alignment: Alignment.center,
child: Padding(
padding: EdgeInsets.only(right: 75),
child: Text(
@ -86,21 +85,17 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
child: Align(
alignment: Alignment.center,
child: Container(
width: 130,
width: 110,
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: ColorsManager.neutralGray.withValues(
alpha: 0.5,
),
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsetsGeometry.only(left: 5),
child: NumberInputField(controller: _controller)),
child: NumberInputField(controller: _controller),
),
Expanded(
child: Text(
@ -132,7 +127,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
),
const Expanded(
child: Align(
alignment: Alignment.bottomCenter,
alignment: Alignment.center,
child: Text(
'2.click Next to Complete the calibration',
style: TextStyle(color: ColorsManager.lightGrayColor),

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class QuickCalibrationDialog extends StatelessWidget {
final int timControl;
@ -18,7 +17,6 @@ class QuickCalibrationDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Quick Calibration',

View File

@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc
emit(OneGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
@ -48,28 +48,42 @@ class OneGangGlassSwitchBloc
}
}
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
void _listenToChanges(
String deviceId,
Emitter<OneGangGlassSwitchState> emit,
) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
final stream = ref.onValue;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
stream.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
final statusList = <Status>[];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
});
} catch (_) {}
} catch (e) {
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
}
}
void _onStatusUpdated(
@ -160,10 +174,4 @@ class OneGangGlassSwitchBloc
deviceStatus = deviceStatus.copyWith(switch1: value);
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -90,8 +90,6 @@ class OneGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '1GT',
),
));
},

View File

@ -80,8 +80,6 @@ class WallLightDeviceControl extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '1G',
),
));
},

View File

@ -277,32 +277,6 @@ class SmartPowerDeviceControl extends StatelessWidget
totalConsumption: 10000,
date: blocProvider.formattedDate,
),
EnergyConsumptionPage(
formattedDate:
'${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}',
onTap: () {
blocProvider.add(SelectDateEvent(context: context));
},
widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty
? blocProvider.energyDataList
: [
EnergyData('12:00 AM', 4.0),
EnergyData('01:00 AM', 6.5),
EnergyData('02:00 AM', 3.8),
EnergyData('03:00 AM', 3.2),
EnergyData('04:00 AM', 6.0),
EnergyData('05:00 AM', 3.4),
EnergyData('06:00 AM', 5.2),
EnergyData('07:00 AM', 3.5),
EnergyData('08:00 AM', 6.8),
EnergyData('09:00 AM', 5.6),
EnergyData('10:00 AM', 3.9),
EnergyData('11:00 AM', 4.0),
],
totalConsumption: 10000,
date: blocProvider.formattedDate,
),
],
),
),

View File

@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
final success = await RemoteControlDeviceService().controlDevice(
deviceUuid: deviceId,
status: Status(
code: event.countdownCode,
code: 'countdown_1',
value: 0,
),
);
@ -80,18 +80,15 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
countdownSeconds: currentState.countdownSeconds,
selectedTime: currentState.selectedTime,
deviceId: deviceId,
scheduleMode: event.scheduleMode,
countdownHours: currentState.countdownHours,
countdownMinutes: currentState.countdownMinutes,
inchingHours: currentState.inchingHours,
inchingMinutes: currentState.inchingMinutes,
countdownRemaining: Duration.zero,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
isCountdownActive: currentState.countdownRemaining > Duration.zero,
));
}
}
@ -224,6 +221,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
deviceId,
event.category,
);
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
@ -232,6 +230,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
countdownRemaining: Duration.zero,
));
} else {
emit(ScheduleLoaded(
@ -286,22 +285,12 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
) async {
try {
if (state is ScheduleLoaded) {
Status status = Status(code: '', value: '');
if (event.deviceType == 'CUR_2') {
status = status.copyWith(
code: 'control',
value: event.functionOn == true ? 'open' : 'close');
} else {
status =
status.copyWith(code: event.category, value: event.functionOn);
}
final dateTime = DateTime.parse(event.time);
final updatedSchedule = ScheduleEntry(
scheduleId: event.scheduleId,
category: event.category,
time: getTimeStampWithoutSeconds(dateTime).toString(),
function: status,
function: Status(code: event.category, value: event.functionOn),
days: event.selectedDays,
);
final success = await DevicesManagementApi().editScheduleRecord(
@ -407,7 +396,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
final totalSeconds =
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
final code = event.mode == ScheduleModes.countdown
? event.countDownCode
? 'countdown_1'
: 'switch_inching';
final currentState = state as ScheduleLoaded;
final duration = Duration(seconds: totalSeconds);
@ -434,7 +423,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
);
if (success) {
if (code == event.countDownCode) {
if (code == 'countdown_1') {
final countdownDuration = Duration(seconds: totalSeconds);
emit(
@ -448,7 +437,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
);
if (countdownDuration.inSeconds > 0) {
_startCountdownTimer(emit, countdownDuration, event.countDownCode);
_startCountdownTimer(emit, countdownDuration);
} else {
_countdownTimer?.cancel();
emit(
@ -478,7 +467,9 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
}
void _startCountdownTimer(
Emitter<ScheduleState> emit, Duration duration, String countdownCode) {
Emitter<ScheduleState> emit,
Duration duration,
) {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
@ -488,7 +479,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
} else {
timer.cancel();
add(StopScheduleEvent(
countdownCode: countdownCode,
mode: _currentCountdown == null
? ScheduleModes.countdown
: ScheduleModes.inching,
@ -525,75 +515,70 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
int totalSeconds = 0;
final countdownItem = status.status.firstWhere(
(item) => item.code == event.countdownCode,
orElse: () => Status(code: '', value: 0),
);
totalSeconds = (countdownItem.value as int?) ?? 0;
final countdownHours = totalSeconds ~/ 3600;
final countdownMinutes = (totalSeconds % 3600) ~/ 60;
final countdownSeconds = totalSeconds % 60;
print(status.status);
final deviceStatus =
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
final isCountdownActive = totalSeconds > 0;
final isInchingActive = !isCountdownActive &&
(deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0);
final newState = state is ScheduleLoaded
? (state as ScheduleLoaded).copyWith(
scheduleMode: ScheduleModes.schedule,
countdownHours: countdownHours,
countdownMinutes: countdownMinutes,
countdownSeconds: countdownSeconds,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: isCountdownActive
? Duration(seconds: totalSeconds)
: Duration.zero,
)
: ScheduleLoaded(
scheduleMode: ScheduleModes.schedule,
schedules: const [],
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
deviceId: event.deviceId,
countdownHours: countdownHours,
countdownMinutes: countdownMinutes,
countdownSeconds: countdownSeconds,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: isCountdownActive
? Duration(seconds: totalSeconds)
: Duration.zero,
);
emit(newState);
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;
if (isCountdownActive) {
_countdownTimer?.cancel();
_currentCountdown = Duration(seconds: totalSeconds);
countdownRemaining = _currentCountdown!;
Duration? countdownRemaining;
var isCountdownActive = false;
var isInchingActive = false;
if (totalSeconds > 0) {
_startCountdownTimer(
emit, Duration(seconds: totalSeconds), event.countdownCode);
} else {
add(StopScheduleEvent(
countdownCode: event.countdownCode,
mode: ScheduleModes.countdown,
deviceId: event.deviceId,
));
}
} else {
_countdownTimer?.cancel();
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'));
}

View File

@ -91,7 +91,6 @@ class ScheduleEditEvent extends ScheduleEvent {
final String time;
final List<String> selectedDays;
final bool functionOn;
final String deviceType;
const ScheduleEditEvent({
required this.scheduleId,
@ -99,7 +98,6 @@ class ScheduleEditEvent extends ScheduleEvent {
required this.time,
required this.selectedDays,
required this.functionOn,
required this.deviceType,
});
@override
@ -109,7 +107,6 @@ class ScheduleEditEvent extends ScheduleEvent {
time,
selectedDays,
functionOn,
deviceType,
];
}
@ -141,13 +138,11 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent {
class UpdateScheduleModeEvent extends ScheduleEvent {
final ScheduleModes scheduleMode;
final String countdownCode;
const UpdateScheduleModeEvent(
{required this.scheduleMode, required this.countdownCode});
const UpdateScheduleModeEvent({required this.scheduleMode});
@override
List<Object> get props => [scheduleMode, countdownCode!];
List<Object> get props => [scheduleMode];
}
class UpdateCountdownTimeEvent extends ScheduleEvent {
@ -182,32 +177,28 @@ class StartScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final int hours;
final int minutes;
final String countDownCode;
const StartScheduleEvent({
required this.mode,
required this.hours,
required this.minutes,
required this.countDownCode,
});
@override
List<Object?> get props => [mode, hours, minutes, countDownCode];
List<Object?> get props => [mode, hours, minutes];
}
class StopScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final String deviceId;
final String countdownCode;
const StopScheduleEvent({
required this.mode,
required this.deviceId,
required this.countdownCode,
});
@override
List<Object?> get props => [mode, deviceId, countdownCode];
List<Object?> get props => [mode, deviceId];
}
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
@ -219,13 +210,11 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent {
class ScheduleFetchStatusEvent extends ScheduleEvent {
final String deviceId;
final String countdownCode;
const ScheduleFetchStatusEvent(
{required this.deviceId, required this.countdownCode});
const ScheduleFetchStatusEvent(this.deviceId);
@override
List<Object> get props => [deviceId, countdownCode];
List<Object> get props => [deviceId];
}
class DeleteScheduleEvent extends ScheduleEvent {

View File

@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState {
final int inchingSeconds;
final bool isInchingActive;
final ScheduleModes scheduleMode;
final Duration countdownRemaining;
final Duration? countdownRemaining;
final int? countdownSeconds;
const ScheduleLoaded({
@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState {
this.inchingMinutes = 0,
this.isInchingActive = false,
this.scheduleMode = ScheduleModes.countdown,
this.countdownRemaining = Duration.zero,
this.countdownRemaining,
});
ScheduleLoaded copyWith({

View File

@ -11,7 +11,6 @@ class CountdownModeButtons extends StatelessWidget {
final String deviceId;
final int hours;
final int minutes;
final String countDownCode;
const CountdownModeButtons({
super.key,
@ -19,7 +18,6 @@ class CountdownModeButtons extends StatelessWidget {
required this.deviceId,
required this.hours,
required this.minutes,
required this.countDownCode,
});
@override
@ -45,7 +43,6 @@ class CountdownModeButtons extends StatelessWidget {
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: deviceId,
countdownCode: countDownCode,
),
);
},
@ -57,10 +54,10 @@ class CountdownModeButtons extends StatelessWidget {
onPressed: () {
context.read<ScheduleBloc>().add(
StartScheduleEvent(
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
countDownCode: countDownCode),
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
),
);
},
backgroundColor: ColorsManager.primaryColor,

View File

@ -75,33 +75,23 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
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);
final displayHours =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inHours
: (isCountDown ? state.countdownHours : state.inchingHours);
_updateControllers(displayHours, displayMinutes, displaySeconds!);
final displayMinutes =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inMinutes.remainder(60)
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
final displaySeconds =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inSeconds.remainder(60)
: (isCountDown ? (state.countdownSeconds ?? 0) : 0);
_updateControllers(displayHours, displayMinutes, displaySeconds);
if (isActive &&
displayHours == 0 &&
displayMinutes == 0 &&
displaySeconds == 0) {
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: widget.deviceId,
countdownCode: '',
),
);
}

View File

@ -43,9 +43,7 @@ class InchingModeButtons extends StatelessWidget {
onPressed: () {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
deviceId: deviceId,
mode: ScheduleModes.inching,
countdownCode: ''),
deviceId: deviceId, mode: ScheduleModes.inching),
);
},
backgroundColor: Colors.red,

View File

@ -18,15 +18,11 @@ class BuildScheduleView extends StatelessWidget {
super.key,
required this.deviceUuid,
required this.category,
required this.countdownCode,
this.code,
required this.deviceType,
});
final String deviceUuid;
final String category;
final String? code;
final String? countdownCode;
final String deviceType;
@override
Widget build(BuildContext context) {
@ -35,8 +31,7 @@ class BuildScheduleView extends StatelessWidget {
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
..add(ScheduleFetchStatusEvent(deviceUuid)),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
@ -57,32 +52,28 @@ class BuildScheduleView extends StatelessWidget {
children: [
const ScheduleHeader(),
const SizedBox(height: 20),
if (deviceType == 'CUR_2')
const SizedBox()
else
ScheduleModeSelector(
countdownCode: countdownCode ?? '',
currentMode: state.scheduleMode,
),
ScheduleModeSelector(
currentMode: state.scheduleMode,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule)
ScheduleManagementUI(
deviceType: deviceType,
category: category,
deviceUuid: deviceUuid,
onAddSchedule: () async {
final entry = await ScheduleDialogHelper
.showAddScheduleDialog(context,
schedule: ScheduleEntry(
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
days: [],
),
isEdit: false,
code: code,
deviceType: deviceType);
.showAddScheduleDialog(
context,
schedule: ScheduleEntry(
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
days: [],
),
isEdit: false,
code: code,
);
if (entry != null) {
context.read<ScheduleBloc>().add(
ScheduleAddEvent(
@ -96,16 +87,14 @@ class BuildScheduleView extends StatelessWidget {
}
},
),
if (deviceType != 'CUR_2')
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(
deviceId: deviceUuid,
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
countDownCode: countdownCode ?? '',
isActive: state.isCountdownActive,
deviceId: deviceUuid,
hours: state.countdownHours,

View File

@ -5,16 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleManagementUI extends StatelessWidget {
final String deviceUuid;
final String deviceUuid;
final VoidCallback onAddSchedule;
final String category;
final String deviceType;
const ScheduleManagementUI({
super.key,
required this.deviceUuid,
required this.onAddSchedule,
required this.deviceType,
this.category = 'switch_1',
});
@ -46,11 +44,7 @@ class ScheduleManagementUI extends StatelessWidget {
),
),
const SizedBox(height: 20),
ScheduleTableWidget(
deviceUuid: deviceUuid,
category: category,
deviceType: deviceType,
),
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
],
);
}

View File

@ -7,12 +7,10 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleModeSelector extends StatelessWidget {
final ScheduleModes currentMode;
final String countdownCode;
const ScheduleModeSelector({
super.key,
required this.currentMode,
required this.countdownCode,
});
@override
@ -73,8 +71,7 @@ class ScheduleModeSelector extends StatelessWidget {
onChanged: (ScheduleModes? value) {
if (value != null) {
context.read<ScheduleBloc>().add(
UpdateScheduleModeEvent(
scheduleMode: value, countdownCode: countdownCode),
UpdateScheduleModeEvent(scheduleMode: value),
);
if (value == ScheduleModes.schedule) {
context.read<ScheduleBloc>().add(

View File

@ -12,13 +12,11 @@ import 'package:syncrow_web/utils/format_date_time.dart';
class ScheduleTableWidget extends StatelessWidget {
final String deviceUuid;
final String category;
final String deviceType;
const ScheduleTableWidget({
super.key,
required this.deviceUuid,
this.category = 'switch_1',
required this.deviceType,
});
@override
@ -27,14 +25,13 @@ class ScheduleTableWidget extends StatelessWidget {
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)..add(ScheduleGetEvent(category: category)),
child: _ScheduleTableView(deviceType),
child: _ScheduleTableView(),
);
}
}
class _ScheduleTableView extends StatelessWidget {
final String deviceType;
const _ScheduleTableView(this.deviceType);
const _ScheduleTableView();
@override
Widget build(BuildContext context) {
@ -84,7 +81,7 @@ class _ScheduleTableView extends StatelessWidget {
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20)),
),
child: _buildTableBody(state.schedules, context, deviceType));
child: _buildTableBody(state.schedules, context));
}
if (state is ScheduleError) {
return Center(child: Text(state.error));
@ -126,8 +123,7 @@ class _ScheduleTableView extends StatelessWidget {
);
}
Widget _buildTableBody(
List<ScheduleModel> schedules, BuildContext context, String deviceType) {
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
return SizedBox(
height: 200,
child: SingleChildScrollView(
@ -136,8 +132,7 @@ class _ScheduleTableView extends StatelessWidget {
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
for (int i = 0; i < schedules.length; i++)
_buildScheduleRow(schedules[i], i, context,
deviceType: deviceType),
_buildScheduleRow(schedules[i], i, context),
],
),
),
@ -160,19 +155,25 @@ class _ScheduleTableView extends StatelessWidget {
}
TableRow _buildScheduleRow(
ScheduleModel schedule, int index, BuildContext context,
{required String deviceType}) {
ScheduleModel schedule, int index, BuildContext context) {
return TableRow(
children: [
Center(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
bool temp;
if (schedule.category == 'CUR_2') {
temp = schedule.function.value == 'open' ? true : false;
} else {
temp = schedule.function.value as bool;
}
context.read<ScheduleBloc>().add(
ScheduleUpdateEntryEvent(
category: schedule.category,
scheduleId: schedule.scheduleId,
functionOn: schedule.function.value,
functionOn: temp,
// schedule.function.value,
enable: !schedule.enable,
),
);
@ -194,9 +195,8 @@ class _ScheduleTableView extends StatelessWidget {
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
if (deviceType == 'CUR_2')
Center(
child: Text(schedule.function.value == true ? 'open' : 'close'))
if (schedule.category == 'CUR_2')
Center(child: Text(schedule.function.value))
else
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
@ -206,27 +206,18 @@ class _ScheduleTableView extends StatelessWidget {
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
ScheduleDialogHelper.showAddScheduleDialog(context,
schedule: ScheduleEntry.fromScheduleModel(schedule),
isEdit: true,
deviceType: deviceType)
.then((updatedSchedule) {
ScheduleDialogHelper.showAddScheduleDialog(
context,
schedule: ScheduleEntry.fromScheduleModel(schedule),
isEdit: true,
).then((updatedSchedule) {
if (updatedSchedule != null) {
bool temp;
if (deviceType == 'CUR_2') {
updatedSchedule.function.value == 'open'
? temp = true
: temp = false;
} else {
temp = updatedSchedule.function.value;
}
context.read<ScheduleBloc>().add(
ScheduleEditEvent(
deviceType: deviceType,
scheduleId: schedule.scheduleId,
category: schedule.category,
time: updatedSchedule.time,
functionOn: temp,
functionOn: updatedSchedule.function.value,
selectedDays: updatedSchedule.days),
);
}

View File

@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc
emit(ThreeGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId);
_listenToChanges(event.deviceId, emit);
deviceStatus =
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
@ -50,28 +50,42 @@ class ThreeGangGlassSwitchBloc
}
}
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
void _listenToChanges(
String deviceId,
Emitter<ThreeGangGlassSwitchState> emit,
) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
final stream = ref.onValue;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
stream.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
final statusList = <Status>[];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
});
} catch (_) {}
} catch (e) {
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
}
}
void _onStatusUpdated(
@ -170,10 +184,4 @@ class ThreeGangGlassSwitchBloc
break;
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -111,8 +111,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '3GT',
),
));
},
@ -129,8 +127,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceId,
countdownCode: 'countdown_2',
deviceType: '3GT',
),
));
},
@ -147,8 +143,6 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_3',
deviceUuid: deviceId,
countdownCode: 'countdown_3',
deviceType: '3GT',
),
));
},

View File

@ -102,8 +102,6 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: '3G',
),
));
},
@ -120,8 +118,6 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
countdownCode: 'countdown_2',
deviceType: '3G',
),
));
},
@ -138,8 +134,6 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_3',
countdownCode: 'countdown_3',
deviceType: '3G',
),
));
},

View File

@ -1,177 +1,173 @@
import 'dart:async';
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
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/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
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/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'two_gang_glass_switch_event.dart';
part 'two_gang_glass_switch_state.dart';
part 'two_gang_glass_switch_event.dart';
part 'two_gang_glass_switch_state.dart';
class TwoGangGlassSwitchBloc
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
class TwoGangGlassSwitchBloc
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late TwoGangGlassStatusModel deviceStatus;
late TwoGangGlassStatusModel deviceStatus;
TwoGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangGlassSwitchInitial()) {
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangGlassSwitchControl>(_onControl);
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
on<TwoGangGlassFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
TwoGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangGlassSwitchInitial()) {
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangGlassSwitchControl>(_onControl);
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
on<TwoGangGlassFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
}
Future<void> _onFetchDeviceStatus(
TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchDeviceStatus(
TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref(
'device-status/$deviceId',
);
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
final statusList = <Status>[];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
} catch (_) {}
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
add(StatusUpdated(deviceStatus));
});
} catch (_) {
log(
'Error listening to changes',
name: 'TwoGangGlassSwitchBloc._listenToChanges',
);
}
}
Future<void> _onControl(
TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
Future<void> _onControl(
TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(
TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
Future<void> _onBatchControl(
TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = TwoGangGlassStatusModel.fromJson(
event.deviceIds.first,
status.status,
);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
Future<void> _onFactoryReset(
TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(TwoGangGlassSwitchError('Failed to reset device'));
} else {
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = TwoGangGlassStatusModel.fromJson(
event.deviceIds.first,
status.status,
);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangGlassSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
}
Future<void> _onFactoryReset(
TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(TwoGangGlassSwitchError('Failed to reset device'));
} else {
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangGlassSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -102,8 +102,6 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceType: '2GT',
countdownCode: 'countdown_1',
deviceUuid: deviceId,
category: 'switch_1',
),
@ -120,8 +118,6 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceType: '2GT',
countdownCode: 'countdown_2',
deviceUuid: deviceId,
category: 'switch_2',
),

View File

@ -97,8 +97,6 @@ class TwoGangBatchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceIds.first,
countdownCode: 'countdown_1',
deviceType: '2G',
),
));
},
@ -116,8 +114,6 @@ class TwoGangBatchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceIds.first,
countdownCode: 'countdown_2',
deviceType: '2G',
),
));
},
@ -125,7 +121,10 @@ class TwoGangBatchControlView extends StatelessWidget
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
// FirmwareUpdateWidget(
// deviceId: deviceIds.first,
// version: 12,
// ),
FactoryResetWidget(callFactoryReset: () {
context.read<TwoGangSwitchBloc>().add(
TwoGangFactoryReset(

View File

@ -103,8 +103,6 @@ class TwoGangDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: '2G',
),
));
},
@ -127,8 +125,6 @@ class TwoGangDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
countdownCode: 'countdown_2',
deviceType: '2G',
),
));
},

View File

@ -18,10 +18,9 @@ class ScheduleDialogHelper {
ScheduleEntry? schedule,
bool isEdit = false,
String? code,
required String deviceType,
}) {
bool temp;
if (deviceType == 'CUR_2') {
if (schedule?.category == 'CUR_2') {
temp = schedule!.function.value == 'open' ? true : false;
} else {
temp = schedule!.function.value;
@ -104,7 +103,8 @@ class ScheduleDialogHelper {
setState(() => selectedDays[i] = v);
}),
const SizedBox(height: 16),
_buildFunctionSwitch(deviceType, ctx, functionOn!, (v) {
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
(v) {
setState(() => functionOn = v);
}),
],
@ -120,29 +120,32 @@ class ScheduleDialogHelper {
),
),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
)),
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (schedule?.category == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
print(temp);
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
// functionOn,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule?.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
),
),
],
);
},

View File

@ -84,8 +84,6 @@ class WaterHeaterDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: device.uuid ?? '',
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: 'WH',
),
));
},

View File

@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
color: const Color(0xFF0026A2),
),
HomeItemModel(
title: 'Device Management',
title: 'Devices Management',
icon: Assets.devicesIcon,
active: true,
onPress: (context) {

View File

@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail(
String? res = await UserPermissionApi().checkEmail(
emailController.text,
);
checkEmailValid = res!;

View File

@ -34,8 +34,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
return Dialog(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(20))),
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
width: 900,
child: Column(
children: [
@ -64,8 +63,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
children: [
_buildStep1Indicator(1, "Basics", _blocRole),
_buildStep2Indicator(2, "Spaces", _blocRole),
_buildStep3Indicator(
3, "Role & Permissions", _blocRole),
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
],
),
),
@ -107,32 +105,18 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
),
InkWell(
onTap: () {
final isBasicsStep = currentStep == 1;
if (isBasicsStep) {
// Validate the form first
final isValid = _blocRole.formKey.currentState
?.validate() ??
false;
if (!isValid)
return; // Stop if form is not valid
}
_blocRole.add(const CheckEmailEvent());
setState(() {
if (currentStep < 3) {
currentStep++;
if (currentStep == 2) {
_blocRole.add(const CheckStepStatus(
isEditUser: false));
_blocRole.add(const CheckStepStatus(isEditUser: false));
} else if (currentStep == 3) {
_blocRole
.add(const CheckSpacesStepStatus());
_blocRole.add(const CheckSpacesStepStatus());
}
} else {
_blocRole
.add(SendInviteUsers(context: context));
_blocRole.add(SendInviteUsers(context: context));
}
});
},
@ -140,11 +124,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
currentStep < 3 ? "Next" : "Save",
style: TextStyle(
color: (_blocRole.isCompleteSpaces == false ||
_blocRole.isCompleteBasics ==
false ||
_blocRole
.isCompleteRolePermissions ==
false) &&
_blocRole.isCompleteBasics == false ||
_blocRole.isCompleteRolePermissions == false) &&
currentStep == 3
? ColorsManager.grayColor
: ColorsManager.secondaryColor),
@ -162,7 +143,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
Widget _getFormContent() {
switch (currentStep) {
case 1:
return BasicsView(
return const BasicsView(
userId: '',
);
case 2:
@ -215,12 +196,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
),
),
],
@ -283,12 +260,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
),
),
],
@ -345,12 +318,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
),
),
],

View File

@ -1,12 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl_phone_field/countries.dart';
import 'package:intl_phone_field/country_picker_dialog.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -14,9 +11,7 @@ import 'package:syncrow_web/utils/style.dart';
class BasicsView extends StatelessWidget {
final String? userId;
Timer? _debounce;
BasicsView({super.key, this.userId = ''});
const BasicsView({super.key, this.userId = ''});
@override
Widget build(BuildContext context) {
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
@ -26,7 +21,6 @@ class BasicsView extends StatelessWidget {
}
return Form(
key: _blocRole.formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ListView(
shrinkWrap: true,
children: [
@ -214,14 +208,6 @@ class BasicsView extends StatelessWidget {
fontSize: 12,
color: ColorsManager.textGray),
),
onChanged: (value) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 800), () {
_blocRole.add(const CheckEmailEvent());
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter Email Address';

View File

@ -32,114 +32,113 @@ class SpaceDropdown extends StatelessWidget {
color: ColorsManager.blackColor,
),
),
DropdownButton2<String>(
underline: const SizedBox(),
buttonStyleData: ButtonStyleData(
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(12)),
),
value: selectedValue,
items: spaces.map((space) {
return DropdownMenuItem<String>(
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: selectedValue == space.uuid
? ColorsManager.dialogBlueTitle
: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: selectedValue == space.uuid
? ColorsManager.dialogBlueTitle
: ColorsManager.blackColor,
),
),
],
),
);
}).toList(),
onChanged: onChanged,
style: TextStyle(
color: Colors.black,
fontSize: 13,
),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
),
customButton: Container(
SizedBox(
child: Container(
height: 40,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedValue != null
? spaces
.firstWhere((e) => e.uuid == selectedValue)
.name
: hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 13,
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
child: DropdownButton2<String>(
underline: const SizedBox(),
value: selectedValue,
items: spaces.map((space) {
return DropdownMenuItem<String>(
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
),
),
],
),
);
}).toList(),
onChanged: onChanged,
style: TextStyle(color: Colors.black),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
),
customButton: Container(
height: 45,
decoration: BoxDecoration(
border:
Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedValue != null
? spaces
.firstWhere((e) => e.uuid == selectedValue)
.name
: hintMessage,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
),
),
),
),
],

View File

@ -121,8 +121,7 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
child: SizedBox(
width: 16,
height: 16,
child:
CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
@ -160,9 +159,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
height: iconSize,
width: iconSize,
fit: BoxFit.contain,
errorBuilder:
(context, error, stackTrace) =>
Image.asset(
errorBuilder: (context, error, stackTrace) =>
Image.asset(
Assets.logo,
height: iconSize,
width: iconSize,
@ -205,8 +203,7 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
maxLines: 1,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize:
widget.isSmallScreenSize(context) ? 10 : 12,
fontSize: widget.isSmallScreenSize(context) ? 10 : 12,
),
),
if (widget.spaceName != '')
@ -225,9 +222,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
maxLines: 1,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: widget.isSmallScreenSize(context)
? 10
: 12,
fontSize:
widget.isSmallScreenSize(context) ? 10 : 12,
),
),
],

View File

@ -1,39 +1,24 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.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';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart';
abstract final class SpaceManagementCommunityDialogHelper {
static void showCreateDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => const CreateCommunityDialog(),
);
static void showEditDialog(
BuildContext context,
CommunityModel community,
) {
static void showCreateDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (_) => EditCommunityDialog(
community: community,
parentContext: context,
builder: (_) => CreateCommunityDialog(
title: const SelectableText('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}
static void showLoadingDialog(BuildContext context) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
static void showSuccessSnackBar(BuildContext context, String message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}

View File

@ -7,11 +7,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
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/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_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';
@ -31,18 +26,6 @@ class SpaceManagementPage extends StatelessWidget {
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

@ -1,109 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.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_structure_header_action_buttons.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';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildCommunityInfo(context, theme, screenWidth),
),
const SizedBox(width: 16),
],
),
],
),
);
}
Widget _buildCommunityInfo(
BuildContext context, ThemeData theme, double screenWidth) {
final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
final selectedSpace =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Community Structure',
style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
if (selectedCommunity != null)
Row(
children: [
Expanded(
child: Row(
children: [
Flexible(
child: SelectableText(
selectedCommunity.name,
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
maxLines: 1,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () {
SpaceManagementCommunityDialogHelper.showEditDialog(
context,
selectedCommunity,
);
},
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
],
),
),
const SizedBox(width: 8),
CommunityStructureHeaderActionButtons(
onDelete: (space) {},
onDuplicate: (space) {},
onEdit: (space) {
SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
);
},
selectedSpace: selectedSpace,
),
],
),
],
);
}
}

View File

@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeaderActionButtons extends StatelessWidget {
const CommunityStructureHeaderActionButtons({
super.key,
required this.onDelete,
required this.selectedSpace,
required this.onDuplicate,
required this.onEdit,
});
final void Function(SpaceModel space) onDelete;
final void Function(SpaceModel space) onDuplicate;
final void Function(SpaceModel space) onEdit;
final SpaceModel? selectedSpace;
@override
Widget build(BuildContext context) {
return Wrap(
alignment: WrapAlignment.end,
spacing: 10,
children: [
if (selectedSpace != null) ...[
CommunityStructureHeaderButton(
label: 'Edit',
svgAsset: Assets.editSpace,
onPressed: () => onEdit(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Duplicate',
svgAsset: Assets.duplicate,
onPressed: () => onDuplicate(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Delete',
svgAsset: Assets.spaceDelete,
onPressed: () => onDelete(selectedSpace!),
),
],
],
);
}
}

View File

@ -1,61 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureHeaderButton extends StatelessWidget {
const CommunityStructureHeaderButton({
super.key,
required this.label,
required this.onPressed,
this.svgAsset,
});
final String label;
final VoidCallback onPressed;
final String? svgAsset;
@override
Widget build(BuildContext context) {
const double buttonHeight = 40;
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 130,
minHeight: buttonHeight,
),
child: DefaultButton(
onPressed: onPressed,
borderWidth: 2,
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderRadius: 12.0,
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: ColorsManager.lightGrayColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (svgAsset != null)
SvgPicture.asset(
svgAsset!,
width: 20,
height: 20,
),
const SizedBox(width: 10),
Flexible(
child: Text(
label,
style: context.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,6 @@
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/community_structure_header.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';
@ -19,17 +18,9 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}

View File

@ -39,26 +39,6 @@ class CommunityModel extends Equatable {
.toList();
}
CommunityModel copyWith({
String? uuid,
String? name,
DateTime? createdAt,
DateTime? updatedAt,
String? description,
String? externalId,
List<SpaceModel>? spaces,
}) {
return CommunityModel(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
description: description ?? this.description,
externalId: externalId ?? this.externalId,
spaces: spaces ?? this.spaces,
);
}
@override
List<Object?> get props => [uuid, name, spaces];
}

View File

@ -19,16 +19,6 @@ class SpaceModel extends Equatable {
required this.parent,
});
factory SpaceModel.empty() => const SpaceModel(
uuid: '',
createdAt: null,
updatedAt: null,
spaceName: '',
icon: '',
children: [],
parent: null,
);
factory SpaceModel.fromJson(Map<String, dynamic> json) {
return SpaceModel(
uuid: json['uuid'] as String? ?? '',

View File

@ -16,7 +16,6 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<InsertCommunity>(_onInsertCommunity);
on<CommunitiesUpdateCommunity>(_onCommunitiesUpdateCommunity);
}
final CommunitiesService _communitiesService;
@ -115,18 +114,4 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
) {
emit(state.copyWith(communities: [event.community, ...state.communities]));
}
void _onCommunitiesUpdateCommunity(
CommunitiesUpdateCommunity event,
Emitter<CommunitiesState> emit,
) {
final updatedCommunities = state.communities
.map((e) => e.uuid == event.community.uuid ? event.community : e)
.toList();
emit(
state.copyWith(
communities: updatedCommunities,
),
);
}
}

View File

@ -31,12 +31,3 @@ final class InsertCommunity extends CommunitiesEvent {
@override
List<Object?> get props => [community];
}
final class CommunitiesUpdateCommunity extends CommunitiesEvent {
const CommunitiesUpdateCommunity(this.community);
final CommunityModel community;
@override
List<Object?> get props => [community];
}

View File

@ -1,58 +1,57 @@
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/main_module/shared/widgets/community_dialog.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/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/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_dialog_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class CreateCommunityDialog extends StatelessWidget {
const CreateCommunityDialog({super.key});
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: BlocConsumer<CreateCommunityBloc, CreateCommunityState>(
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
listener: (context, state) {
switch (state) {
case CreateCommunityLoading() || CreateCommunityInitial():
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
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();
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
context,
'${community.name} community created successfully',
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Community created successfully')),
);
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
onCreateCommunity.call(community);
break;
case CreateCommunityFailure():
Navigator.of(context).pop();
break;
default:
break;
}
},
builder: (BuildContext context, CreateCommunityState state) {
return CommunityDialog(
title: const Text('Create Community'),
initialName: null,
onSubmit: (name) => context.read<CreateCommunityBloc>().add(
CreateCommunity(CreateCommunityParam(name: name)),
),
errorMessage: state is CreateCommunityFailure ? state.message : null,
);
},
child: CreateCommunityDialogWidget(
title: title,
initialName: initialName,
),
),
);
}

View File

@ -1,29 +1,28 @@
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';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityDialog extends StatefulWidget {
class CreateCommunityDialogWidget extends StatefulWidget {
final String? initialName;
final Widget title;
final void Function(String name) onSubmit;
final String? errorMessage;
const CommunityDialog({
required this.title,
required this.onSubmit,
this.initialName,
this.errorMessage,
const CreateCommunityDialogWidget({
super.key,
required this.title,
this.initialName,
});
@override
State<CommunityDialog> createState() => _CommunityDialogState();
State<CreateCommunityDialogWidget> createState() =>
_CreateCommunityDialogWidgetState();
}
class _CommunityDialogState extends State<CommunityDialog> {
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
late final TextEditingController _nameController;
@override
@ -64,20 +63,35 @@ class _CommunityDialogState extends State<CommunityDialog> {
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: 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),
_buildErrorMessage(),
const SizedBox(height: 24),
_buildActionButtons(context),
],
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),
],
);
},
),
),
),
@ -118,22 +132,13 @@ class _CommunityDialogState extends State<CommunityDialog> {
void _onSubmit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
widget.onSubmit.call(_nameController.text.trim());
context.read<CreateCommunityBloc>().add(
CreateCommunity(
CreateCommunityParam(
name: _nameController.text.trim(),
),
),
);
}
}
Widget _buildErrorMessage() {
return Visibility(
visible: widget.errorMessage != null,
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(vertical: 18),
child: SelectableText(
'* ${widget.errorMessage}',
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
}

View File

@ -1,9 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_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 RemoteProductsService implements ProductsService {
const RemoteProductsService(this._httpService);
@ -13,14 +13,17 @@ class RemoteProductsService implements ProductsService {
static const _defaultErrorMessage = 'Failed to load devices';
@override
Future<List<Product>> getProducts() async {
Future<List<Product>> getProducts(LoadProductsParam param) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.listProducts,
path: 'devices',
queryParameters: {
'spaceUuid': param.spaceUuid,
if (param.type != null) 'type': param.type,
if (param.status != null) 'status': param.status,
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>;
final products = json['data'] as List<dynamic>;
return products
return (data as List)
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
},

View File

@ -1,24 +1,18 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class Product extends Equatable {
final String uuid;
final String name;
const Product({
required this.uuid,
required this.name,
required this.productType,
});
final String uuid;
final String name;
final String productType;
String get icon => _mapIconToProduct(productType);
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
uuid: json['uuid'] as String? ?? '',
name: json['name'] as String? ?? '',
productType: json['prodType'] as String? ?? '',
uuid: json['uuid'] as String,
name: json['name'] as String,
);
}
@ -26,37 +20,9 @@ class Product extends Equatable {
return {
'uuid': uuid,
'name': name,
'productType': productType,
};
}
static String _mapIconToProduct(String prodType) {
const iconMapping = {
'1G': Assets.Gang1SwitchIcon,
'1GT': Assets.oneTouchSwitch,
'2G': Assets.Gang2SwitchIcon,
'2GT': Assets.twoTouchSwitch,
'3G': Assets.Gang3SwitchIcon,
'3GT': Assets.threeTouchSwitch,
'CUR': Assets.curtain,
'CUR_2': Assets.curtain,
'GD': Assets.garageDoor,
'GW': Assets.SmartGatewayIcon,
'DL': Assets.DoorLockIcon,
'WL': Assets.waterLeakSensor,
'WH': Assets.waterHeater,
'WM': Assets.waterLeakSensor,
'SOS': Assets.sos,
'AC': Assets.ac,
'CPS': Assets.presenceSensor,
'PC': Assets.powerClamp,
'WPS': Assets.presenceSensor,
'DS': Assets.doorSensor
};
return iconMapping[prodType] ?? Assets.presenceSensor;
}
@override
List<Object?> get props => [uuid, name, productType];
List<Object?> get props => [uuid, name];
}

View File

@ -0,0 +1,11 @@
class LoadProductsParam {
final String spaceUuid;
final String? type;
final String? status;
const LoadProductsParam({
required this.spaceUuid,
this.type,
this.status,
});
}

View File

@ -1,5 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
abstract class ProductsService {
Future<List<Product>> getProducts();
Future<List<Product>> getProducts(LoadProductsParam param);
}

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -8,20 +9,20 @@ part 'products_event.dart';
part 'products_state.dart';
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
ProductsBloc(this._productsService) : super(ProductsInitial()) {
final ProductsService _deviceService;
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
on<LoadProducts>(_onLoadProducts);
}
final ProductsService _productsService;
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductsState> emit,
) async {
emit(ProductsLoading());
try {
final products = await _productsService.getProducts();
emit(ProductsLoaded(products));
final devices = await _deviceService.getProducts(event.param);
emit(ProductsLoaded(devices));
} on APIException catch (e) {
emit(ProductsFailure(e.message));
} catch (e) {

View File

@ -8,5 +8,10 @@ sealed class ProductsEvent extends Equatable {
}
final class LoadProducts extends ProductsEvent {
const LoadProducts();
const LoadProducts(this.param);
final LoadProductsParam param;
@override
List<Object> get props => [param];
}

View File

@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
}
final class ProductsFailure extends ProductsState {
final String errorMessage;
final String message;
const ProductsFailure(this.errorMessage);
const ProductsFailure(this.message);
@override
List<Object> get props => [errorMessage];
List<Object> get props => [message];
}

View File

@ -1,7 +1,6 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -16,15 +15,12 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
static const _defaultErrorMessage = 'Failed to load space details';
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param) async {
try {
final response = await _httpService.get(
path: await _makeEndpoint(param),
path: 'endpoint',
expectedResponseModel: (data) {
final response = data as Map<String, dynamic>;
return SpaceDetailsModel.fromJson(
response['data'] as Map<String, dynamic>,
);
return SpaceDetailsModel.fromJson(data as Map<String, dynamic>);
},
);
return response;
@ -41,13 +37,4 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeEndpoint(LoadSpaceDetailsParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
}
}

View File

@ -1,27 +0,0 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService {
final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee);
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{};
for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) {
uniqueSubspaces[normalizedName] = subspace;
}
}
return response.copyWith(
subspaces: uniqueSubspaces.values.toList(),
);
}
}

View File

@ -1,8 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -19,13 +17,6 @@ class SpaceDetailsModel extends Equatable {
required this.subspaces,
});
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.location,
productAllocations: [],
subspaces: [],
);
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel(
uuid: json['uuid'] as String,
@ -50,40 +41,23 @@ class SpaceDetailsModel extends Equatable {
};
}
SpaceDetailsModel copyWith({
String? uuid,
String? spaceName,
String? icon,
List<ProductAllocation>? productAllocations,
List<Subspace>? subspaces,
}) {
return SpaceDetailsModel(
uuid: uuid ?? this.uuid,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
productAllocations: productAllocations ?? this.productAllocations,
subspaces: subspaces ?? this.subspaces,
);
}
@override
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
}
class ProductAllocation extends Equatable {
final String uuid;
final Product product;
final Tag tag;
final String? location;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
this.location,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
@ -91,26 +65,13 @@ class ProductAllocation extends Equatable {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [uuid, product, tag];
List<Object?> get props => [product, tag];
}
class Subspace extends Equatable {
@ -127,7 +88,7 @@ class Subspace extends Equatable {
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String,
name: json['subspaceName'] as String,
name: json['name'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
@ -142,18 +103,6 @@ class Subspace extends Equatable {
};
}
Subspace copyWith({
String? uuid,
String? name,
List<ProductAllocation>? productAllocations,
}) {
return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
}
@override
List<Object?> get props => [uuid, name, productAllocations];
}

View File

@ -1,9 +0,0 @@
class LoadSpaceDetailsParam {
const LoadSpaceDetailsParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}

View File

@ -0,0 +1,3 @@
class LoadSpacesParam {
const LoadSpacesParam();
}

View File

@ -1,6 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
abstract class SpaceDetailsService {
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param);
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param);
}

View File

@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -9,13 +9,12 @@ part 'space_details_event.dart';
part 'space_details_state.dart';
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
final SpaceDetailsService _spaceDetailsService;
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
on<LoadSpaceDetails>(_onLoadSpaceDetails);
on<ClearSpaceDetails>(_onClearSpaceDetails);
}
final SpaceDetailsService _spaceDetailsService;
Future<void> _onLoadSpaceDetails(
LoadSpaceDetails event,
Emitter<SpaceDetailsState> emit,
@ -32,11 +31,4 @@ class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
emit(SpaceDetailsFailure(e.toString()));
}
}
void _onClearSpaceDetails(
ClearSpaceDetails event,
Emitter<SpaceDetailsState> emit,
) {
emit(SpaceDetailsInitial());
}
}

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