Compare commits

..

17 Commits
dev ... master

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
209 changed files with 2770 additions and 7716 deletions

View File

@ -1,3 +1,2 @@
ENV_NAME=development
BASE_URL=https://syncrow-dev.azurewebsites.net
RTDB_URL=https://syncrow-dev-79446.asia-southeast1.firebasedatabase.app/
BASE_URL=https://syncrow-dev.azurewebsites.net

View File

@ -1,3 +1,2 @@
ENV_NAME=production
BASE_URL=https://syncrow-staging.azurewebsites.net
RTDB_URL=https://syncrow-prod-79446.asia-southeast1.firebasedatabase.app/
BASE_URL=https://api.syncrow.me

View File

@ -1,3 +1,2 @@
ENV_NAME=staging
BASE_URL=https://syncrow-staging.azurewebsites.net
RTDB_URL=https://syncrow-staging-79446.asia-southeast1.firebasedatabase.app/
BASE_URL=https://syncrow-staging.azurewebsites.net

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

112
.vscode/launch.json vendored
View File

@ -1,49 +1,67 @@
{
"configurations": [
{
"name": "DEVELOPMENT",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_dev.dart",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
},
{
"name": "STAGING",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_staging.dart",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
},
{
"name": "PRODUCTION",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main.dart",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
}
]
"configurations": [
{
"name": "DEVELOPMENT",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_dev.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
},{
"name": "STAGING",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_staging.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
},{
"name": "PRODUCTION",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
},
]
}

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
}
}

View File

@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"syncrow-prod-79446","configurations":{"web":"1:255001682464:web:a03e2d6214c13101561245"}}}}}}
{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"test2-8a3d2","configurations":{"android":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","ios":"1:427332280600:ios:14346b200780dc760c7e6d","macos":"1:427332280600:ios:14346b200780dc760c7e6d","web":"1:427332280600:web:ad50516a87a35a1a0c7e6d","windows":"1:427332280600:web:f7a25537ccd5a7bd0c7e6d"}}}}}}

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,16 +0,0 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
final class DefaultFirebaseOptions extends FirebaseOptions {
const DefaultFirebaseOptions({
required String databaseUrl,
}) : super(
apiKey: 'AIzaSyDgq5ywsnFVbbQO-Xz1Z4sR5bBcuiDaS9g',
appId: '1:255001682464:web:a03e2d6214c13101561245',
messagingSenderId: '255001682464',
projectId: 'syncrow-prod-79446',
authDomain: 'syncrow-prod-79446.firebaseapp.com',
storageBucket: 'syncrow-prod-79446.firebasestorage.app',
databaseURL: databaseUrl,
measurementId: 'G-1850Q89RMK',
);
}

View File

@ -0,0 +1,93 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptionsDev {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyCVEvKsJYzhWDFM-9Od68FE0nPpP933st0',
appId: '1:427332280600:web:ad50516a87a35a1a0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
authDomain: 'test2-8a3d2.firebaseapp.com',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
measurementId: 'G-Z1RTTTV5H9',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0',
appId: '1:427332280600:android:2bc36fbe82994a3e0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
iosBundleId: 'com.example.syncrowWeb',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
iosBundleId: 'com.example.syncrowWeb',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyDizKjPC5rdkEjDxwXjM-RU5unB0Ziq3iw',
appId: '1:427332280600:web:f7a25537ccd5a7bd0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
authDomain: 'test2-8a3d2.firebaseapp.com',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
measurementId: 'G-4LFVXEXWKY',
);
}

View File

@ -0,0 +1,77 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptionsStaging {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDP9GpYfLE8gHTj3kZ1hW8fx_FkJqOqSQk',
appId: '1:786692570726:android:0ef7079c2b978d4417b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyAWlRiuJ75FMlf2_UDdri1voWKvkaSHtRg',
appId: '1:786692570726:ios:455a6fcff77e130f17b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
iosBundleId: 'com.example.syncrow.app',
);
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyDyGaQ3sZhb4meaY6sGke-YglhdhJ2is8Q',
appId: '1:786692570726:web:93c931e6701797b317b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
authDomain: 'syncrow-staging.firebaseapp.com',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
measurementId: 'G-CZ3J3G6LMQ',
);
}

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -27,9 +27,7 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
options: DefaultFirebaseOptionsStaging.currentPlatform,
);
initialSetup();
} catch (_) {}
@ -61,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/firebase_options_dev.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -27,9 +27,7 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
options: DefaultFirebaseOptionsDev.currentPlatform,
);
initialSetup();
} catch (_) {}
@ -61,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -24,9 +24,7 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
options: DefaultFirebaseOptionsStaging.currentPlatform,
);
initialSetup();
} catch (_) {}
@ -58,7 +56,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
import 'package:syncrow_web/services/access_mang_api.dart';

View File

@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
abstract class AccessState extends Equatable {
const AccessState();

View File

@ -1,52 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_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 RemoteBookableSpacesService implements BookableSystemService {
const RemoteBookableSpacesService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load bookable spaces';
@override
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getBookableSpaces,
queryParameters: {
'page': param.page,
'size': param.size,
'active': true,
'configured': true,
if (param.search != null &&
param.search.isNotEmpty &&
param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) {
return PaginatedBookableSpaces.fromJson(
json as Map<String, dynamic>,
);
},
);
return response;
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
final errorMessage = responseData['error']?['message'] as String? ??
responseData['message'] as String? ??
_defaultErrorMessage;
throw APIException(errorMessage);
}
throw APIException(_defaultErrorMessage);
} catch (e) {
throw APIException('$_defaultErrorMessage: ${e.toString()}');
}
}
}

View File

@ -1,170 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_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 RemoteCalendarService implements CalendarSystemService {
const RemoteCalendarService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load Calendar';
@override
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
final errorMessage = responseData['error']?['message'] as String? ??
responseData['message'] as String? ??
_defaultErrorMessage;
throw APIException(errorMessage);
}
throw APIException(_defaultErrorMessage);
} catch (e) {
throw APIException('$_defaultErrorMessage: ${e.toString()}');
}
}
}
class FakeRemoteCalendarService implements CalendarSystemService {
const FakeRemoteCalendarService(this._httpService, {this.useDummy = false});
final HTTPService _httpService;
final bool useDummy;
static const _defaultErrorMessage = 'Failed to load Calendar';
@override
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
}) async {
if (useDummy) {
final dummyJson = {
'statusCode': 200,
'message': 'Successfully fetched all bookings',
'data': [
{
'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '09:00:00',
'endTime': '12:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '12:00:00',
'endTime': '13:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-13T10:22:00.626Z',
'startTime': '15:30:00',
'endTime': '19:00:00',
'cost': 20,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
}
],
'success': true
};
final response = CalendarEventsResponse.fromJson(dummyJson);
// Filter events by spaceId
final filteredData = response.data.where((event) {
return event.space.uuid == spaceId;
}).toList();
print('Filtering events for spaceId: $spaceId');
print('Found ${filteredData.length} matching events');
return filteredData.isNotEmpty
? CalendarEventsResponse(
statusCode: response.statusCode,
message: response.message,
data: filteredData,
success: response.success,
)
: CalendarEventsResponse(
statusCode: 404,
message: 'No events found for spaceId: $spaceId',
data: [],
success: false,
);
}
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
final errorMessage = responseData['error']?['message'] as String? ??
responseData['message'] as String? ??
_defaultErrorMessage;
throw APIException(errorMessage);
}
throw APIException(_defaultErrorMessage);
} catch (e) {
throw APIException('$_defaultErrorMessage: ${e.toString()}');
}
}
}

View File

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

View File

@ -1,52 +0,0 @@
class BookableSpaceModel {
final String uuid;
final String spaceName;
final String virtualLocation;
final BookableConfig bookableConfig;
BookableSpaceModel({
required this.uuid,
required this.spaceName,
required this.virtualLocation,
required this.bookableConfig,
});
factory BookableSpaceModel.fromJson(Map<String, dynamic> json) {
return BookableSpaceModel(
uuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
virtualLocation: json['virtualLocation'] as String,
bookableConfig: BookableConfig.fromJson(
json['bookableConfig'] as Map<String, dynamic>),
);
}
}
class BookableConfig {
final String uuid;
final List<String> daysAvailable;
final String startTime;
final String endTime;
final bool active;
final int points;
BookableConfig({
required this.uuid,
required this.daysAvailable,
required this.startTime,
required this.endTime,
required this.active,
required this.points,
});
factory BookableConfig.fromJson(Map<String, dynamic> json) {
return BookableConfig(
uuid: json['uuid'] as String,
daysAvailable: (json['daysAvailable'] as List).cast<String>(),
startTime: json['startTime'] as String,
endTime: json['endTime'] as String,
active: json['active'] as bool,
points: json['points'] as int,
);
}
}

View File

@ -1,134 +0,0 @@
class CalendarEventBooking {
final String uuid;
final DateTime date;
final String startTime;
final String endTime;
final int cost;
final BookingUser user;
final BookingSpace space;
CalendarEventBooking({
required this.uuid,
required this.date,
required this.startTime,
required this.endTime,
required this.cost,
required this.user,
required this.space,
});
factory CalendarEventBooking.fromJson(Map<String, dynamic> json) {
return CalendarEventBooking(
uuid: json['uuid'] as String? ?? '',
date: json['date'] != null
? DateTime.parse(json['date'] as String)
: DateTime.now(),
startTime: json['startTime'] as String? ?? '',
endTime: json['endTime'] as String? ?? '',
cost: _parseInt(json['cost']),
user: json['user'] != null
? BookingUser.fromJson(json['user'] as Map<String, dynamic>)
: BookingUser.empty(),
space: json['space'] != null
? BookingSpace.fromJson(json['space'] as Map<String, dynamic>)
: BookingSpace.empty(),
);
}
static int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
}
class BookingUser {
final String uuid;
final String firstName;
final String lastName;
final String email;
final String? companyName;
BookingUser({
required this.uuid,
required this.firstName,
required this.lastName,
required this.email,
this.companyName,
});
factory BookingUser.fromJson(Map<String, dynamic> json) {
return BookingUser(
uuid: json['uuid'] as String? ?? '',
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
email: json['email'] as String? ?? '',
companyName: json['companyName'] as String?,
);
}
factory BookingUser.empty() {
return BookingUser(
uuid: '',
firstName: '',
lastName: '',
email: '',
companyName: null,
);
}
}
class BookingSpace {
final String uuid;
final String spaceName;
BookingSpace({
required this.uuid,
required this.spaceName,
});
factory BookingSpace.fromJson(Map<String, dynamic> json) {
return BookingSpace(
uuid: json['uuid'] as String? ?? '',
spaceName: json['spaceName'] as String? ?? '',
);
}
factory BookingSpace.empty() {
return BookingSpace(
uuid: '',
spaceName: '',
);
}
}
class CalendarEventsResponse {
final int statusCode;
final String message;
final List<CalendarEventBooking> data;
final bool success;
CalendarEventsResponse({
required this.statusCode,
required this.message,
required this.data,
required this.success,
});
factory CalendarEventsResponse.fromJson(Map<String, dynamic> json) {
return CalendarEventsResponse(
statusCode: _parseInt(json['statusCode']),
message: json['message'] as String? ?? '',
data: (json['data'] as List? ?? [])
.map((e) => CalendarEventBooking.fromJson(e as Map<String, dynamic>))
.toList(),
success: json['success'] as bool? ?? false,
);
}
}
int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}

View File

@ -1,40 +0,0 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
class PaginatedBookableSpaces {
final List<BookableSpaceModel> data;
final String message;
final int page;
final int size;
final int totalItem;
final int totalPage;
final bool hasNext;
final bool hasPrevious;
PaginatedBookableSpaces({
required this.data,
required this.message,
required this.page,
required this.size,
required this.totalItem,
required this.totalPage,
required this.hasNext,
required this.hasPrevious,
});
factory PaginatedBookableSpaces.fromJson(Map<String, dynamic> json) {
return PaginatedBookableSpaces(
data: (json['data'] as List)
.map((item) => BookableSpaceModel.fromJson(item))
.toList(),
message: json['message'] as String,
page: json['page'] as int,
size: json['size'] as int,
totalItem: json['totalItem'] as int,
totalPage: json['totalPage'] as int,
hasNext: json['hasNext'] as bool,
hasPrevious: json['hasPrevious'] as bool,
);
}
}

View File

@ -1,8 +0,0 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
abstract class BookableSystemService {
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
});
}

View File

@ -1,7 +0,0 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
abstract class CalendarSystemService {
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
});
}

View File

@ -1,45 +0,0 @@
import 'dart:async';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
class DebouncedBookableSpacesService implements BookableSystemService {
final BookableSystemService _inner;
final Duration debounceDuration;
Timer? _debounceTimer;
Completer<PaginatedBookableSpaces>? _lastCompleter;
DebouncedBookableSpacesService(
this._inner, {
this.debounceDuration = const Duration(milliseconds: 500),
});
@override
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
}) {
_debounceTimer?.cancel();
if (_lastCompleter != null && !_lastCompleter!.isCompleted) {
_lastCompleter!.completeError(StateError("Cancelled by new search"));
}
final completer = Completer<PaginatedBookableSpaces>();
_lastCompleter = completer;
_debounceTimer = Timer(debounceDuration, () async {
try {
final result = await _inner.getBookableSpaces(param: param);
if (!completer.isCompleted) {
completer.complete(result);
}
} catch (e, st) {
if (!completer.isCompleted) {
completer.completeError(e, st);
}
}
});
return completer.future;
}
}

View File

@ -1,115 +0,0 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
part 'events_event.dart';
part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
final CalendarSystemService calendarService;
CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek);
}
Future<void> _onLoadEvents(
LoadEvents event,
Emitter<CalendarEventState> emit,
) async {
emit(EventsLoading());
try {
final response = await calendarService.getCalendarEvents(
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events);
emit(EventsLoaded(events: events));
} catch (e) {
emit(EventsError('Failed to load events'));
}
}
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event);
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
emit(EventsLoaded(
events: [...eventController.events],
));
}
}
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose();
}
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded(
events: loaded.events,
));
}
}
CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
final date = booking.date;
final localDate = date.toLocal();
final startParts = booking.startTime.split(':').map(int.parse).toList();
final endParts = booking.endTime.split(':').map(int.parse).toList();
final startTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
startParts[0],
startParts[1],
);
final endTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
endParts[0],
endParts[1],
);
return CalendarEventData(
date: startTime,
startTime: startTime,
endTime: endTime,
title:
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}',
color: Colors.blue,
event: booking,
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
@override
Future<void> close() {
eventController.dispose();
return super.close();
}
}

View File

@ -1,37 +0,0 @@
part of 'events_bloc.dart';
@immutable
abstract class CalendarEventsEvent {
const CalendarEventsEvent();
}
class LoadEvents extends CalendarEventsEvent {
final String spaceId;
final DateTime weekStart;
final DateTime weekEnd;
const LoadEvents({
required this.spaceId,
required this.weekStart,
required this.weekEnd,
});
}
class AddEvent extends CalendarEventsEvent {
final CalendarEventData event;
const AddEvent(this.event);
}
class StartTimer extends CalendarEventsEvent {}
class DisposeResources extends CalendarEventsEvent {}
class GoToWeek extends CalendarEventsEvent {
final DateTime weekDate;
GoToWeek(this.weekDate);
}
class CheckWeekHasEvents extends CalendarEventsEvent {
final DateTime weekStart;
const CheckWeekHasEvents(this.weekStart);
}

View File

@ -1,21 +0,0 @@
part of 'events_bloc.dart';
@immutable
abstract class CalendarEventState {}
class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events;
EventsLoaded({
required this.events,
});
}
class EventsError extends CalendarEventState {
final String message;
EventsError(this.message);
}

View File

@ -1,40 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
import 'date_selection_state.dart';
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
DateSelectionBloc() : super(DateSelectionState.initial()) {
on<SelectDate>((event, emit) {
final newWeekStart = _getStartOfWeek(event.selectedDate);
emit(state.copyWith(
selectedDate: event.selectedDate,
weekStart: newWeekStart,
));
});
on<NextWeek>((event, emit) {
final newWeekStart = state.weekStart.add(const Duration(days: 7));
emit(state.copyWith(
weekStart: newWeekStart,
));
});
on<PreviousWeek>((event, emit) {
final newWeekStart = state.weekStart.subtract(const Duration(days: 7));
emit(state.copyWith(
weekStart: newWeekStart,
));
});
on<SelectDateFromSidebarCalendar>((event, emit) {
emit(state.copyWith(
selectedDateFromSideBarCalender: event.selectedDate,
));
});
}
static DateTime _getStartOfWeek(DateTime date) {
return date.subtract(Duration(days: date.weekday - 1));
}
}

View File

@ -1,18 +0,0 @@
abstract class DateSelectionEvent {
const DateSelectionEvent();
}
class SelectDate extends DateSelectionEvent {
final DateTime selectedDate;
const SelectDate(this.selectedDate);
}
class NextWeek extends DateSelectionEvent {}
class PreviousWeek extends DateSelectionEvent {}
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
final DateTime selectedDate;
SelectDateFromSidebarCalendar(this.selectedDate);
}

View File

@ -1,34 +0,0 @@
class DateSelectionState {
final DateTime selectedDate;
final DateTime weekStart;
final DateTime? selectedDateFromSideBarCalender;
DateSelectionState({
required this.selectedDate,
required this.weekStart,
this.selectedDateFromSideBarCalender,
});
factory DateSelectionState.initial() {
final now = DateTime.now();
final weekStart = now.subtract(Duration(days: now.weekday - 1));
return DateSelectionState(
selectedDate: now,
weekStart: weekStart,
selectedDateFromSideBarCalender: null,
);
}
DateSelectionState copyWith({
DateTime? selectedDate,
DateTime? weekStart,
DateTime? selectedDateFromSideBarCalender,
}) {
return DateSelectionState(
selectedDate: selectedDate ?? this.selectedDate,
weekStart: weekStart ?? this.weekStart,
selectedDateFromSideBarCalender: selectedDateFromSideBarCalender ?? this.selectedDateFromSideBarCalender,
);
}
}

View File

@ -1,14 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
part 'selected_bookable_space_event.dart';
part 'selected_bookable_space_state.dart';
class SelectedBookableSpaceBloc
extends Bloc<SelectedBookableSpaceEvent, SelectedBookableSpaceState> {
SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) {
on<SelectBookableSpace>((event, emit) {
emit(SelectedBookableSpaceState(
selectedBookableSpace: event.bookableSpace));
});
}
}

View File

@ -1,11 +0,0 @@
part of 'selected_bookable_space_bloc.dart';
abstract class SelectedBookableSpaceEvent {
const SelectedBookableSpaceEvent();
}
class SelectBookableSpace extends SelectedBookableSpaceEvent {
final BookableSpaceModel bookableSpace;
const SelectBookableSpace(this.bookableSpace);
}

View File

@ -1,9 +0,0 @@
part of 'selected_bookable_space_bloc.dart';
class SelectedBookableSpaceState {
final BookableSpaceModel? selectedBookableSpace;
const SelectedBookableSpaceState(
{ this.selectedBookableSpace,}
);
}

View File

@ -1,148 +0,0 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
final BookableSystemService _bookingService;
int _currentPage = 1;
final int _pageSize = 20;
String _currentSearch = '';
SidebarBloc(this._bookingService)
: super(SidebarState(
allRooms: [],
displayedRooms: [],
isLoading: true,
hasMore: true,
)) {
on<LoadBookableSpaces>(_onLoadBookableSpaces);
on<LoadMoreSpaces>(_onLoadMoreSpaces);
on<SelectRoomEvent>(_onSelectRoom);
on<SearchRoomsEvent>(_onSearchRooms);
on<ResetSearch>(_onResetSearch);
}
Future<void> _onLoadBookableSpaces(
LoadBookableSpaces event,
Emitter<SidebarState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
_currentPage = 1;
_currentSearch = '';
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
emit(state.copyWith(
allRooms: paginatedSpaces.data,
displayedRooms: paginatedSpaces.data,
isLoading: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Failed to load rooms: ${e.toString()}',
));
}
}
Future<void> _onLoadMoreSpaces(
LoadMoreSpaces event,
Emitter<SidebarState> emit,
) async {
if (!state.hasMore || state.isLoadingMore) return;
try {
emit(state.copyWith(isLoadingMore: true));
_currentPage++;
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
emit(state.copyWith(
allRooms: updatedRooms,
displayedRooms: updatedRooms,
isLoadingMore: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
_currentPage--;
emit(state.copyWith(
isLoadingMore: false,
errorMessage: 'Failed to load more rooms: ${e.toString()}',
));
}
}
Future<void> _onSearchRooms(
SearchRoomsEvent event,
Emitter<SidebarState> emit,
) async {
try {
_currentSearch = event.query;
_currentPage = 1;
emit(state.copyWith(isLoading: true, errorMessage: null));
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
emit(state.copyWith(
allRooms: paginatedSpaces.data,
displayedRooms: paginatedSpaces.data,
isLoading: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Search failed: ${e.toString()}',
));
}
}
void _onResetSearch(
ResetSearch event,
Emitter<SidebarState> emit,
) {
_currentSearch = '';
add(LoadBookableSpaces());
}
void _onSelectRoom(
SelectRoomEvent event,
Emitter<SidebarState> emit,
) {
emit(state.copyWith(selectedRoomId: event.roomId));
}
@override
Future<void> close() {
return super.close();
}
}

View File

@ -1,25 +0,0 @@
abstract class SidebarEvent {}
class LoadBookableSpaces extends SidebarEvent {}
class SelectRoomEvent extends SidebarEvent {
final String roomId;
SelectRoomEvent(this.roomId);
}
class SearchRoomsEvent extends SidebarEvent {
final String query;
SearchRoomsEvent(this.query);
}
class LoadMoreSpaces extends SidebarEvent {}
class ResetSearch extends SidebarEvent {}
class ExecuteSearch extends SidebarEvent {
final String query;
ExecuteSearch(this.query);
}

View File

@ -1,49 +0,0 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
class SidebarState {
final List<BookableSpaceModel> allRooms;
final List<BookableSpaceModel> displayedRooms;
final bool isLoading;
final bool isLoadingMore;
final String? errorMessage;
final String? selectedRoomId;
final bool hasMore;
final int totalPages;
final int currentPage;
SidebarState({
required this.allRooms,
required this.displayedRooms,
required this.isLoading,
this.isLoadingMore = false,
this.errorMessage,
this.selectedRoomId,
this.hasMore = true,
this.totalPages = 0,
this.currentPage = 1,
});
SidebarState copyWith({
List<BookableSpaceModel>? allRooms,
List<BookableSpaceModel>? displayedRooms,
bool? isLoading,
bool? isLoadingMore,
String? errorMessage,
String? selectedRoomId,
bool? hasMore,
int? totalPages,
int? currentPage,
}) {
return SidebarState(
allRooms: allRooms ?? this.allRooms,
displayedRooms: displayedRooms ?? this.displayedRooms,
isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
errorMessage: errorMessage ?? this.errorMessage,
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
hasMore: hasMore ?? this.hasMore,
totalPages: totalPages ?? this.totalPages,
currentPage: currentPage ?? this.currentPage,
);
}
}

View File

@ -1,240 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatefulWidget {
const BookingPage({super.key});
@override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
late final EventController _eventController;
@override
void initState() {
super.initState();
_eventController = EventController();
}
@override
void dispose() {
_eventController.dispose();
super.dispose();
}
void _dispatchLoadEvents(BuildContext context) {
final selectedRoom =
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
final dateState = context.read<DateSelectionBloc>().state;
if (selectedRoom != null) {
context.read<CalendarEventsBloc>().add(
LoadEvents(
spaceId: selectedRoom.uuid,
weekStart: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
),
);
}
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
),
),
],
child: Builder(
builder: (context) =>
BlocListener<CalendarEventsBloc, CalendarEventState>(
listenWhen: (prev, curr) => curr is EventsLoaded,
listener: (context, state) {
if (state is EventsLoaded) {
_eventController.removeWhere((_) => true);
_eventController.addAll(state.events);
}
},
child: BlocListener<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: BlocListener<DateSelectionBloc, DateSelectionState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(3, 0),
blurRadius: 6,
spreadRadius: 0,
),
],
),
child: Column(
children: [
Expanded(
flex: 2,
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, state) {
return BookingSidebar(
onRoomSelected: (selectedRoom) {
context
.read<SelectedBookableSpaceBloc>()
.add(SelectBookableSpace(selectedRoom));
},
);
},
),
),
Expanded(
child: BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return CustomCalendarPage(
selectedDate: dateState.selectedDate,
onDateChanged: (day, month, year) {
final newDate = DateTime(year, month, day);
context
.read<DateSelectionBloc>()
.add(SelectDate(newDate));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
},
);
},
),
),
],
),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {},
),
const SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {},
),
],
),
BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, state) {
final weekStart = state.weekStart;
final weekEnd =
weekStart.add(const Duration(days: 6));
return WeekNavigation(
weekStart: weekStart,
weekEnd: weekEnd,
onPreviousWeek: () {
context
.read<DateSelectionBloc>()
.add(PreviousWeek());
},
onNextWeek: () {
context
.read<DateSelectionBloc>()
.add(NextWeek());
},
);
},
),
],
),
Expanded(
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {
final selectedRoom =
roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return BlocListener<CalendarEventsBloc,
CalendarEventState>(
listenWhen: (prev, curr) =>
curr is EventsLoaded,
listener: (context, state) {
if (state is EventsLoaded) {
_eventController
.removeWhere((_) => true);
_eventController.addAll(state.events);
}
},
child: WeeklyCalendarPage(
startTime: selectedRoom
?.bookableConfig.startTime,
endTime: selectedRoom
?.bookableConfig.endTime,
weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate,
eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
),
);
},
);
},
),
),
],
),
),
),
],
),
),
),
),
),
);
}
}

View File

@ -1,242 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingSidebar extends StatelessWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const BookingSidebar({
super.key,
required this.onRoomSelected,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SidebarBloc(RemoteBookableSpacesService(
HTTPService(),
))
..add(LoadBookableSpaces()),
child: _SidebarContent(onRoomSelected: onRoomSelected),
);
}
}
class _SidebarContent extends StatefulWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const _SidebarContent({
required this.onRoomSelected,
});
@override
State<_SidebarContent> createState() => __SidebarContentState();
}
class __SidebarContentState extends State<_SidebarContent> {
final TextEditingController searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
context.read<SidebarBloc>().add(LoadMoreSpaces());
}
}
void _handleSearch(String value) {
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
}
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) {
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) {
return Column(
children: [
const _SidebarHeader(title: 'Spaces'),
Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, -2),
blurRadius: 4,
spreadRadius: 0,
),
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 4,
spreadRadius: 0,
),
],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
children: [
Expanded(
child: TextField(
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.blackColor,
),
controller: searchController,
onChanged: _handleSearch,
decoration: InputDecoration(
hintText: 'Search',
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 20,
height: 20,
child: SvgPicture.asset(
Assets.searchIconUser,
color: ColorsManager.primaryTextColor,
),
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 12),
border: const OutlineInputBorder(
borderSide: BorderSide.none),
),
),
),
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
context.read<SidebarBloc>().add(ResetSearch());
},
),
],
),
),
),
),
),
),
if (state.isLoading)
const Expanded(
child: Center(child: CircularProgressIndicator()),
)
else if (state.errorMessage != null)
Expanded(
child: Center(child: Text(state.errorMessage!)),
)
else
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount:
state.displayedRooms.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.displayedRooms.length) {
return _buildLoadMoreIndicator(state);
}
final room = state.displayedRooms[index];
return RoomListItem(
room: room,
isSelected: state.selectedRoomId == room.uuid,
onTap: () {
context
.read<SidebarBloc>()
.add(SelectRoomEvent(room.uuid));
widget.onRoomSelected(room);
},
);
},
),
),
],
);
},
);
}
Widget _buildLoadMoreIndicator(SidebarState state) {
if (state.isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: CircularProgressIndicator()),
);
} else if (state.hasMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: Text('Scroll to load more')),
);
} else {
return const SizedBox.shrink();
}
}
}
class _SidebarHeader extends StatelessWidget {
final String title;
const _SidebarHeader({
required this.title,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.primaryTextColor,
fontSize: 20,
),
),
],
),
);
}
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CustomCalendarPage extends StatefulWidget {
final DateTime selectedDate;
final Function(int day, int month, int year) onDateChanged;
const CustomCalendarPage({
super.key,
required this.selectedDate,
required this.onDateChanged,
});
@override
State<CustomCalendarPage> createState() => _CustomCalendarPageState();
}
class _CustomCalendarPageState extends State<CustomCalendarPage> {
late DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = widget.selectedDate;
}
@override
void didUpdateWidget(CustomCalendarPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
setState(() {
_selectedDate = widget.selectedDate;
});
}
}
@override
Widget build(BuildContext context) {
final config = CalendarDatePicker2Config(
calendarType: CalendarDatePicker2Type.single,
selectedDayHighlightColor: const Color(0xFF3B82F6),
selectedDayTextStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 14,
),
dayTextStyle: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
weekdayLabelTextStyle: const TextStyle(
color: ColorsManager.grey50,
fontWeight: FontWeight.w400,
fontSize: 14,
),
controlsTextStyle: const TextStyle(
color: Color(0xFF232D3A),
fontWeight: FontWeight.w400,
fontSize: 18,
),
centerAlignModePicker: false,
disableMonthPicker: true,
firstDayOfWeek: 1,
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
);
return CalendarDatePicker2(
config: config,
value: [_selectedDate],
onValueChanged: (dates) {
final picked = dates.first;
if (picked != null) {
setState(() {
_selectedDate = picked;
});
widget.onDateChanged(picked.day, picked.month, picked.year);
}
},
);
}
}

View File

@ -1,60 +0,0 @@
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EventTileWidget extends StatelessWidget {
final List<CalendarEventData<Object?>> events;
const EventTileWidget({
super.key,
required this.events,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final bool isEventEnded =
event.endTime != null && event.endTime!.isBefore(DateTime.now());
return Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('h:mm a').format(event.startTime!),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
);
}).toList(),
),
);
}
}

View File

@ -1,91 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class HatchedColumnBackground extends StatelessWidget {
final Color backgroundColor;
final Color lineColor;
final double opacity;
final double stripeSpacing;
final BorderRadius? borderRadius;
const HatchedColumnBackground({
super.key,
required this.backgroundColor,
required this.lineColor,
this.opacity = 0.15,
this.stripeSpacing = 12,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _HatchedBackgroundPainter(
backgroundColor: backgroundColor,
opacity: opacity,
lineColor: lineColor,
stripeSpacing: stripeSpacing,
borderRadius: borderRadius,
),
size: Size.infinite,
);
}
}
class _HatchedBackgroundPainter extends CustomPainter {
final Color backgroundColor;
final double opacity;
final Color lineColor;
final double stripeSpacing;
final BorderRadius? borderRadius;
_HatchedBackgroundPainter({
required this.backgroundColor,
required this.opacity,
required this.lineColor,
required this.stripeSpacing,
this.borderRadius,
});
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final RRect rrect = borderRadius?.toRRect(rect) ??
RRect.fromRectAndRadius(rect, Radius.zero);
final backgroundPaint = Paint()
..color = backgroundColor.withOpacity(0.02)
..style = PaintingStyle.fill;
canvas.drawRRect(rrect, backgroundPaint);
canvas.save();
canvas.clipRRect(rrect);
final linePaint = Paint()
..color = lineColor
..strokeWidth = 0.5
..style = PaintingStyle.stroke;
final maxExtent =
math.sqrt(size.width * size.width + size.height * size.height);
canvas.translate(0, size.height);
canvas.rotate(-math.pi / 4);
double y = -maxExtent;
while (y < maxExtent) {
canvas.drawLine(
Offset(-maxExtent, y),
Offset(maxExtent, y),
linePaint,
);
y += stripeSpacing;
}
canvas.restore();
}
@override
bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) {
return backgroundColor != oldDelegate.backgroundColor ||
opacity != oldDelegate.opacity ||
lineColor != oldDelegate.lineColor ||
stripeSpacing != oldDelegate.stripeSpacing ||
borderRadius != oldDelegate.borderRadius;
}
}

View File

@ -1,72 +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.black,
this.borderRadius = 10.0,
this.boxShadow = const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
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,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
color: labelColor,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
);
}
}

View File

@ -1,42 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RoomListItem extends StatelessWidget {
final BookableSpaceModel room;
final bool isSelected;
final VoidCallback onTap;
const RoomListItem({
required this.room,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return RadioListTile(
value: room.uuid,
contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
groupValue: isSelected ? room.uuid : null,
visualDensity: const VisualDensity(vertical: -4),
onChanged: (value) => onTap(),
activeColor: ColorsManager.primaryColor,
title: Text(
room.spaceName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w700,
fontSize: 12),
),
subtitle: Text(
room.virtualLocation,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w400,
color: ColorsManager.textGray,
),
),
);
}
}

View File

@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class TimeLineWidget extends StatelessWidget {
final DateTime date;
const TimeLineWidget({Key? key, required this.date}) : super(key: key);
@override
Widget build(BuildContext context) {
int hour =
date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour);
String period = date.hour >= 12 ? 'PM' : 'AM';
return Container(
height: 60,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$hour',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24,
color: ColorsManager.blackColor,
),
),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(left: 2, top: 6),
child: Text(
period,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: ColorsManager.blackColor,
letterSpacing: 1,
),
),
),
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
),
],
),
),
);
}
}

View File

@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekDayHeader extends StatelessWidget {
final DateTime date;
final bool isSelectedDay;
const WeekDayHeader({
Key? key,
required this.date,
required this.isSelectedDay,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color:
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
),
),
],
);
}
}

View File

@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekNavigation extends StatelessWidget {
final DateTime weekStart;
final DateTime weekEnd;
final VoidCallback onPreviousWeek;
final VoidCallback onNextWeek;
const WeekNavigation({
Key? key,
required this.weekStart,
required this.weekEnd,
required this.onPreviousWeek,
required this.onNextWeek,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: onPreviousWeek,
),
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: onNextWeek,
),
],
),
);
}
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
}

View File

@ -1,218 +0,0 @@
import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeeklyCalendarPage extends StatelessWidget {
final DateTime weekStart;
final DateTime selectedDate;
final EventController eventController;
final String? startTime;
final String? endTime;
final DateTime? selectedDateFromSideBarCalender;
const WeeklyCalendarPage({
super.key,
required this.weekStart,
required this.selectedDate,
required this.eventController,
this.startTime,
this.endTime,
this.selectedDateFromSideBarCalender,
});
@override
Widget build(BuildContext context) {
final startHour = _parseHour(startTime, defaultValue: 0);
final endHour = _parseHour(endTime, defaultValue: 24);
if (endTime == null || endTime!.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
color: ColorsManager.lightGrayColor,
size: 80,
),
SizedBox(height: 20),
Text(
'Please select a bookable space to view the calendar.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: ColorsManager.lightGrayColor),
),
],
),
);
}
final weekDays = _getWeekDays(weekStart);
final selectedDayIndex =
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
final selectedSidebarIndex = selectedDateFromSideBarCalender == null
? -1
: weekDays
.indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!));
const double timeLineWidth = 80;
const int totalDays = 7;
final DateTime highlightStart = DateTime(2025, 7, 10);
final DateTime highlightEnd = DateTime(2025, 7, 19);
return LayoutBuilder(
builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth;
final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1;
bool isInRange(DateTime date, DateTime start, DateTime end) {
return !date.isBefore(start) && !date.isAfter(end);
}
return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
WeekView(
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
return isInRange(date, highlightStart, highlightEnd)
? HatchedColumnBackground(
backgroundColor: ColorsManager.grey800,
lineColor: ColorsManager.textGray,
opacity: 0.3,
stripeSpacing: 12,
borderRadius: BorderRadius.circular(8),
)
: const SizedBox();
},
pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart),
controller: eventController,
initialDay: weekStart,
startHour: startHour - 1,
endHour: endHour,
heightPerMinute: 1.1,
showLiveTimeLineInAllDays: false,
showVerticalLines: true,
emulateVerticalOffsetBy: -80,
startDay: WeekDays.monday,
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
showBullet: false,
height: 0,
),
weekDayBuilder: (date) {
return WeekDayHeader(
date: date,
isSelectedDay: isSameDay(date, selectedDate),
);
},
timeLineBuilder: (date) {
return TimeLineWidget(date: date);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
return EventTileWidget(
events: events,
);
},
),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
),
),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
],
),
);
},
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
}
bool isSameDay(DateTime d1, DateTime d2) {
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
}
int _parseHour(String? time, {required int defaultValue}) {
if (time == null || time.isEmpty || !time.contains(':')) {
return defaultValue;
}
try {
return int.parse(time.split(':')[0]);
} catch (e) {
return defaultValue;
}
}

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/presentation/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
@ -29,9 +27,7 @@ class CountdownModeButtons extends StatelessWidget {
children: [
Expanded(
child: DefaultButton(
elevation: 2.5,
height: 40,
borderRadius: 8,
onPressed: () => Navigator.pop(context),
backgroundColor: ColorsManager.boxColor,
child: Text('Cancel', style: context.textTheme.bodyMedium),
@ -41,35 +37,30 @@ class CountdownModeButtons extends StatelessWidget {
Expanded(
child: isActive
? DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: () {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: deviceId,
countdownCode: countDownCode,
),
);
},
backgroundColor: ColorsManager.red100,
backgroundColor: Colors.red,
child: const Text('Stop'),
)
: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40,
onPressed: () {
context.read<ScheduleBloc>().add(
StartScheduleEvent(
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
countDownCode: countDownCode),
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
),
);
},
backgroundColor: ColorsManager.primaryColorWithOpacity,
backgroundColor: ColorsManager.primaryColor,
child: const Text('Save'),
),
),

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: '',
),
);
}
@ -226,7 +216,6 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
index.toString().padLeft(2, '0'),
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: isActive ? ColorsManager.grayColor : Colors.black,
),
),
@ -241,8 +230,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
label,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 24,
fontWeight: FontWeight.w400,
fontSize: 18,
),
),
],

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,24 +18,20 @@ 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) {
return BlocProvider(
create: (_) => ScheduleBloc(deviceId: deviceUuid,)
create: (_) => ScheduleBloc(
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),
@ -56,33 +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: true),
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

@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
Text(
'Scheduling',
style: TextStyle(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.w700,
fontSize: 30,
fontWeight: FontWeight.bold,
fontSize: 22,
color: ColorsManager.dialogBlueTitle,
),
),
Container(

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